Avoid polling in main GUI loop (#245)

Instead of waking up regularly WaitEventsTimeout, WaitEvents and
PostEmptyEvent are used to make the main loop purely event driven. The
rate of redraws due to terminal activity can be tweaked via the new
wakePeriod const.

This leads to some significant performance improvements: Aminal now consumes:
- no CPU when idle (previously ~2.7% on my laptop)
- ~8.5% CPU on my machine when running htop full screen on a large
  monitor (previously ~18.5% on my laptop)
- scrolling large amounts of output is an order of magnitude faster

This change also incidentally fixes data races around the terminal
dirty flag (which is now gone).
This commit is contained in:
Menno Finlay-Smits 2019-03-12 09:57:41 +13:00 committed by Max Risuhin
parent a355d10656
commit 8f183ba440
8 changed files with 291 additions and 190 deletions

View File

@ -21,13 +21,17 @@ const (
SelectionRegionRectangular
)
type Notifier interface {
Notify()
}
type Buffer struct {
lines []Line
displayChangeHandlers []chan bool
savedX uint16
savedY uint16
savedCursorAttr *CellAttributes
dirty bool
dirty Notifier
selectionStart *Position
selectionEnd *Position
selectionMode SelectionMode
@ -54,7 +58,7 @@ func comparePositions(pos1 *Position, pos2 *Position) int {
}
// NewBuffer creates a new terminal buffer
func NewBuffer(terminalState *TerminalState) *Buffer {
func NewBuffer(terminalState *TerminalState, dirty Notifier) *Buffer {
b := &Buffer{
lines: []Line{},
selectionStart: nil,
@ -62,6 +66,7 @@ func NewBuffer(terminalState *TerminalState) *Buffer {
selectionMode: SelectionChar,
isSelectionComplete: true,
terminalState: terminalState,
dirty: dirty,
}
return b
}
@ -251,17 +256,15 @@ func (buffer *Buffer) StartSelection(col uint16, viewRow uint16, mode SelectionM
}
buffer.isSelectionComplete = false
buffer.emitDisplayChange()
buffer.dirty.Notify()
}
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
if buffer.isSelectionComplete {
return
}
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
if buffer.selectionStart == nil {
buffer.selectionEnd = nil
@ -285,7 +288,7 @@ func (buffer *Buffer) ClearSelection() {
buffer.selectionEnd = nil
buffer.isSelectionComplete = true
buffer.emitDisplayChange()
buffer.dirty.Notify()
}
func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) {
@ -368,14 +371,6 @@ func (buffer *Buffer) InSelection(col uint16, row uint16, selectionRegionMode Se
(rawY < end.Line || (rawY == end.Line && int(col) <= end.Col))
}
func (buffer *Buffer) IsDirty() bool {
if !buffer.dirty {
return false
}
buffer.dirty = false
return true
}
func (buffer *Buffer) HasScrollableRegion() bool {
return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1
}
@ -395,7 +390,7 @@ func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) {
}
func (buffer *Buffer) AreaScrollDown(lines uint16) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
// NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange()
@ -411,7 +406,7 @@ func (buffer *Buffer) AreaScrollDown(lines uint16) {
}
func (buffer *Buffer) AreaScrollUp(lines uint16) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
// NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange()
@ -471,10 +466,6 @@ func (buffer *Buffer) GetRawCell(viewCol uint16, rawLine uint64) *Cell {
return &line.cells[viewCol]
}
func (buffer *Buffer) emitDisplayChange() {
buffer.dirty = true
}
// Column returns cursor column
func (buffer *Buffer) CursorColumn() uint16 {
// @todo originMode and left margin
@ -550,8 +541,7 @@ func (buffer *Buffer) deleteLine() {
}
func (buffer *Buffer) insertLine() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
if !buffer.InScrollableRegion() {
pos := buffer.RawLine()
@ -636,7 +626,7 @@ func (buffer *Buffer) Index() {
// This sequence causes the active position to move downward one line without changing the column position.
// If the active position is at the bottom margin, a scroll up is performed."
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
if buffer.InScrollableRegion() {
@ -662,8 +652,7 @@ func (buffer *Buffer) Index() {
}
func (buffer *Buffer) ReverseIndex() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin {
buffer.AreaScrollDown(1)
@ -674,7 +663,8 @@ func (buffer *Buffer) ReverseIndex() {
// Write will write a rune to the terminal at the position of the cursor, and increment the cursor position
func (buffer *Buffer) Write(runes ...rune) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
// scroll to bottom on input
buffer.terminalState.scrollLinesFromBottom = 0
@ -837,7 +827,7 @@ func (buffer *Buffer) MovePosition(x int16, y int16) {
}
func (buffer *Buffer) SetPosition(col uint16, line uint16) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
useCol := col
useLine := line
@ -876,7 +866,7 @@ func (buffer *Buffer) GetVisibleLines() []Line {
// tested to here
func (buffer *Buffer) Clear() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
for i := 0; i < int(buffer.ViewHeight()); i++ {
buffer.lines = append(buffer.lines, newLine())
}
@ -916,13 +906,13 @@ func (buffer *Buffer) getViewLine(index uint16) *Line {
}
func (buffer *Buffer) EraseLine() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
line.cells = []Cell{}
}
func (buffer *Buffer) EraseLineToCursor() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
if i < len(line.cells) {
@ -932,7 +922,7 @@ func (buffer *Buffer) EraseLineToCursor() {
}
func (buffer *Buffer) EraseLineFromCursor() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
if len(line.cells) > 0 {
@ -950,7 +940,7 @@ func (buffer *Buffer) EraseLineFromCursor() {
}
func (buffer *Buffer) EraseDisplay() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
for i := uint16(0); i < (buffer.ViewHeight()); i++ {
rawLine := buffer.convertViewLineToRawLine(i)
if int(rawLine) < len(buffer.lines) {
@ -960,7 +950,7 @@ func (buffer *Buffer) EraseDisplay() {
}
func (buffer *Buffer) DeleteChars(n int) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
if int(buffer.terminalState.cursorX) >= len(line.cells) {
@ -975,7 +965,7 @@ func (buffer *Buffer) DeleteChars(n int) {
}
func (buffer *Buffer) EraseCharacters(n int) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
@ -990,7 +980,7 @@ func (buffer *Buffer) EraseCharacters(n int) {
}
func (buffer *Buffer) EraseDisplayFromCursor() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
max := int(buffer.terminalState.cursorX)
@ -1006,7 +996,7 @@ func (buffer *Buffer) EraseDisplayFromCursor() {
}
func (buffer *Buffer) EraseDisplayToCursor() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
@ -1024,8 +1014,7 @@ func (buffer *Buffer) EraseDisplayToCursor() {
}
func (buffer *Buffer) ResizeView(width uint16, height uint16) {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
if buffer.terminalState.viewHeight == 0 {
buffer.terminalState.viewWidth = width
@ -1162,7 +1151,7 @@ func (buffer *Buffer) CompareViewLines(path string) bool {
}
func (buffer *Buffer) ReverseVideo() {
defer buffer.emitDisplayChange()
defer buffer.dirty.Notify()
for _, line := range buffer.lines {
line.ReverseVideo()

View File

@ -9,8 +9,24 @@ import (
"github.com/stretchr/testify/assert"
)
func TestBufferCreation(t *testing.T) {
b, n := makeBufferForTesting(10, 20)
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(20), b.ViewHeight())
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
assert.NotNil(t, b.lines)
n.AssertNotNotified(t)
}
func TestWriteNotify(t *testing.T) {
b, n := makeBufferForTesting(30, 3)
b.Write(rune('x'))
n.AssertNotified(t)
}
func TestTabbing(t *testing.T) {
b := NewBuffer(NewTerminalState(30, 3, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(30, 3)
b.Write([]rune("hello")...)
b.Tab()
b.Write([]rune("x")...)
@ -39,7 +55,7 @@ hell xxx good
}
func TestOffsets(t *testing.T) {
b := NewBuffer(NewTerminalState(10, 3, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(10, 3)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
@ -59,18 +75,8 @@ func TestOffsets(t *testing.T) {
assert.Equal(t, 5, b.Height())
}
func TestBufferCreation(t *testing.T) {
b := NewBuffer(NewTerminalState(10, 20, CellAttributes{}, 1000))
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(20), b.ViewHeight())
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
assert.NotNil(t, b.lines)
}
func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
b := NewBuffer(NewTerminalState(5, 4, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(5, 4)
/*01234
|-----
@ -117,7 +123,7 @@ func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
}
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(3, 20)
b.terminalState.LineFeedMode = false
b.Write('a', 'b', 'c')
@ -142,7 +148,7 @@ func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
}
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(3, 20)
b.terminalState.LineFeedMode = false
/*
|abc
@ -170,46 +176,59 @@ func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
}
func TestSetPosition(t *testing.T) {
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
b, n := makeBufferForTesting(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.SetPosition(60, 10)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 10, int(b.CursorLine()))
n.AssertNotified(t)
b.SetPosition(0, 0)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
n.AssertNotified(t)
b.SetPosition(120, 90)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
n.AssertNotified(t)
}
func TestMovePosition(t *testing.T) {
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
b, n := makeBufferForTesting(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.MovePosition(-1, -1)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
n.AssertNotified(t)
b.MovePosition(30, 20)
assert.Equal(t, 30, int(b.CursorColumn()))
assert.Equal(t, 20, int(b.CursorLine()))
n.AssertNotified(t)
b.MovePosition(30, 20)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 40, int(b.CursorLine()))
n.AssertNotified(t)
b.MovePosition(-1, -1)
assert.Equal(t, 59, int(b.CursorColumn()))
assert.Equal(t, 39, int(b.CursorLine()))
n.AssertNotified(t)
b.MovePosition(100, 100)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
n.AssertNotified(t)
}
func TestVisibleLines(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 10)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
@ -259,7 +278,7 @@ func TestVisibleLines(t *testing.T) {
}
func TestClearWithoutFullView(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 10)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
@ -275,7 +294,7 @@ func TestClearWithoutFullView(t *testing.T) {
}
func TestClearWithFullView(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
@ -306,7 +325,7 @@ func TestClearWithFullView(t *testing.T) {
}
func TestCarriageReturn(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 20)
b.Write([]rune("hello!")...)
b.CarriageReturn()
b.Write([]rune("secret")...)
@ -315,7 +334,7 @@ func TestCarriageReturn(t *testing.T) {
}
func TestCarriageReturnOnFullLine(t *testing.T) {
b := NewBuffer(NewTerminalState(20, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(20, 20)
b.Write([]rune("abcdeabcdeabcdeabcde")...)
b.CarriageReturn()
b.Write([]rune("xxxxxxxxxxxxxxxxxxxx")...)
@ -324,7 +343,7 @@ func TestCarriageReturnOnFullLine(t *testing.T) {
}
func TestCarriageReturnOnFullLastLine(t *testing.T) {
b := NewBuffer(NewTerminalState(20, 2, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(20, 2)
b.NewLine()
b.Write([]rune("abcdeabcdeabcdeabcde")...)
b.CarriageReturn()
@ -335,7 +354,7 @@ func TestCarriageReturnOnFullLastLine(t *testing.T) {
}
func TestCarriageReturnOnWrappedLine(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 6, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 6)
b.Write([]rune("hello!")...)
b.CarriageReturn()
b.Write([]rune("secret")...)
@ -345,7 +364,7 @@ func TestCarriageReturnOnWrappedLine(t *testing.T) {
}
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
b := NewBuffer(NewTerminalState(6, 10, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(6, 10)
b.terminalState.cursorY = 3
b.CarriageReturn()
assert.Equal(t, uint16(0), b.terminalState.cursorX)
@ -353,7 +372,7 @@ func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
}
func TestGetCell(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 20)
b.Write([]rune("Hello")...)
b.CarriageReturn()
b.NewLine()
@ -369,7 +388,7 @@ func TestGetCell(t *testing.T) {
}
func TestGetCellWithHistory(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 2)
b.Write([]rune("Hello")...)
b.CarriageReturn()
@ -387,7 +406,7 @@ func TestGetCellWithHistory(t *testing.T) {
}
func TestGetCellWithBadCursor(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 2)
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
require.Nil(t, b.GetCell(8, 3))
require.Nil(t, b.GetCell(90, 0))
@ -395,12 +414,12 @@ func TestGetCellWithBadCursor(t *testing.T) {
}
func TestCursorAttr(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 2)
assert.Equal(t, &b.terminalState.CursorAttr, b.CursorAttr())
}
func TestCursorPositionQuerying(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 20)
b.terminalState.cursorX = 17
b.terminalState.cursorY = 9
assert.Equal(t, b.terminalState.cursorX, b.CursorColumn())
@ -408,7 +427,7 @@ func TestCursorPositionQuerying(t *testing.T) {
}
func TestRawPositionQuerying(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
@ -445,7 +464,7 @@ func TestRawPositionQuerying(t *testing.T) {
// CSI 2 K
func TestEraseLine(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
@ -457,7 +476,7 @@ func TestEraseLine(t *testing.T) {
// CSI 1 K
func TestEraseLineToCursor(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
@ -471,7 +490,7 @@ func TestEraseLineToCursor(t *testing.T) {
// CSI 0 K
func TestEraseLineAfterCursor(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
@ -482,7 +501,7 @@ func TestEraseLineAfterCursor(t *testing.T) {
assert.Equal(t, "dele", b.lines[1].String())
}
func TestEraseDisplay(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
@ -498,7 +517,7 @@ func TestEraseDisplay(t *testing.T) {
}
}
func TestEraseDisplayToCursor(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
@ -516,7 +535,7 @@ func TestEraseDisplayToCursor(t *testing.T) {
}
func TestEraseDisplayFromCursor(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
@ -532,7 +551,7 @@ func TestEraseDisplayFromCursor(t *testing.T) {
assert.Equal(t, "", lines[2].String())
}
func TestBackspace(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 5)
b.Write([]rune("hello")...)
b.Backspace()
b.Backspace()
@ -542,7 +561,7 @@ func TestBackspace(t *testing.T) {
}
func TestHorizontalResizeView(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
b, _ := makeBufferForTesting(80, 10)
// 60 characters
b.Write([]rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
@ -626,7 +645,7 @@ dbye
*/
func TestBufferMaxLines(t *testing.T) {
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2))
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2), new(testNotifier))
b.terminalState.LineFeedMode = false
b.Write([]rune("hello")...)
@ -640,30 +659,20 @@ func TestBufferMaxLines(t *testing.T) {
assert.Equal(t, "world", b.lines[1].String())
}
func makeBufferForTestingSelection() *Buffer {
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 10))
b.terminalState.LineFeedMode = false
b.Write([]rune("The quick brown")...)
b.NewLine()
b.Write([]rune("fox jumps over")...)
b.NewLine()
b.Write([]rune("the lazy dog")...)
return b
}
func TestSelectingChars(t *testing.T) {
b := makeBufferForTestingSelection()
b, n := makeBufferForTestingSelection()
b.StartSelection(2, 0, SelectionChar)
n.AssertNotified(t)
b.ExtendSelection(4, 1, true)
n.AssertNotified(t)
assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText(SelectionRegionNormal))
}
func TestSelectingWordsDown(t *testing.T) {
b := makeBufferForTestingSelection()
b, _ := makeBufferForTestingSelection()
b.StartSelection(6, 1, SelectionWord)
b.ExtendSelection(5, 2, true)
@ -672,7 +681,7 @@ func TestSelectingWordsDown(t *testing.T) {
}
func TestSelectingWordsUp(t *testing.T) {
b := makeBufferForTestingSelection()
b, _ := makeBufferForTestingSelection()
b.StartSelection(5, 2, SelectionWord)
b.ExtendSelection(6, 1, true)
@ -681,7 +690,7 @@ func TestSelectingWordsUp(t *testing.T) {
}
func TestSelectingLinesDown(t *testing.T) {
b := makeBufferForTestingSelection()
b, _ := makeBufferForTestingSelection()
b.StartSelection(6, 1, SelectionLine)
b.ExtendSelection(4, 2, true)
@ -690,7 +699,7 @@ func TestSelectingLinesDown(t *testing.T) {
}
func TestSelectingLineUp(t *testing.T) {
b := makeBufferForTestingSelection()
b, _ := makeBufferForTestingSelection()
b.StartSelection(8, 2, SelectionLine)
b.ExtendSelection(3, 1, true)
@ -699,7 +708,7 @@ func TestSelectingLineUp(t *testing.T) {
}
func TestSelectingAfterText(t *testing.T) {
b := makeBufferForTestingSelection()
b, _ := makeBufferForTestingSelection()
b.StartSelection(6, 3, SelectionChar)
b.ExtendSelection(6, 3, true)
@ -711,3 +720,41 @@ func TestSelectingAfterText(t *testing.T) {
assert.Equal(t, end.Col, 79)
assert.Equal(t, end.Line, 3)
}
func makeBufferForTestingSelection() (*Buffer, *testNotifier) {
b, n := makeBufferForTesting(80, 10)
b.terminalState.LineFeedMode = false
b.Write([]rune("The quick brown")...)
b.NewLine()
b.Write([]rune("fox jumps over")...)
b.NewLine()
b.Write([]rune("the lazy dog")...)
return b, n
}
func makeBufferForTesting(cols, rows uint16) (*Buffer, *testNotifier) {
n := new(testNotifier)
b := NewBuffer(NewTerminalState(cols, rows, CellAttributes{}, 100), n)
return b, n
}
// testNotifier implements the Notifier interface and provides helpers
// for checking it Notify was called (or not).
type testNotifier struct {
notified bool
}
func (n *testNotifier) Notify() {
n.notified = true
}
func (n *testNotifier) AssertNotified(t *testing.T) {
assert.True(t, n.notified)
n.notified = false
}
func (n *testNotifier) AssertNotNotified(t *testing.T) {
assert.False(t, n.notified)
}

View File

@ -34,7 +34,7 @@ func actionPaste(gui *GUI) {
func actionToggleDebug(gui *GUI) {
gui.showDebugInfo = !gui.showDebugInfo
gui.terminal.SetDirty()
gui.terminal.NotifyDirty()
}
func actionSearchSelection(gui *GUI) {

View File

@ -24,6 +24,12 @@ import (
"go.uber.org/zap"
)
// wakePeriod controls how often the main loop is woken up. This has
// significant impact on how Aminal feels to use. Adjust with care and
// test changes on all supported platforms.
const wakePeriod = time.Second / 120
const halfWakePeriod = wakePeriod / 2
type GUI struct {
window *glfw.Window
logger *zap.SugaredLogger
@ -362,11 +368,11 @@ func (gui *GUI) Render() error {
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
gui.window.SetRefreshCallback(func(w *glfw.Window) {
gui.terminal.SetDirtyLocked()
gui.terminal.NotifyDirty()
})
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
if focused {
gui.terminal.SetDirtyLocked()
gui.terminal.NotifyDirty()
}
})
gui.window.SetPosCallback(gui.windowPosChangeCallback)
@ -419,82 +425,119 @@ func (gui *GUI) Render() error {
r, err := version.GetNewerRelease()
if err == nil && r != nil {
latestVersion = r.TagName
gui.terminal.SetDirty()
gui.terminal.NotifyDirty()
}
}()
startTime := time.Now()
showMessage := true
stop := make(chan struct{})
go gui.waker(stop)
for !gui.window.ShouldClose() {
gui.redraw()
forceRedraw := false
select {
case <-titleChan:
gui.window.SetTitle(gui.terminal.GetTitle())
case <-resizeChan:
gui.resizeToTerminal()
case reverse := <-reverseChan:
gui.generateDefaultCell(reverse)
forceRedraw = true
default:
// this is more efficient than glfw.PollEvents()
glfw.WaitEventsTimeout(0.02) // up to 50fps on no input, otherwise higher
}
if gui.terminal.CheckDirty() || forceRedraw {
gui.redraw(true)
if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
View Size: %d,%d
Buffer Size: %d lines
`,
gui.terminal.GetLogicalCursorX(),
gui.terminal.GetLogicalCursorY(),
gui.terminal.ActiveBuffer().ViewWidth(),
gui.terminal.ActiveBuffer().ViewHeight(),
gui.terminal.ActiveBuffer().Height(),
),
[3]float32{1, 1, 1},
[3]float32{0.8, 0, 0},
)
}
if showMessage {
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.SetDirtyLocked)
_, h := gui.terminal.GetSize()
var msg string
if version.Version == "" {
msg = "You are using a development build of Aminal."
} else {
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
}
gui.textbox(
2,
uint16(h-3),
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
[3]float32{1, 1, 1},
[3]float32{0, 0.5, 0},
)
} else {
showMessage = false
}
}
gui.SwapBuffers()
gui.terminal.GetLogicalCursorX(),
gui.terminal.GetLogicalCursorY(),
gui.terminal.ActiveBuffer().ViewWidth(),
gui.terminal.ActiveBuffer().ViewHeight(),
gui.terminal.ActiveBuffer().Height(),
),
[3]float32{1, 1, 1},
[3]float32{0.8, 0, 0},
)
}
if showMessage {
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.NotifyDirty)
_, h := gui.terminal.GetSize()
var msg string
if version.Version == "" {
msg = "You are using a development build of Aminal."
} else {
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
}
gui.textbox(
2,
uint16(h-3),
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
[3]float32{1, 1, 1},
[3]float32{0, 0.5, 0},
)
} else {
showMessage = false
}
}
gui.SwapBuffers()
glfw.WaitEvents() // Go to sleep until next event.
// Process any terminal events since the last wakeup.
terminalEvents:
for {
select {
case <-titleChan:
gui.window.SetTitle(gui.terminal.GetTitle())
case <-resizeChan:
gui.resizeToTerminal()
case reverse := <-reverseChan:
gui.generateDefaultCell(reverse)
default:
break terminalEvents
}
}
}
close(stop) // Tell waker to end.
gui.logger.Debugf("Stopping render...")
return nil
}
// waker is a goroutine which listens to the terminal's dirty channel,
// waking up the main thread when the GUI needs to be
// redrawn. Limiting is applied on wakeups to avoid excessive CPU
// usage when the terminal is being updated rapidly.
func (gui *GUI) waker(stop <-chan struct{}) {
dirty := gui.terminal.Dirty()
var nextWake <-chan time.Time
var last time.Time
for {
select {
case <-dirty:
if nextWake == nil {
if time.Since(last) > wakePeriod {
// There hasn't been a wakeup recently so schedule
// the next one sooner.
nextWake = time.After(halfWakePeriod)
} else {
nextWake = time.After(wakePeriod)
}
}
case last = <-nextWake:
// TODO(mjs) - This is somewhat of a voodoo sleep but it
// avoid various rendering issues on Windows in some
// situations. Suspect that this will become unnecessary
// once various goroutine synchronisation issues have been
// resolved.
time.Sleep(halfWakePeriod)
glfw.PostEmptyEvent()
nextWake = nil
case <-stop:
return
}
}
}
func (gui *GUI) renderTerminalData(shouldLock bool) {
if shouldLock {
gui.terminal.Lock()

View File

@ -5,8 +5,8 @@ type overlay interface {
}
func (gui *GUI) setOverlay(m overlay) {
defer gui.terminal.SetDirtyLocked()
gui.overlay = m
gui.terminal.NotifyDirty()
}
func (gui *GUI) renderOverlay() {

19
terminal/notify.go Normal file
View File

@ -0,0 +1,19 @@
package terminal
func newNotifier() *notifier {
return &notifier{
C: make(chan struct{}, 1),
}
}
type notifier struct {
C chan struct{}
}
// Notify is used to signal an event in a non-blocking way.
func (n *notifier) Notify() {
select {
case n.C <- struct{}{}:
default:
}
}

View File

@ -28,25 +28,25 @@ var runeMap = map[rune]runeHandler{
func newLineHandler(terminal *Terminal) error {
terminal.ActiveBuffer().NewLine()
terminal.isDirty = true
terminal.NotifyDirty()
return nil
}
func tabHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Tab()
terminal.isDirty = true
terminal.NotifyDirty()
return nil
}
func carriageReturnHandler(terminal *Terminal) error {
terminal.ActiveBuffer().CarriageReturn()
terminal.isDirty = true
terminal.NotifyDirty()
return nil
}
func backspaceHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Backspace()
terminal.isDirty = true
terminal.NotifyDirty()
return nil
}
@ -80,16 +80,16 @@ func (terminal *Terminal) processRuneLocked(b rune) {
}
func (terminal *Terminal) processRune(b rune) {
defer terminal.NotifyDirty()
if handler, ok := runeMap[b]; ok {
if err := handler(terminal); err != nil {
terminal.logger.Errorf("Error handling control code: %s", err)
}
terminal.isDirty = true
return
}
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
terminal.ActiveBuffer().Write(terminal.translateRune(b))
terminal.isDirty = true
}
func (terminal *Terminal) translateRune(b rune) rune {
@ -129,7 +129,7 @@ func (terminal *Terminal) processInput(pty chan rune) {
if err := ansiHandler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err)
}
terminal.SetDirtyLocked()
terminal.NotifyDirty()
continue
}

View File

@ -50,12 +50,12 @@ type Terminal struct {
mouseMode MouseMode
mouseExtMode MouseExtMode
bracketedPasteMode bool
isDirty bool
charWidth float32
charHeight float32
lastBuffer uint8
terminalState *buffer.TerminalState
platformDependentSettings platform.PlatformDependentSettings
dirty *notifier
}
type Modes struct {
@ -85,15 +85,28 @@ func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Te
ShowCursor: true,
},
platformDependentSettings: pty.GetPlatformDependentSettings(),
dirty: newNotifier(),
}
t.buffers = []*buffer.Buffer{
buffer.NewBuffer(t.terminalState),
buffer.NewBuffer(t.terminalState),
buffer.NewBuffer(t.terminalState),
buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState, t.dirty),
}
t.activeBuffer = t.buffers[0]
return t
return t
}
// Dirty returns a channel that receives an empty struct whenever the
// terminal becomes dirty.
func (terminal *Terminal) Dirty() <-chan struct{} {
return terminal.dirty.C
}
// NotifyDirty is used to signal that the terminal is dirty and the
// screen must be redrawn.
func (terminal *Terminal) NotifyDirty() {
terminal.dirty.Notify()
}
func (terminal *Terminal) SetProgram(program uint32) {
@ -104,19 +117,6 @@ func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
terminal.bracketedPasteMode = enabled
}
func (terminal *Terminal) CheckDirty() bool {
terminal.Lock()
defer terminal.Unlock()
d := terminal.isDirty
terminal.isDirty = false
return d || terminal.ActiveBuffer().IsDirty()
}
func (terminal *Terminal) SetDirty() {
terminal.isDirty = true
}
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
return terminal.modes.ApplicationCursorKeys
}
@ -174,7 +174,7 @@ func (terminal *Terminal) GetScrollOffset() uint {
}
func (terminal *Terminal) ScreenScrollDown(lines uint16) {
defer terminal.SetDirty()
defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) {
@ -202,7 +202,7 @@ func (terminal *Terminal) AreaScrollDown(lines uint16) {
}
func (terminal *Terminal) ScreenScrollUp(lines uint16) {
defer terminal.SetDirty()
defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) {
@ -226,8 +226,8 @@ func (terminal *Terminal) ScrollPageUp() {
}
func (terminal *Terminal) ScrollToEnd() {
defer terminal.SetDirty()
terminal.terminalState.SetScrollOffset(0)
terminal.NotifyDirty()
}
func (terminal *Terminal) GetVisibleLines() []buffer.Line {
@ -301,6 +301,7 @@ func (terminal *Terminal) GetTitle() string {
func (terminal *Terminal) SetTitle(title string) {
terminal.title = title
terminal.emitTitleChange()
terminal.NotifyDirty()
}
// Write sends data, i.e. locally typed keystrokes to the pty
@ -378,6 +379,7 @@ func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
terminal.emitResize()
terminal.NotifyDirty()
return nil
}
@ -424,6 +426,7 @@ func (terminal *Terminal) SetScreenMode(enabled bool) {
buffer.ReverseVideo()
}
terminal.emitReverse(enabled)
terminal.NotifyDirty()
}
func (terminal *Terminal) Lock() {