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

View File

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

View File

@ -24,6 +24,12 @@ import (
"go.uber.org/zap" "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 { type GUI struct {
window *glfw.Window window *glfw.Window
logger *zap.SugaredLogger logger *zap.SugaredLogger
@ -362,11 +368,11 @@ func (gui *GUI) Render() error {
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback) gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
gui.window.SetCursorPosCallback(gui.mouseMoveCallback) gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
gui.window.SetRefreshCallback(func(w *glfw.Window) { gui.window.SetRefreshCallback(func(w *glfw.Window) {
gui.terminal.SetDirtyLocked() gui.terminal.NotifyDirty()
}) })
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) { gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
if focused { if focused {
gui.terminal.SetDirtyLocked() gui.terminal.NotifyDirty()
} }
}) })
gui.window.SetPosCallback(gui.windowPosChangeCallback) gui.window.SetPosCallback(gui.windowPosChangeCallback)
@ -419,33 +425,18 @@ func (gui *GUI) Render() error {
r, err := version.GetNewerRelease() r, err := version.GetNewerRelease()
if err == nil && r != nil { if err == nil && r != nil {
latestVersion = r.TagName latestVersion = r.TagName
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
} }
}() }()
startTime := time.Now() startTime := time.Now()
showMessage := true showMessage := true
stop := make(chan struct{})
go gui.waker(stop)
for !gui.window.ShouldClose() { 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 { if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
@ -465,7 +456,7 @@ Buffer Size: %d lines
if showMessage { if showMessage {
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 { if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.SetDirtyLocked) time.AfterFunc(time.Second, gui.terminal.NotifyDirty)
_, h := gui.terminal.GetSize() _, h := gui.terminal.GetSize()
var msg string var msg string
if version.Version == "" { if version.Version == "" {
@ -486,15 +477,67 @@ Buffer Size: %d lines
} }
gui.SwapBuffers() 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...") gui.logger.Debugf("Stopping render...")
return nil 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) { func (gui *GUI) renderTerminalData(shouldLock bool) {
if shouldLock { if shouldLock {
gui.terminal.Lock() gui.terminal.Lock()

View File

@ -5,8 +5,8 @@ type overlay interface {
} }
func (gui *GUI) setOverlay(m overlay) { func (gui *GUI) setOverlay(m overlay) {
defer gui.terminal.SetDirtyLocked()
gui.overlay = m gui.overlay = m
gui.terminal.NotifyDirty()
} }
func (gui *GUI) renderOverlay() { 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 { func newLineHandler(terminal *Terminal) error {
terminal.ActiveBuffer().NewLine() terminal.ActiveBuffer().NewLine()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func tabHandler(terminal *Terminal) error { func tabHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Tab() terminal.ActiveBuffer().Tab()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func carriageReturnHandler(terminal *Terminal) error { func carriageReturnHandler(terminal *Terminal) error {
terminal.ActiveBuffer().CarriageReturn() terminal.ActiveBuffer().CarriageReturn()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func backspaceHandler(terminal *Terminal) error { func backspaceHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Backspace() terminal.ActiveBuffer().Backspace()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
@ -80,16 +80,16 @@ func (terminal *Terminal) processRuneLocked(b rune) {
} }
func (terminal *Terminal) processRune(b rune) { func (terminal *Terminal) processRune(b rune) {
defer terminal.NotifyDirty()
if handler, ok := runeMap[b]; ok { if handler, ok := runeMap[b]; ok {
if err := handler(terminal); err != nil { if err := handler(terminal); err != nil {
terminal.logger.Errorf("Error handling control code: %s", err) terminal.logger.Errorf("Error handling control code: %s", err)
} }
terminal.isDirty = true
return return
} }
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b)) //terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
terminal.ActiveBuffer().Write(terminal.translateRune(b)) terminal.ActiveBuffer().Write(terminal.translateRune(b))
terminal.isDirty = true
} }
func (terminal *Terminal) translateRune(b rune) rune { 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 { if err := ansiHandler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err) terminal.logger.Errorf("Error handling escape sequence: %s", err)
} }
terminal.SetDirtyLocked() terminal.NotifyDirty()
continue continue
} }

View File

@ -50,12 +50,12 @@ type Terminal struct {
mouseMode MouseMode mouseMode MouseMode
mouseExtMode MouseExtMode mouseExtMode MouseExtMode
bracketedPasteMode bool bracketedPasteMode bool
isDirty bool
charWidth float32 charWidth float32
charHeight float32 charHeight float32
lastBuffer uint8 lastBuffer uint8
terminalState *buffer.TerminalState terminalState *buffer.TerminalState
platformDependentSettings platform.PlatformDependentSettings platformDependentSettings platform.PlatformDependentSettings
dirty *notifier
} }
type Modes struct { type Modes struct {
@ -85,15 +85,28 @@ func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Te
ShowCursor: true, ShowCursor: true,
}, },
platformDependentSettings: pty.GetPlatformDependentSettings(), platformDependentSettings: pty.GetPlatformDependentSettings(),
dirty: newNotifier(),
} }
t.buffers = []*buffer.Buffer{ t.buffers = []*buffer.Buffer{
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
} }
t.activeBuffer = t.buffers[0] 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) { func (terminal *Terminal) SetProgram(program uint32) {
@ -104,19 +117,6 @@ func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
terminal.bracketedPasteMode = enabled 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 { func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
return terminal.modes.ApplicationCursorKeys return terminal.modes.ApplicationCursorKeys
} }
@ -174,7 +174,7 @@ func (terminal *Terminal) GetScrollOffset() uint {
} }
func (terminal *Terminal) ScreenScrollDown(lines uint16) { func (terminal *Terminal) ScreenScrollDown(lines uint16) {
defer terminal.SetDirty() defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer() buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) { if buffer.Height() < int(buffer.ViewHeight()) {
@ -202,7 +202,7 @@ func (terminal *Terminal) AreaScrollDown(lines uint16) {
} }
func (terminal *Terminal) ScreenScrollUp(lines uint16) { func (terminal *Terminal) ScreenScrollUp(lines uint16) {
defer terminal.SetDirty() defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer() buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) { if buffer.Height() < int(buffer.ViewHeight()) {
@ -226,8 +226,8 @@ func (terminal *Terminal) ScrollPageUp() {
} }
func (terminal *Terminal) ScrollToEnd() { func (terminal *Terminal) ScrollToEnd() {
defer terminal.SetDirty()
terminal.terminalState.SetScrollOffset(0) terminal.terminalState.SetScrollOffset(0)
terminal.NotifyDirty()
} }
func (terminal *Terminal) GetVisibleLines() []buffer.Line { func (terminal *Terminal) GetVisibleLines() []buffer.Line {
@ -301,6 +301,7 @@ func (terminal *Terminal) GetTitle() string {
func (terminal *Terminal) SetTitle(title string) { func (terminal *Terminal) SetTitle(title string) {
terminal.title = title terminal.title = title
terminal.emitTitleChange() terminal.emitTitleChange()
terminal.NotifyDirty()
} }
// Write sends data, i.e. locally typed keystrokes to the pty // 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.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
terminal.emitResize() terminal.emitResize()
terminal.NotifyDirty()
return nil return nil
} }
@ -424,6 +426,7 @@ func (terminal *Terminal) SetScreenMode(enabled bool) {
buffer.ReverseVideo() buffer.ReverseVideo()
} }
terminal.emitReverse(enabled) terminal.emitReverse(enabled)
terminal.NotifyDirty()
} }
func (terminal *Terminal) Lock() { func (terminal *Terminal) Lock() {