diff --git a/.travis.yml b/.travis.yml index c30aaba..64fe1cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ script: - if [[ $TRAVIS_OS_NAME == 'osx' ]]; then make build-darwin-native-travis; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make build-linux-travis; fi - if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make windows-cross-compile-travis; fi -- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make check-gofmt; fi +- if [[ $TRAVIS_OS_NAME == 'linux' && $TRAVIS_GO_VERSION =~ ^1\.11\. ]]; then echo 'check-gofmt'; make check-gofmt; fi env: global: - secure: "pdRpTOGQSUgbC9tK37voxUYJHMWDPJEmdMhNBsljpP9VnxxbR6JEFwvOQEmUHGlsYv8jma6a17jE60ngVQk8QP12cPh48i2bdbVgym/zTUOKFawCtPAzs8i7evh0di5eZ3uoyc42kG4skc+ePuVHbXC8jDxwaPpMqSHD7QyQc1/6ckI9LLkyWUqhnJJXkVwhmI74Aa1Im6QhywAWFMeTBRRL02cwr6k7VKSYOn6yrtzJRCALFGpZ/n58lPrpDxN7W8o+HRQP89wIDy8FyNeEPdmqGFNfMHDvI3oJRN4dGC4H9EkKf/iGuNJia1Bs+MgaG9kKlMHsI6Fkh5uw9KNTvC1llx43VRQJzm26cn1CpRxxRtF4F8lqkpY4tHjxxCitV+98ddW8jdmQYyx+LeueC5wqlO9g2M5L3oXsGMqZ++mDRDa8oQoQAVUSVtimeO8ODXFuVNR8TlupP0Cthgucil63VUZfAD8EHc2zpRSFxfYByDH53uMEinn20uovL6W42fqgboC43HOnR6aVfSANPsBFDlcpZFa2BY5RkcKyYdaLkucy0DKJ946UDfhOu6FNm0GPHq5HcgWkLojNF0dEFgG6J+SGQGiPjxTlHP/zoe61qMlWu+fYRXQnKWZN5Kk0T1TbAk6pKSE6wRLG8ddxvMg+eVpGLT+gAvQdrrkMFvs=" diff --git a/Makefile b/Makefile index b3c2c93..deb15d5 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ build: .PHONY: test test: - go test -v ./... + go test -v -timeout=8m ./... go vet -v .PHONY: check-gofmt diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index c6a8195..fde5165 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -29,6 +29,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi ## Checklist: +- [ ] I have run `gofmt` on the project - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas diff --git a/README.md b/README.md index bfe5e8a..26d14ff 100644 --- a/README.md +++ b/README.md @@ -114,28 +114,29 @@ shell = "/bin/bash" # The shell to run for the terminal session. Default search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replace when searching. max_lines = 1000 # Maximum number of lines in the terminal buffer. copy_and_paste_with_mouse = true # Text selected with the mouse is copied to the clipboard on end selection, and is pasted on right mouse button click. +show_vertical_scrollbar = true # Whether to show the vertical scrollbar dpi-scale = 0.0 # Override DPI scale. Defaults to 0.0 (let Aminal determine the DPI scale itself). [colours] cursor = "#e8dfd6" foreground = "#e8dfd6" background = "#021b21" - black = "#032c36" - red = "#c2454e" - green = "#7cbf9e" - yellow = "#8a7a63" - blue = "#065f73" - magenta = "#ff5879" - cyan = "#44b5b1" - light_grey = "#f2f1b9" - dark_grey = "#3e4360" - light_red = "#ef5847" - light_green = "#a2db91" - light_yellow = "#beb090" - light_blue = "#61778d" - light_magenta = "#ff99a1" - light_cyan = "#9ed9d8" - white = "#f6f6c9" + black = "#000000" + red = "#800000" + green = "#008000" + yellow = "#808000" + blue = "#000080" + magenta = "#800080" + cyan = "#008080" + light_grey = "#f2f2f2" + dark_grey = "#808080" + light_red = "#ff0000" + light_green = "#00ff00" + light_yellow = "#ffff00" + light_blue = "#0000ff" + light_magenta = "#ff00ff" + light_cyan = "#00ffff" + white = "#ffffff" selection = "#333366" # Mouse selection background colour [keys] diff --git a/buffer/buffer.go b/buffer/buffer.go index 80670e8..a362a69 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -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 } @@ -210,7 +215,7 @@ func (buffer *Buffer) GetSelectedText(selectionRegionMode SelectionRegionMode) s maxX := int(buffer.terminalState.viewWidth) - 1 if row == start.Line { minX = start.Col - } else if !line.wrapped { + } else if !line.wrapped && !line.nobreak { builder.WriteString("\n") } if row == end.Line { @@ -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 @@ -527,6 +518,15 @@ func (buffer *Buffer) convertRawLineToViewLine(rawLine uint64) uint16 { return uint16(int(rawLine) - (rawHeight - int(buffer.terminalState.viewHeight))) } +func (buffer *Buffer) GetVPosition() int { + result := int(uint(buffer.Height()) - uint(buffer.ViewHeight()) - buffer.terminalState.scrollLinesFromBottom) + if result < 0 { + result = 0 + } + + return result +} + // Width returns the width of the buffer in columns func (buffer *Buffer) Width() uint16 { return buffer.terminalState.viewWidth @@ -550,12 +550,11 @@ func (buffer *Buffer) deleteLine() { } func (buffer *Buffer) insertLine() { - - defer buffer.emitDisplayChange() + defer buffer.dirty.Notify() if !buffer.InScrollableRegion() { pos := buffer.RawLine() - maxLines := buffer.getMaxLines() + maxLines := buffer.GetMaxLines() newLineCount := uint64(len(buffer.lines) + 1) if newLineCount > maxLines { newLineCount = maxLines @@ -636,7 +635,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() { @@ -651,7 +650,7 @@ func (buffer *Buffer) Index() { if buffer.terminalState.cursorY >= buffer.ViewHeight()-1 { buffer.lines = append(buffer.lines, newLine()) - maxLines := buffer.getMaxLines() + maxLines := buffer.GetMaxLines() if uint64(len(buffer.lines)) > maxLines { copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:]) buffer.lines = buffer.lines[:maxLines] @@ -662,8 +661,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 +672,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 @@ -705,6 +704,7 @@ func (buffer *Buffer) Write(runes ...rune) { buffer.NewLineEx(true) newLine := buffer.getCurrentLine() + newLine.setNoBreak(true) if len(newLine.cells) == 0 { newLine.Append(buffer.terminalState.DefaultCell(true)) } @@ -837,7 +837,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 +876,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()) } @@ -884,7 +884,7 @@ func (buffer *Buffer) Clear() { } func (buffer *Buffer) ReallyClear() { - defer buffer.emitDisplayChange() + defer buffer.dirty.Notify() buffer.lines = []Line{} buffer.terminalState.SetScrollOffset(0) buffer.SetPosition(0, 0) @@ -916,13 +916,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 +932,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 +950,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 +960,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 +975,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 +990,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 +1006,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 +1024,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 @@ -1126,7 +1125,7 @@ func (buffer *Buffer) ResizeView(width uint16, height uint16) { buffer.terminalState.ResetVerticalMargins() } -func (buffer *Buffer) getMaxLines() uint64 { +func (buffer *Buffer) GetMaxLines() uint64 { result := buffer.terminalState.maxLines if result < uint64(buffer.terminalState.viewHeight) { result = uint64(buffer.terminalState.viewHeight) @@ -1162,7 +1161,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() diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index 43064bf..a62abc0 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -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) +} diff --git a/buffer/line.go b/buffer/line.go index 270b26f..3919d44 100644 --- a/buffer/line.go +++ b/buffer/line.go @@ -6,12 +6,14 @@ import ( type Line struct { wrapped bool // whether line was wrapped onto from the previous one + nobreak bool // true if no line break at the beginning of the line cells []Cell } func newLine() Line { return Line{ wrapped: false, + nobreak: false, cells: []Cell{}, } } @@ -45,6 +47,10 @@ func (line *Line) setWrapped(wrapped bool) { line.wrapped = wrapped } +func (line *Line) setNoBreak(nobreak bool) { + line.nobreak = nobreak +} + func (line *Line) String() string { runes := []rune{} for _, cell := range line.cells { diff --git a/config.go b/config.go index b0c54c0..3db76bf 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,13 @@ func getActuallyProvidedFlags() map[string]bool { return result } +func maybeGetConfig(override *config.Config) *config.Config { + if override != nil { + return override + } + return getConfig() +} + func getConfig() *config.Config { showVersion := false ignoreConfig := false @@ -53,7 +60,7 @@ func getConfig() *config.Config { var conf *config.Config if ignoreConfig { - conf = &config.DefaultConfig + conf = config.DefaultConfig() } else { conf = loadConfigFile() } @@ -83,12 +90,12 @@ func loadConfigFile() *config.Config { usr, err := user.Current() if err != nil { fmt.Printf("Failed to get current user information: %s\n", err) - return &config.DefaultConfig + return config.DefaultConfig() } home := usr.HomeDir if home == "" { - return &config.DefaultConfig + return config.DefaultConfig() } places := []string{} @@ -111,7 +118,7 @@ func loadConfigFile() *config.Config { } } - if b, err := config.DefaultConfig.Encode(); err != nil { + if b, err := config.DefaultConfig().Encode(); err != nil { fmt.Printf("Failed to encode config file: %s\n", err) } else { err = os.MkdirAll(filepath.Dir(places[0]), 0744) @@ -124,5 +131,5 @@ func loadConfigFile() *config.Config { } } - return &config.DefaultConfig + return config.DefaultConfig() } diff --git a/config/config.go b/config/config.go index 92e187f..dff0785 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ type Config struct { SearchURL string `toml:"search_url"` MaxLines uint64 `toml:"max_lines"` CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"` + ShowVerticalScrollbar bool `toml:"show_vertical_scrollbar"` // Developer options. DebugMode bool `toml:"debug"` @@ -24,12 +25,12 @@ type Config struct { type KeyMappingConfig map[string]string func Parse(data []byte) (*Config, error) { - c := DefaultConfig - err := toml.Unmarshal(data, &c) + c := DefaultConfig() + err := toml.Unmarshal(data, c) if c.KeyMapping == nil { c.KeyMapping = KeyMappingConfig(map[string]string{}) } - return &c, err + return c, err } func (c *Config) Encode() ([]byte, error) { diff --git a/config/defaults.go b/config/defaults.go index bc0557c..6a62c78 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -2,52 +2,51 @@ package config import "runtime" -var DefaultConfig = Config{ - DebugMode: false, - ColourScheme: ColourScheme{ - Cursor: strToColourNoErr("#e8dfd6"), - Foreground: strToColourNoErr("#e8dfd6"), - Background: strToColourNoErr("#021b21"), - Black: strToColourNoErr("#000000"), - Red: strToColourNoErr("#800000"), - Green: strToColourNoErr("#008000"), - Yellow: strToColourNoErr("#808000"), - Blue: strToColourNoErr("#000080"), - Magenta: strToColourNoErr("#800080"), - Cyan: strToColourNoErr("#008080"), - LightGrey: strToColourNoErr("#f2f2f2"), - DarkGrey: strToColourNoErr("#808080"), - LightRed: strToColourNoErr("#ff0000"), - LightGreen: strToColourNoErr("#00ff00"), - LightYellow: strToColourNoErr("#ffff00"), - LightBlue: strToColourNoErr("#0000ff"), - LightMagenta: strToColourNoErr("#ff00ff"), - LightCyan: strToColourNoErr("#00ffff"), - White: strToColourNoErr("#ffffff"), - Selection: strToColourNoErr("#333366"), - }, - KeyMapping: KeyMappingConfig(map[string]string{}), - SearchURL: "https://www.google.com/search?q=$QUERY", - MaxLines: 1000, - CopyAndPasteWithMouse: true, -} - -func init() { - DefaultConfig.KeyMapping[string(ActionCopy)] = addMod("c") - DefaultConfig.KeyMapping[string(ActionPaste)] = addMod("v") - DefaultConfig.KeyMapping[string(ActionSearch)] = addMod("g") - DefaultConfig.KeyMapping[string(ActionToggleDebug)] = addMod("d") - DefaultConfig.KeyMapping[string(ActionToggleSlomo)] = addMod(";") - DefaultConfig.KeyMapping[string(ActionReportBug)] = addMod("r") - DefaultConfig.KeyMapping[string(ActionBufferClear)] = addMod("k") +func DefaultConfig() *Config { + return &Config{ + DebugMode: false, + ColourScheme: ColourScheme{ + Cursor: strToColourNoErr("#e8dfd6"), + Foreground: strToColourNoErr("#e8dfd6"), + Background: strToColourNoErr("#021b21"), + Black: strToColourNoErr("#000000"), + Red: strToColourNoErr("#800000"), + Green: strToColourNoErr("#008000"), + Yellow: strToColourNoErr("#808000"), + Blue: strToColourNoErr("#000080"), + Magenta: strToColourNoErr("#800080"), + Cyan: strToColourNoErr("#008080"), + LightGrey: strToColourNoErr("#f2f2f2"), + DarkGrey: strToColourNoErr("#808080"), + LightRed: strToColourNoErr("#ff0000"), + LightGreen: strToColourNoErr("#00ff00"), + LightYellow: strToColourNoErr("#ffff00"), + LightBlue: strToColourNoErr("#0000ff"), + LightMagenta: strToColourNoErr("#ff00ff"), + LightCyan: strToColourNoErr("#00ffff"), + White: strToColourNoErr("#ffffff"), + Selection: strToColourNoErr("#333366"), + }, + KeyMapping: KeyMappingConfig(map[string]string{ + string(ActionCopy): addMod("c"), + string(ActionPaste): addMod("v"), + string(ActionSearch): addMod("g"), + string(ActionToggleDebug): addMod("d"), + string(ActionToggleSlomo): addMod(";"), + string(ActionReportBug): addMod("r"), + string(ActionBufferClear): addMod("k"), + }), + SearchURL: "https://www.google.com/search?q=$QUERY", + MaxLines: 1000, + CopyAndPasteWithMouse: true, + ShowVerticalScrollbar: true, + } } func addMod(keys string) string { standardMod := "ctrl + shift + " - if runtime.GOOS == "darwin" { standardMod = "super + " } - return standardMod + keys } diff --git a/glfont/truetype.go b/glfont/truetype.go index c7856a2..57af926 100644 --- a/glfont/truetype.go +++ b/glfont/truetype.go @@ -48,7 +48,7 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error) gl.BindVertexArray(f.vao) gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) - gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.STATIC_DRAW) + gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.DYNAMIC_DRAW) vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00"))) gl.EnableVertexAttribArray(vertAttrib) diff --git a/gui/actions.go b/gui/actions.go index ba611c1..ed355d9 100644 --- a/gui/actions.go +++ b/gui/actions.go @@ -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) { diff --git a/gui/fonts.go b/gui/fonts.go index 3e0aaf6..911be0e 100644 --- a/gui/fonts.go +++ b/gui/fonts.go @@ -8,14 +8,14 @@ import ( "github.com/liamg/aminal/glfont" ) -func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) { +func (gui *GUI) getPackedFont(name string, actualWidth int, actualHeight int) (*glfont.Font, error) { box := packr.NewBox("./packed-fonts") fontBytes, err := box.Find(name) if err != nil { return nil, fmt.Errorf("packaged font '%s' could not be read: %s", name, err) } - font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), gui.width, gui.height) + font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), actualWidth, actualHeight) if err != nil { return nil, fmt.Errorf("font '%s' failed to load: %v", name, err) } @@ -23,16 +23,16 @@ func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) { return font, nil } -func (gui *GUI) loadFonts() error { +func (gui *GUI) loadFonts(actualWidth int, actualHeight int) error { // from https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack - defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf") + defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf", actualWidth, actualHeight) if err != nil { return err } - boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf") + boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf", actualWidth, actualHeight) if err != nil { return err } diff --git a/gui/gui.go b/gui/gui.go index 28cb66a..b35a8b8 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -24,6 +24,24 @@ 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 + +const ( + DefaultWindowWidth = 800 + DefaultWindowHeight = 600 +) + +type mouseEventsHandler interface { + mouseMoveCallback(g *GUI, px float64, py float64) + mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) + cursorEnterCallback(g *GUI, enter bool) + isMouseInside(px float64, py float64) bool +} + type GUI struct { window *glfw.Window logger *zap.SugaredLogger @@ -57,8 +75,17 @@ type GUI struct { leftClickTime time.Time leftClickCount int // number of clicks in a serie - single click, double click, or triple click mouseMovedAfterSelectionStarted bool - internalResize bool - selectionRegionMode buffer.SelectionRegionMode + + catchedMouseHandler mouseEventsHandler + mouseCatchedOnButton glfw.MouseButton + prevMouseEventHandler mouseEventsHandler + + internalResize bool + selectionRegionMode buffer.SelectionRegionMode + + vScrollbar *scrollbar + + mainThreadFunc chan func() } func Min(x, y int) int { @@ -148,22 +175,33 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared } return &GUI{ - config: config, - logger: logger, - width: 800, - height: 600, - appliedWidth: 0, - appliedHeight: 0, - dpiScale: 1, - terminal: terminal, - fontScale: 10.0, - terminalAlpha: 1, - keyboardShortcuts: shortcuts, - resizeLock: &sync.Mutex{}, - internalResize: false, + config: config, + logger: logger, + width: DefaultWindowWidth, + height: DefaultWindowHeight, + appliedWidth: 0, + appliedHeight: 0, + dpiScale: 1, + terminal: terminal, + fontScale: 10.0, + terminalAlpha: 1, + keyboardShortcuts: shortcuts, + resizeLock: &sync.Mutex{}, + internalResize: false, + vScrollbar: nil, + catchedMouseHandler: nil, + + mainThreadFunc: make(chan func()), }, nil } +func (gui *GUI) Free() { + if gui.vScrollbar != nil { + gui.vScrollbar.Free() + gui.vScrollbar = nil + } +} + // inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl func (gui *GUI) scale() float32 { @@ -198,15 +236,20 @@ func (gui *GUI) resizeToTerminal() { gui.logger.Debugf("Initiating GUI resize to columns=%d rows=%d", newCols, newRows) gui.logger.Debugf("Calculating size...") - width, height := gui.renderer.GetRectangleSize(newCols, newRows) + width, height := gui.renderer.ConvertCoordinates(newCols, newRows) roundedWidth := int(math.Ceil(float64(width))) roundedHeight := int(math.Ceil(float64(height))) + if gui.vScrollbar != nil { + roundedWidth += int(gui.vScrollbar.position.width()) + } + gui.resizeCache = &ResizeCache{roundedWidth, roundedHeight, newCols, newRows} gui.logger.Debugf("Resizing window to %dx%d", roundedWidth, roundedHeight) gui.internalResize = true + gui.window.SetSize(roundedWidth, roundedHeight) // will trigger resize() gui.internalResize = false } @@ -268,14 +311,20 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) { gui.width = width gui.height = height - gui.appliedWidth = width - gui.appliedHeight = height + gui.appliedWidth = gui.width + gui.appliedHeight = gui.height + + vScrollbarWidth := 0 + if gui.vScrollbar != nil { + gui.vScrollbar.resize(gui) + vScrollbarWidth = int(gui.vScrollbar.position.width()) + } gui.logger.Debugf("Updating font resolutions...") - gui.loadFonts() + gui.loadFonts(gui.width, gui.height) gui.logger.Debugf("Setting renderer area...") - gui.renderer.SetArea(0, 0, gui.width, gui.height) + gui.renderer.SetArea(0, 0, gui.width-vScrollbarWidth, gui.height) if gui.resizeCache != nil && gui.resizeCache.Width == width && gui.resizeCache.Height == height { gui.logger.Debugf("No need to resize internal terminal!") @@ -320,21 +369,21 @@ func (gui *GUI) getTermSize() (uint, uint) { func (gui *GUI) Close() { gui.window.SetShouldClose(true) + glfw.PostEmptyEvent() // wake up main loop so it notices close request } func (gui *GUI) Render() error { - gui.logger.Debugf("Creating window...") var err error gui.window, err = gui.createWindow() - gui.SetDPIScale() - gui.window.SetSize(int(float32(gui.width)*gui.dpiScale), - int(float32(gui.height)*gui.dpiScale)) if err != nil { return fmt.Errorf("Failed to create window: %s", err) } defer glfw.Terminate() + gui.SetDPIScale() + gui.window.SetSize(int(float32(gui.width)*gui.dpiScale), int(float32(gui.height)*gui.dpiScale)) + gui.logger.Debugf("Initialising OpenGL and creating program...") program, err := gui.createProgram() if err != nil { @@ -344,8 +393,19 @@ func (gui *GUI) Render() error { gui.colourAttr = uint32(gl.GetAttribLocation(program, gl.Str("inColour\x00"))) gl.BindFragDataLocation(program, 0, gl.Str("outColour\x00")) + vScrollbarWidth := 0 + if gui.config.ShowVerticalScrollbar { + vScrollbar, err := newScrollbar() + if err != nil { + return err + } + gui.vScrollbar = vScrollbar + gui.vScrollbar.resize(gui) + vScrollbarWidth = int(gui.vScrollbar.position.width()) + } + gui.logger.Debugf("Loading font...") - if err := gui.loadFonts(); err != nil { + if err := gui.loadFonts(gui.width, gui.height); err != nil { return fmt.Errorf("Failed to load font: %s", err) } @@ -353,20 +413,24 @@ func (gui *GUI) Render() error { resizeChan := make(chan bool, 1) reverseChan := make(chan bool, 1) - gui.renderer = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width, gui.height, gui.colourAttr, program) + gui.renderer, err = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width-vScrollbarWidth, gui.height, gui.colourAttr, program) + if err != nil { + return err + } gui.window.SetFramebufferSizeCallback(gui.resize) gui.window.SetKeyCallback(gui.key) gui.window.SetCharCallback(gui.char) gui.window.SetScrollCallback(gui.glfwScrollCallback) - gui.window.SetMouseButtonCallback(gui.mouseButtonCallback) - gui.window.SetCursorPosCallback(gui.mouseMoveCallback) + gui.window.SetMouseButtonCallback(gui.globalMouseButtonCallback) + gui.window.SetCursorPosCallback(gui.globalMouseMoveCallback) + gui.window.SetCursorEnterCallback(gui.globalCursorEnterCallback) 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) @@ -393,6 +457,10 @@ func (gui *GUI) Render() error { gui.Close() }() + if gui.vScrollbar != nil { + gui.vScrollbar.resize(gui) + } + gui.logger.Debugf("Starting render...") gl.UseProgram(program) @@ -401,16 +469,6 @@ func (gui *GUI) Render() error { gl.Disable(gl.DEPTH_TEST) gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - go func() { - for { - <-ticker.C - gui.logger.Sync() - } - }() - gui.terminal.SetProgram(program) latestVersion := "" @@ -419,82 +477,130 @@ 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{}) + var waitForWaker sync.WaitGroup + waitForWaker.Add(1) + go gui.waker(stop, &waitForWaker) + for !gui.window.ShouldClose() { + gui.redraw(true) - 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) + case funcForMainThread := <-gui.mainThreadFunc: + funcForMainThread() + default: + break terminalEvents + } + } } - gui.logger.Debugf("Stopping render...") + gui.logger.Debug("Stopping render...") + + close(stop) // Tell waker to end... + waitForWaker.Wait() // ...and wait it to end + + gui.logger.Debug("Render stopped") + 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{}, wg *sync.WaitGroup) { + defer wg.Done() + + dirty := gui.terminal.Dirty() + var nextWake <-chan time.Time + var last time.Time +forLoop: + 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: + break forLoop + } + } +} + func (gui *GUI) renderTerminalData(shouldLock bool) { if shouldLock { gui.terminal.Lock() @@ -627,8 +733,9 @@ func (gui *GUI) renderTerminalData(shouldLock bool) { gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour) } } - } + + gui.renderScrollbar() } func (gui *GUI) redraw(shouldLock bool) { @@ -760,18 +867,27 @@ func (gui *GUI) SwapBuffers() { gui.window.SwapBuffers() } -func (gui *GUI) Screenshot(path string) { +func (gui *GUI) Screenshot(path string) error { x, y := gui.window.GetPos() w, h := gui.window.GetSize() img, err := screenshot.CaptureRect(image.Rectangle{Min: image.Point{X: x, Y: y}, Max: image.Point{X: x + w, Y: y + h}}) if err != nil { - panic(err) + return err + } + file, err := os.Create(path) + if err != nil { + return err } - file, _ := os.Create(path) defer file.Close() - png.Encode(file, img) + err = png.Encode(file, img) + if err != nil { + os.Remove(path) + return err + } + + return nil } func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) { @@ -781,3 +897,24 @@ func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) { func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorEvent) { gui.SetDPIScale() } + +func (gui *GUI) renderScrollbar() { + if gui.vScrollbar != nil { + position := gui.terminal.ActiveBuffer().GetVPosition() + maxPosition := int(gui.terminal.ActiveBuffer().GetMaxLines()) - int(gui.terminal.ActiveBuffer().ViewHeight()) + + gui.vScrollbar.setPosition(maxPosition, position) + gui.vScrollbar.render(gui) + } +} + +// Synchronously executes the argument function in the main thread. +// Does not return until f() executed! +func (gui *GUI) executeInMainThread(f func() error) error { + resultChan := make(chan error, 1) + gui.mainThreadFunc <- func() { + resultChan <- f() + } + gui.terminal.NotifyDirty() // wake up the main thread to allow processing + return <-resultChan +} diff --git a/gui/input.go b/gui/input.go index c5caada..ec7b65e 100644 --- a/gui/input.go +++ b/gui/input.go @@ -52,7 +52,7 @@ func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) { } if gui.selectionRegionMode != mode { gui.selectionRegionMode = mode - gui.terminal.SetDirty() + gui.terminal.NotifyDirty() } } diff --git a/gui/mouse.go b/gui/mouse.go index 14c71c5..19703e9 100644 --- a/gui/mouse.go +++ b/gui/mouse.go @@ -36,7 +36,86 @@ func (gui *GUI) getArrowCursor() *glfw.Cursor { return gui.arrowCursor } -func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { +func (gui *GUI) scaleMouseCoordinates(px float64, py float64) (float64, float64) { + scale := float64(gui.scale()) + px = px / scale + py = py / scale + + return px, py +} + +func (gui *GUI) globalMouseMoveCallback(w *glfw.Window, px float64, py float64) { + px, py = gui.scaleMouseCoordinates(px, py) + + if gui.catchedMouseHandler != nil { + gui.catchedMouseHandler.mouseMoveCallback(gui, px, py) + } else { + if gui.isMouseInside(px, py) { + if gui.prevMouseEventHandler != gui { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + } + gui.cursorEnterCallback(gui, true) + } + gui.mouseMoveCallback(gui, px, py) + gui.prevMouseEventHandler = gui + } else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(px, py) { + if gui.prevMouseEventHandler != gui.vScrollbar { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + } + gui.vScrollbar.cursorEnterCallback(gui, true) + } + gui.vScrollbar.mouseMoveCallback(gui, px, py) + gui.prevMouseEventHandler = gui.vScrollbar + } + } +} + +func (gui *GUI) globalMouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { + mouseX, mouseY := gui.scaleMouseCoordinates(w.GetCursorPos()) + + if gui.catchedMouseHandler != nil { + gui.catchedMouseHandler.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + if action == glfw.Release && button == gui.mouseCatchedOnButton { + gui.catchMouse(nil, 0) + } + } else { + + if gui.isMouseInside(mouseX, mouseY) { + if action == glfw.Press { + gui.catchMouse(gui, button) + } + gui.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + } else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(mouseX, mouseY) { + if action == glfw.Press { + gui.catchMouse(gui.vScrollbar, button) + } + gui.vScrollbar.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY) + } + } +} + +func (gui *GUI) globalCursorEnterCallback(w *glfw.Window, entered bool) { + if !entered { + if gui.prevMouseEventHandler != nil { + gui.prevMouseEventHandler.cursorEnterCallback(gui, false) + gui.prevMouseEventHandler = nil + } + } +} + +func (gui *GUI) catchMouse(newHandler mouseEventsHandler, button glfw.MouseButton) { + gui.catchedMouseHandler = newHandler + gui.mouseCatchedOnButton = button +} + +func (gui *GUI) isMouseInside(px float64, py float64) bool { + return px >= float64(gui.renderer.areaX) && px < float64(gui.renderer.areaX+gui.renderer.areaWidth) && + py >= float64(gui.renderer.areaY) && py < float64(gui.renderer.areaY+gui.renderer.areaHeight) +} + +func (gui *GUI) mouseMoveCallback(g *GUI, px float64, py float64) { x, y := gui.convertMouseCoordinates(px, py) @@ -59,16 +138,13 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) { } if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" { - w.SetCursor(gui.getHandCursor()) + gui.window.SetCursor(gui.getHandCursor()) } else { - w.SetCursor(gui.getArrowCursor()) + gui.window.SetCursor(gui.getArrowCursor()) } } func (gui *GUI) convertMouseCoordinates(px float64, py float64) (uint16, uint16) { - scale := gui.scale() - px = px / float64(scale) - py = py / float64(scale) x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth()))) y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight()))) @@ -122,7 +198,7 @@ func btnCode(button glfw.MouseButton, release bool, mod glfw.ModifierKey) (b byt return b, true } -func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) { +func (gui *GUI) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) { if gui.overlay != nil { if button == glfw.MouseButtonRight && action == glfw.Release { @@ -132,7 +208,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act } // before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc. - x, y := gui.convertMouseCoordinates(w.GetCursorPos()) + x, y := gui.convertMouseCoordinates(mouseX, mouseY) tx := int(x) + 1 // vt100 is 1 indexed ty := int(y) + 1 @@ -252,7 +328,10 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act default: panic("Unsupported mouse mode") } +} +func (gui *GUI) cursorEnterCallback(g *GUI, entered bool) { + // empty, just to conform to the mouseEventsHandler interface } func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16, mod glfw.ModifierKey) { diff --git a/gui/overlays.go b/gui/overlays.go index b792fa3..d98653e 100644 --- a/gui/overlays.go +++ b/gui/overlays.go @@ -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() { diff --git a/gui/rectangleRenderer.go b/gui/rectangleRenderer.go new file mode 100644 index 0000000..484fe51 --- /dev/null +++ b/gui/rectangleRenderer.go @@ -0,0 +1,157 @@ +package gui + +import ( + "github.com/go-gl/gl/all-core/gl" + "github.com/liamg/aminal/config" +) + +const ( + rectangleRendererVertexShaderSource = ` + #version 330 core + layout (location = 0) in vec2 position; + uniform vec2 resolution; + + void main() { + // convert from window coordinates to GL coordinates + vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1); + + gl_Position = vec4(glCoordinates, 0.0, 1.0); + }` + "\x00" + + rectangleRendererFragmentShaderSource = ` + #version 330 core + uniform vec4 inColor; + out vec4 outColor; + void main() { + outColor = inColor; + }` + "\x00" +) + +type rectangleRenderer struct { + program uint32 + vbo uint32 + vao uint32 + ibo uint32 + uniformLocationResolution int32 + uniformLocationInColor int32 +} + +func createRectangleRendererProgram() (uint32, error) { + vertexShader, err := compileShader(rectangleRendererVertexShaderSource, gl.VERTEX_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(vertexShader) + + fragmentShader, err := compileShader(rectangleRendererFragmentShaderSource, gl.FRAGMENT_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(fragmentShader) + + prog := gl.CreateProgram() + gl.AttachShader(prog, vertexShader) + gl.AttachShader(prog, fragmentShader) + gl.LinkProgram(prog) + + return prog, nil +} + +func newRectangleRenderer() (*rectangleRenderer, error) { + prog, err := createRectangleRendererProgram() + if err != nil { + return nil, err + } + + var vbo uint32 + var vao uint32 + var ibo uint32 + + gl.GenBuffers(1, &vbo) + gl.GenVertexArrays(1, &vao) + gl.GenBuffers(1, &ibo) + + indices := [...]uint32{ + 0, 1, 2, + 2, 3, 0, + } + + gl.BindVertexArray(vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.BufferData(gl.ARRAY_BUFFER, 8*4, nil, gl.DYNAMIC_DRAW) // just reserve data for the buffer + + gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo) + gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(indices)*4, gl.Ptr(&indices[0]), gl.DYNAMIC_DRAW) + + gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil) + gl.EnableVertexAttribArray(0) + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + + return &rectangleRenderer{ + program: prog, + vbo: vbo, + vao: vao, + ibo: ibo, + uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")), + uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")), + }, nil +} + +func (rr *rectangleRenderer) Free() { + if rr.program != 0 { + gl.DeleteProgram(rr.program) + rr.program = 0 + } + + if rr.vbo != 0 { + gl.DeleteBuffers(1, &rr.vbo) + rr.vbo = 0 + } + + if rr.vao != 0 { + gl.DeleteBuffers(1, &rr.vao) + rr.vao = 0 + } + + if rr.ibo != 0 { + gl.DeleteBuffers(1, &rr.ibo) + rr.ibo = 0 + } +} + +func (rr *rectangleRenderer) render(left float32, top float32, width float32, height float32, colour config.Colour) { + var savedProgram int32 + gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram) + defer gl.UseProgram(uint32(savedProgram)) + + currentViewport := [4]int32{} + gl.GetIntegerv(gl.VIEWPORT, ¤tViewport[0]) + + gl.UseProgram(rr.program) + gl.Uniform2f(rr.uniformLocationResolution, float32(currentViewport[2]), float32(currentViewport[3])) + + gl.Uniform4f(rr.uniformLocationInColor, colour[0], colour[1], colour[2], 1.0) + + vertices := [...]float32{ + left, top, + left + width, top, + left + width, top + height, + left, top + height, + } + + /* + gl.NamedBufferSubData(rr.vbo, 0, len(vertices)*4, gl.Ptr(&vertices[0])) + /*/ + gl.BindBuffer(gl.ARRAY_BUFFER, rr.vbo) + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(&vertices[0])) + //*/ + + gl.BindVertexArray(rr.vao) + + gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0)) + + gl.BindVertexArray(0) +} diff --git a/gui/renderer.go b/gui/renderer.go index b5aee10..9b9e6ab 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -1,13 +1,12 @@ package gui import ( - "image" - "math" - "github.com/go-gl/gl/all-core/gl" "github.com/liamg/aminal/buffer" "github.com/liamg/aminal/config" "github.com/liamg/aminal/glfont" + "image" + "math" ) type OpenGLRenderer struct { @@ -26,16 +25,8 @@ type OpenGLRenderer struct { textureMap map[*image.RGBA]uint32 fontMap *FontMap backgroundColour [3]float32 -} -type rectangle struct { - vao uint32 - vbo uint32 - cv uint32 - colourAttr uint32 - colour [3]float32 - points [18]float32 - prog uint32 + rectRenderer *rectangleRenderer } func (r *OpenGLRenderer) CellWidth() float32 { @@ -46,95 +37,12 @@ func (r *OpenGLRenderer) CellHeight() float32 { return r.cellHeight } -func (r *OpenGLRenderer) newRectangleEx(x float32, y float32, width float32, height float32, colourAttr uint32) *rectangle { - - rect := &rectangle{} - - halfAreaWidth := float32(r.areaWidth / 2) - halfAreaHeight := float32(r.areaHeight / 2) - - x = (x - halfAreaWidth) / halfAreaWidth - y = -(y - (halfAreaHeight)) / halfAreaHeight - w := width / halfAreaWidth - h := height / halfAreaHeight - - rect.points = [18]float32{ - x, y, 0, - x, y + h, 0, - x + w, y + h, 0, - - x + w, y, 0, - x, y, 0, - x + w, y + h, 0, +func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) (*OpenGLRenderer, error) { + rectRenderer, err := newRectangleRenderer() + if err != nil { + return nil, err } - rect.colourAttr = colourAttr - rect.prog = r.program - - // SHAPE - gl.GenBuffers(1, &rect.vbo) - gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo) - gl.BufferData(gl.ARRAY_BUFFER, 4*len(rect.points), gl.Ptr(&rect.points[0]), gl.STATIC_DRAW) - - gl.GenVertexArrays(1, &rect.vao) - gl.BindVertexArray(rect.vao) - gl.EnableVertexAttribArray(0) - - gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo) - gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil) - - // colour - gl.GenBuffers(1, &rect.cv) - - rect.setColour([3]float32{0, 1, 0}) - - return rect -} - -func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *rectangle { - return r.newRectangleEx(x, y, r.cellWidth, r.cellHeight, colourAttr) -} - -func (rect *rectangle) Draw() { - gl.UseProgram(rect.prog) - gl.BindVertexArray(rect.vao) - gl.DrawArrays(gl.TRIANGLES, 0, 6) -} - -func (rect *rectangle) setColour(colour [3]float32) { - if rect.colour == colour { - return - } - - c := []float32{ - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - colour[0], colour[1], colour[2], - } - - gl.UseProgram(rect.prog) - gl.BindBuffer(gl.ARRAY_BUFFER, rect.cv) - gl.BufferData(gl.ARRAY_BUFFER, len(c)*4, gl.Ptr(c), gl.STATIC_DRAW) - gl.EnableVertexAttribArray(rect.colourAttr) - gl.VertexAttribPointer(rect.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0)) - - rect.colour = colour -} - -func (rect *rectangle) Free() { - gl.DeleteVertexArrays(1, &rect.vao) - gl.DeleteBuffers(1, &rect.vbo) - gl.DeleteBuffers(1, &rect.cv) - - rect.vao = 0 - rect.vbo = 0 - rect.cv = 0 -} - -func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) *OpenGLRenderer { r := &OpenGLRenderer{ areaWidth: areaWidth, areaHeight: areaHeight, @@ -146,9 +54,10 @@ func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY program: program, textureMap: map[*image.RGBA]uint32{}, fontMap: fontMap, + rectRenderer: rectRenderer, } r.SetArea(areaX, areaY, areaWidth, areaHeight) - return r + return r, nil } // This method ensures that all OpenGL resources are deleted correctly @@ -162,6 +71,11 @@ func (r *OpenGLRenderer) Free() { gl.DeleteProgram(r.program) r.program = 0 + + if r.rectRenderer != nil { + r.rectRenderer.Free() + r.rectRenderer = nil + } } func (r *OpenGLRenderer) GetTermSize() (uint, uint) { @@ -181,26 +95,16 @@ func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight))) } -func (r *OpenGLRenderer) GetRectangleSize(col uint, row uint) (float32, float32) { - x := float32(float32(col) * r.cellWidth) - y := float32(float32(row) * r.cellHeight) +func (r *OpenGLRenderer) ConvertCoordinates(col uint, row uint) (float32, float32) { + left := float32(float32(col) * r.cellWidth) + top := float32(float32(row) * r.cellHeight) - return x, y -} - -func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle { - x := float32(float32(col) * r.cellWidth) - y := float32(float32(row)*r.cellHeight) + r.cellHeight - - return r.newRectangle(x, y, r.colourAttr) + return left, top } func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) { - rect := r.getRectangle(col, row) - rect.setColour(colour) - rect.Draw() - - rect.Free() + left, top := r.ConvertCoordinates(col, row) + r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, colour) } func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour *config.Colour, force bool) { @@ -214,13 +118,9 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour } if bg != r.backgroundColour || force { - rect := r.getRectangle(col, row) - rect.setColour(bg) - rect.Draw() - - rect.Free() + left, top := r.ConvertCoordinates(col, row) + r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, bg) } - } // DrawUnderline draws a line under 'span' characters starting at (col, row) @@ -233,12 +133,8 @@ func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]f if thickness < 1 { thickness = 1 } - rect := r.newRectangleEx(x, y, r.cellWidth*float32(span), thickness, r.colourAttr) - rect.setColour(colour) - rect.Draw() - - rect.Free() + r.rectRenderer.render(x, y, r.cellWidth*float32(span), thickness, colour) } func (r *OpenGLRenderer) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) { diff --git a/gui/scrollbar.go b/gui/scrollbar.go new file mode 100644 index 0000000..a8b5c54 --- /dev/null +++ b/gui/scrollbar.go @@ -0,0 +1,519 @@ +package gui + +import ( + "github.com/go-gl/gl/all-core/gl" + "github.com/go-gl/glfw/v3.2/glfw" +) + +const ( + scrollbarVertexShaderSource = ` + #version 330 core + layout (location = 0) in vec2 position; + uniform vec2 resolution; + + void main() { + // convert from window coordinates to GL coordinates + vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1); + + gl_Position = vec4(glCoordinates, 0.0, 1.0); + }` + "\x00" + + scrollbarFragmentShaderSource = ` + #version 330 core + uniform vec4 inColor; + out vec4 outColor; + void main() { + outColor = inColor; + }` + "\x00" + + NumberOfVertexValues = 100 +) + +var ( + scrollbarColor_Bg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ThumbNormal = [3]float32{float32(193) / float32(255), float32(193) / float32(255), float32(193) / float32(255)} + scrollbarColor_ThumbHover = [3]float32{float32(168) / float32(255), float32(168) / float32(255), float32(168) / float32(255)} + scrollbarColor_ThumbClicked = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)} + + scrollbarColor_ButtonNormalBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ButtonNormalFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)} + + scrollbarColor_ButtonHoverBg = [3]float32{float32(210) / float32(255), float32(210) / float32(255), float32(210) / float32(255)} + scrollbarColor_ButtonHoverFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)} + + scrollbarColor_ButtonDisabledBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)} + scrollbarColor_ButtonDisabledFg = [3]float32{float32(163) / float32(255), float32(163) / float32(255), float32(163) / float32(255)} + + scrollbarColor_ButtonClickedBg = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)} + scrollbarColor_ButtonClickedFg = [3]float32{float32(255) / float32(255), float32(255) / float32(255), float32(255) / float32(255)} +) + +type scrollbarPart int + +const ( + None scrollbarPart = iota + UpperArrow + UpperSpace // the space between upper arrow and thumb + Thumb + BottomSpace // the space between thumb and bottom arrow + BottomArrow +) + +type ScreenRectangle struct { + left, top float32 // upper left corner in pixels relative to the window (in pixels) + right, bottom float32 +} + +func (sr *ScreenRectangle) width() float32 { + return sr.right - sr.left +} + +func (sr *ScreenRectangle) height() float32 { + return sr.bottom - sr.top +} + +func (sr *ScreenRectangle) isInside(x float32, y float32) bool { + return x >= sr.left && x < sr.right && + y >= sr.top && y < sr.bottom +} + +type scrollbar struct { + program uint32 + vbo uint32 + vao uint32 + uniformLocationResolution int32 + uniformLocationInColor int32 + + position ScreenRectangle // relative to the window's top left corner, in pixels + positionUpperArrow ScreenRectangle // relative to the control's top left corner + positionBottomArrow ScreenRectangle + positionThumb ScreenRectangle + + scrollPosition int + maxScrollPosition int + + thumbIsDragging bool + startedDraggingAtPosition int // scrollPosition when the dragging was started + startedDraggingAtThumbTop float32 // sb.positionThumb.top when the dragging was started + offsetInThumbY float32 // y offset inside the thumb of the dragging point + scrollPositionDelta int + + upperArrowIsDown bool + bottomArrowIsDown bool + + upperArrowFg []float32 + upperArrowBg []float32 + bottomArrowFg []float32 + bottomArrowBg []float32 + thumbColor []float32 +} + +// Returns the vertical scrollbar width in pixels +func getDefaultScrollbarWidth() int { + return 13 +} + +func createScrollbarProgram() (uint32, error) { + vertexShader, err := compileShader(scrollbarVertexShaderSource, gl.VERTEX_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(vertexShader) + + fragmentShader, err := compileShader(scrollbarFragmentShaderSource, gl.FRAGMENT_SHADER) + if err != nil { + return 0, err + } + defer gl.DeleteShader(fragmentShader) + + prog := gl.CreateProgram() + gl.AttachShader(prog, vertexShader) + gl.AttachShader(prog, fragmentShader) + gl.LinkProgram(prog) + + return prog, nil +} + +func newScrollbar() (*scrollbar, error) { + prog, err := createScrollbarProgram() + if err != nil { + return nil, err + } + + var vbo uint32 + var vao uint32 + + gl.GenBuffers(1, &vbo) + gl.GenVertexArrays(1, &vao) + + gl.BindVertexArray(vao) + + gl.BindBuffer(gl.ARRAY_BUFFER, vbo) + gl.BufferData(gl.ARRAY_BUFFER, NumberOfVertexValues*4, nil, gl.DYNAMIC_DRAW) // only reserve the space + + gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil) + gl.EnableVertexAttribArray(0) + + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + + result := &scrollbar{ + program: prog, + vbo: vbo, + vao: vao, + uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")), + uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")), + + position: ScreenRectangle{ + right: 0, + bottom: 0, + left: 0, + top: 0, + }, + + scrollPosition: 0, + maxScrollPosition: 0, + + thumbIsDragging: false, + upperArrowIsDown: false, + bottomArrowIsDown: false, + } + + result.recalcElementPositions() + result.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse + + return result, nil +} + +func (sb *scrollbar) Free() { + if sb.program != 0 { + gl.DeleteProgram(sb.program) + sb.program = 0 + } + + if sb.vbo != 0 { + gl.DeleteBuffers(1, &sb.vbo) + sb.vbo = 0 + } + + if sb.vao != 0 { + gl.DeleteBuffers(1, &sb.vao) + sb.vao = 0 + } +} + +// Recalc positions of the scrollbar elements according to current +func (sb *scrollbar) recalcElementPositions() { + arrowHeight := sb.position.width() + + sb.positionUpperArrow = ScreenRectangle{ + left: 0, + top: 0, + right: sb.position.width(), + bottom: arrowHeight, + } + + sb.positionBottomArrow = ScreenRectangle{ + left: sb.positionUpperArrow.left, + top: sb.position.height() - arrowHeight, + right: sb.positionUpperArrow.right, + bottom: sb.position.height(), + } + thumbHeight := sb.position.width() + thumbTop := arrowHeight + if sb.maxScrollPosition != 0 { + thumbTop += (float32(sb.scrollPosition) * (sb.position.height() - thumbHeight - arrowHeight*2)) / float32(sb.maxScrollPosition) + } + + sb.positionThumb = ScreenRectangle{ + left: 2, + top: thumbTop, + right: sb.position.width() - 2, + bottom: thumbTop + thumbHeight, + } +} + +func (sb *scrollbar) resize(gui *GUI) { + sb.position.left = float32(gui.width) - float32(getDefaultScrollbarWidth())*gui.dpiScale + sb.position.top = float32(0.0) + sb.position.right = float32(gui.width) + sb.position.bottom = float32(gui.height - 1) + + sb.recalcElementPositions() + gui.terminal.NotifyDirty() +} + +func (sb *scrollbar) render(gui *GUI) { + var savedProgram int32 + gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram) + defer gl.UseProgram(uint32(savedProgram)) + + gl.UseProgram(sb.program) + gl.Uniform2f(sb.uniformLocationResolution, float32(gui.width), float32(gui.height)) + gl.BindVertexArray(sb.vao) + gl.BindBuffer(gl.ARRAY_BUFFER, sb.vbo) + defer func() { + gl.BindBuffer(gl.ARRAY_BUFFER, 0) + gl.BindVertexArray(0) + }() + + // Draw background + gl.Uniform4f(sb.uniformLocationInColor, scrollbarColor_Bg[0], scrollbarColor_Bg[1], scrollbarColor_Bg[2], 1.0) + borderVertices := [...]float32{ + sb.position.left, sb.position.top, + sb.position.right, sb.position.top, + sb.position.right, sb.position.bottom, + + sb.position.right, sb.position.bottom, + sb.position.left, sb.position.bottom, + sb.position.left, sb.position.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(borderVertices)*4, gl.Ptr(&borderVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(borderVertices)/2)) + + // Draw upper arrow + // Upper arrow background + gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowBg[0], sb.upperArrowBg[1], sb.upperArrowBg[2], 1.0) + upperArrowBgVertices := [...]float32{ + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top, + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.top, + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom, + + sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom, + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.bottom, + sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowBgVertices)*4, gl.Ptr(&upperArrowBgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowBgVertices)/2)) + + // Upper arrow foreground + gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowFg[0], sb.upperArrowFg[1], sb.upperArrowFg[2], 1.0) + upperArrowFgVertices := [...]float32{ + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/2.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/3.0, + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()*2.0/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0, + sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowFgVertices)*4, gl.Ptr(&upperArrowFgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowFgVertices)/2)) + + // Draw bottom arrow + // Bottom arrow background + gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowBg[0], sb.bottomArrowBg[1], sb.bottomArrowBg[2], 1.0) + bottomArrowBgVertices := [...]float32{ + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top, + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.top, + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom, + + sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom, + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.bottom, + sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowBgVertices)*4, gl.Ptr(&bottomArrowBgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowBgVertices)/2)) + + // Bottom arrow foreground + gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowFg[0], sb.bottomArrowFg[1], sb.bottomArrowFg[2], 1.0) + bottomArrowFgVertices := [...]float32{ + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0, + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()*2.0/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0, + sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/2.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()*2.0/3.0, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowFgVertices)*4, gl.Ptr(&bottomArrowFgVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowFgVertices)/2)) + + // Draw thumb + gl.Uniform4f(sb.uniformLocationInColor, sb.thumbColor[0], sb.thumbColor[1], sb.thumbColor[2], 1.0) + thumbVertices := [...]float32{ + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top, + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.top, + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom, + + sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom, + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.bottom, + sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top, + } + gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(thumbVertices)*4, gl.Ptr(&thumbVertices[0])) + gl.DrawArrays(gl.TRIANGLES, 0, int32(len(thumbVertices)/2)) + + gui.terminal.NotifyDirty() +} + +func (sb *scrollbar) setPosition(max int, position int) { + if max <= 0 { + max = position + } + + if position > max { + position = max + } + + sb.maxScrollPosition = max + sb.scrollPosition = position + + sb.recalcElementPositions() +} + +func (sb *scrollbar) mouseHitTest(px float64, py float64) scrollbarPart { + // convert to local coordinates + mouseX := float32(px - float64(sb.position.left)) + mouseY := float32(py - float64(sb.position.top)) + + result := None + + if sb.positionUpperArrow.isInside(mouseX, mouseY) { + result = UpperArrow + } else if sb.positionBottomArrow.isInside(mouseX, mouseY) { + result = BottomArrow + } else if sb.positionThumb.isInside(mouseX, mouseY) { + result = Thumb + } else { + // construct UpperSpace + pos := ScreenRectangle{ + left: sb.positionThumb.left, + top: sb.positionUpperArrow.bottom, + right: sb.positionThumb.right, + bottom: sb.positionThumb.top, + } + + if pos.isInside(mouseX, mouseY) { + result = UpperSpace + } + + // now update it to be BottomSpace + pos.top = sb.positionThumb.bottom + pos.bottom = sb.positionBottomArrow.top + if pos.isInside(mouseX, mouseY) { + result = BottomSpace + } + } + + return result +} + +func (sb *scrollbar) isMouseInside(px float64, py float64) bool { + return sb.position.isInside(float32(px), float32(py)) +} + +func (sb *scrollbar) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) { + if button == glfw.MouseButtonLeft { + if action == glfw.Press { + switch sb.mouseHitTest(mouseX, mouseY) { + case UpperArrow: + sb.upperArrowIsDown = true + g.terminal.ScreenScrollUp(1) + + case UpperSpace: + g.terminal.ScrollPageUp() + + case Thumb: + sb.thumbIsDragging = true + sb.startedDraggingAtPosition = sb.scrollPosition + sb.startedDraggingAtThumbTop = sb.positionThumb.top + sb.offsetInThumbY = float32(mouseY) - sb.position.top - sb.positionThumb.top + sb.scrollPositionDelta = 0 + + case BottomSpace: + g.terminal.ScrollPageDown() + + case BottomArrow: + sb.bottomArrowIsDown = true + g.terminal.ScreenScrollDown(1) + } + } else if action == glfw.Release { + if sb.thumbIsDragging { + sb.thumbIsDragging = false + } + + if sb.upperArrowIsDown { + sb.upperArrowIsDown = false + } + + if sb.bottomArrowIsDown { + sb.bottomArrowIsDown = false + } + } + + g.terminal.NotifyDirty() + } + + sb.resetElementColors(mouseX, mouseY) +} + +func (sb *scrollbar) mouseMoveCallback(g *GUI, px float64, py float64) { + sb.resetElementColors(px, py) + + if sb.thumbIsDragging { + py -= float64(sb.position.top) + + minThumbTop := sb.positionUpperArrow.bottom + maxThumbTop := sb.positionBottomArrow.top - sb.positionThumb.height() + + newThumbTop := float32(py) - sb.offsetInThumbY + + newPositionDelta := int((float32(sb.maxScrollPosition) * (newThumbTop - minThumbTop - sb.startedDraggingAtThumbTop)) / (maxThumbTop - minThumbTop)) + + if newPositionDelta > sb.scrollPositionDelta { + scrollLines := newPositionDelta - sb.scrollPositionDelta + g.logger.Debugf("old position: %d, new position delta: %d, scroll down %d lines", sb.scrollPosition, newPositionDelta, scrollLines) + g.terminal.ScreenScrollDown(uint16(scrollLines)) + sb.scrollPositionDelta = newPositionDelta + } else if newPositionDelta < sb.scrollPositionDelta { + scrollLines := sb.scrollPositionDelta - newPositionDelta + g.logger.Debugf("old position: %d, new position delta: %d, scroll up %d lines", sb.scrollPosition, newPositionDelta, scrollLines) + g.terminal.ScreenScrollUp(uint16(scrollLines)) + sb.scrollPositionDelta = newPositionDelta + } + + sb.recalcElementPositions() + g.logger.Debugf("new thumbTop: %f, fact thumbTop: %f, position: %d", newThumbTop, sb.positionThumb.top, sb.scrollPosition) + } + + g.terminal.NotifyDirty() +} + +func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) { + part := sb.mouseHitTest(mouseX, mouseY) + + if sb.scrollPosition == 0 { + sb.upperArrowBg = scrollbarColor_ButtonDisabledBg[:] + sb.upperArrowFg = scrollbarColor_ButtonDisabledFg[:] + } else if sb.upperArrowIsDown { + sb.upperArrowFg = scrollbarColor_ButtonClickedFg[:] + sb.upperArrowBg = scrollbarColor_ButtonClickedBg[:] + } else if part == UpperArrow { + sb.upperArrowFg = scrollbarColor_ButtonHoverFg[:] + sb.upperArrowBg = scrollbarColor_ButtonHoverBg[:] + } else { + sb.upperArrowFg = scrollbarColor_ButtonNormalFg[:] + sb.upperArrowBg = scrollbarColor_ButtonNormalBg[:] + } + + if sb.scrollPosition == sb.maxScrollPosition { + sb.bottomArrowBg = scrollbarColor_ButtonDisabledBg[:] + sb.bottomArrowFg = scrollbarColor_ButtonDisabledFg[:] + } else if sb.bottomArrowIsDown { + sb.bottomArrowFg = scrollbarColor_ButtonClickedFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonClickedBg[:] + } else if part == BottomArrow { + sb.bottomArrowFg = scrollbarColor_ButtonHoverFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonHoverBg[:] + } else { + sb.bottomArrowFg = scrollbarColor_ButtonNormalFg[:] + sb.bottomArrowBg = scrollbarColor_ButtonNormalBg[:] + } + + if sb.thumbIsDragging { + sb.thumbColor = scrollbarColor_ThumbClicked[:] + } else if part == Thumb { + sb.thumbColor = scrollbarColor_ThumbHover[:] + } else { + sb.thumbColor = scrollbarColor_ThumbNormal[:] + } +} + +func (sb *scrollbar) cursorEnterCallback(g *GUI, entered bool) { + if !entered { + sb.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse + g.terminal.NotifyDirty() + } +} diff --git a/gui/windowManipulationImpl.go b/gui/windowManipulationImpl.go new file mode 100644 index 0000000..e6c6459 --- /dev/null +++ b/gui/windowManipulationImpl.go @@ -0,0 +1,107 @@ +package gui + +import ( + "fmt" + "github.com/go-gl/glfw/v3.2/glfw" + "github.com/liamg/aminal/terminal" +) + +// +// Implementation of the terminal.WindowManipulationInterface +// + +func (gui *GUI) RestoreWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + gui.window.Restore() + return nil + }) +} + +func (gui *GUI) IconifyWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + return gui.window.Iconify() + }) +} + +func (gui *GUI) MoveWindow(term *terminal.Terminal, pixelX int, pixelY int) error { + return gui.executeInMainThread(func() error { + gui.window.SetPos(pixelX, pixelY) + return nil + }) +} + +func (gui *GUI) ResizeWindowByPixels(term *terminal.Terminal, pixelsHeight int, pixelsWidth int) error { + return gui.executeInMainThread(func() error { + term.Unlock() + gui.window.SetSize(pixelsWidth, pixelsHeight) + term.Lock() + return nil + }) +} + +func (gui *GUI) BringWindowToFront(term *terminal.Terminal) error { + var err error + if gui.window.GetAttrib(glfw.Iconified) != 0 { + err = gui.executeInMainThread(func() error { + return gui.window.Restore() + }) + } + + if err != nil { + err = gui.window.Focus() + } + + return err +} + +func (gui *GUI) ResizeWindowByChars(term *terminal.Terminal, charsHeight int, charsWidth int) error { + return gui.executeInMainThread(func() error { + return term.SetSize(uint(charsWidth), uint(charsHeight)) + }) +} + +func (gui *GUI) MaximizeWindow(term *terminal.Terminal) error { + return gui.executeInMainThread(func() error { + term.Lock() + err := gui.window.Maximize() + term.Unlock() + return err + }) +} + +func (gui *GUI) ReportWindowState(term *terminal.Terminal) error { + // Report xterm window state. If the xterm window is open (non-iconified), it returns CSI 1 t . + // If the xterm window is iconified, it returns CSI 2 t . + if gui.window.GetAttrib(glfw.Iconified) != 0 { + _ = term.Write([]byte("\x1b[2t")) + } else { + _ = term.Write([]byte("\x1b[1t")) + } + + return nil +} + +func (gui *GUI) ReportWindowPosition(term *terminal.Terminal) error { + // Report xterm window position as CSI 3 ; x; yt + x, y := gui.window.GetPos() + + _ = term.Write([]byte(fmt.Sprintf("\x1b[3;%d;%dt", x, y))) + + return nil +} + +func (gui *GUI) ReportWindowSizeInPixels(term *terminal.Terminal) error { + // Report xterm window in pixels as CSI 4 ; height ; width t + _ = term.Write([]byte(fmt.Sprintf("\x1b[4;%d;%dt", gui.height, gui.width))) + + return nil +} + +func (gui *GUI) ReportWindowSizeInChars(term *terminal.Terminal) error { + // Report the size of the text area in characters as CSI 8 ; height ; width t + charsWidth, charsHeight := gui.renderer.GetTermSize() + + _ = term.Write([]byte(fmt.Sprintf("\x1b[8;%d;%dt", charsHeight, charsWidth))) + + return nil +} diff --git a/main.go b/main.go index 0cd8fa0..63d6a69 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,12 @@ import ( "runtime" "runtime/pprof" + "github.com/liamg/aminal/config" "github.com/liamg/aminal/gui" "github.com/liamg/aminal/platform" "github.com/liamg/aminal/terminal" "github.com/riywo/loginshell" + "time" ) type callback func(terminal *terminal.Terminal, g *gui.GUI) @@ -20,17 +22,28 @@ func init() { } func main() { - initialize(nil) + initialize(nil, nil) } -func initialize(unitTestfunc callback) { - conf := getConfig() +func initialize(unitTestfunc callback, configOverride *config.Config) { + conf := maybeGetConfig(configOverride) + logger, err := getLogger(conf) if err != nil { fmt.Printf("Failed to create logger: %s\n", err) os.Exit(1) } - defer logger.Sync() + ticker := time.NewTicker(time.Second) + go func() { + for { + <-ticker.C + logger.Sync() + } + }() + defer func() { + ticker.Stop() + logger.Sync() + }() if conf.CPUProfile != "" { logger.Infof("Starting CPU profiling...") @@ -59,7 +72,7 @@ func initialize(unitTestfunc callback) { guestProcess, err := pty.CreateGuestProcess(shellStr) if err != nil { pty.Close() - logger.Fatalf("Failed to start your shell: %s", err) + logger.Fatalf("Failed to start your shell %q: %s", shellStr, err) } defer guestProcess.Close() @@ -70,6 +83,9 @@ func initialize(unitTestfunc callback) { if err != nil { logger.Fatalf("Cannot start: %s", err) } + defer g.Free() + + terminal.WindowManipulation = g if unitTestfunc != nil { go unitTestfunc(terminal, g) diff --git a/main_test.go b/main_test.go index 5ef9b88..e82de2d 100644 --- a/main_test.go +++ b/main_test.go @@ -6,14 +6,15 @@ import ( "flag" "fmt" "os" + "runtime" "strings" "testing" "time" + "github.com/carlogit/phash" + "github.com/liamg/aminal/config" "github.com/liamg/aminal/gui" "github.com/liamg/aminal/terminal" - - "github.com/carlogit/phash" ) var termRef *terminal.Terminal @@ -45,10 +46,14 @@ func hash(path string) string { return imageHash } +func imagesAreEqual(expected string, actual string) int { + expectedHash := hash(expected) + actualHash := hash(actual) + return phash.GetDistance(expectedHash, actualHash) +} + func compareImages(expected string, actual string) { - template := hash(expected) - screen := hash(actual) - distance := phash.GetDistance(template, screen) + distance := imagesAreEqual(expected, actual) if distance != 0 { os.Exit(terminate(fmt.Sprintf("Screenshot \"%s\" doesn't match expected image \"%s\". Distance of hashes difference: %d\n", actual, expected, distance))) @@ -56,19 +61,55 @@ func compareImages(expected string, actual string) { } func send(terminal *terminal.Terminal, cmd string) { - terminal.Write([]byte(cmd)) + err := terminal.Write([]byte(cmd)) + if err != nil { + panic(err) + } } func enter(terminal *terminal.Terminal) { - terminal.Write([]byte("\n")) + err := terminal.Write([]byte("\n")) + if err != nil { + panic(err) + } } -func validateScreen(img string) { - guiRef.Screenshot(img) +func validateScreen(img string, waitForChange bool) { + fmt.Printf("taking screenshot: %s and comparing...", img) + + err := guiRef.Screenshot(img) + if err != nil { + panic(err) + } + compareImages(strings.Join([]string{"vttest/", img}, ""), img) + fmt.Printf("compare OK\n") + enter(termRef) - sleep() + + if waitForChange { + fmt.Print("Waiting for screen change...") + attempts := 10 + for { + sleep() + actualScren := "temp.png" + err = guiRef.Screenshot(actualScren) + if err != nil { + panic(err) + } + distance := imagesAreEqual(actualScren, img) + if distance != 0 { + break + } + fmt.Printf(" %d", attempts) + attempts-- + if attempts <= 0 { + break + } + } + fmt.Print("done\n") + } } func TestMain(m *testing.M) { @@ -113,17 +154,17 @@ func TestCursorMovement(t *testing.T) { os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1"))) } - validateScreen("test-cursor-movement-1.png") - validateScreen("test-cursor-movement-2.png") - validateScreen("test-cursor-movement-3.png") - validateScreen("test-cursor-movement-4.png") - validateScreen("test-cursor-movement-5.png") - validateScreen("test-cursor-movement-6.png") + validateScreen("test-cursor-movement-1.png", true) + validateScreen("test-cursor-movement-2.png", true) + validateScreen("test-cursor-movement-3.png", true) + validateScreen("test-cursor-movement-4.png", true) + validateScreen("test-cursor-movement-5.png", true) + validateScreen("test-cursor-movement-6.png", false) g.Close() } - initialize(testFunc) + initialize(testFunc, testConfig()) }) } @@ -140,26 +181,26 @@ func TestScreenFeatures(t *testing.T) { send(term, "2\n") sleep() - validateScreen("test-screen-features-1.png") - validateScreen("test-screen-features-2.png") - validateScreen("test-screen-features-3.png") - validateScreen("test-screen-features-4.png") - validateScreen("test-screen-features-5.png") - validateScreen("test-screen-features-6.png") - validateScreen("test-screen-features-7.png") - validateScreen("test-screen-features-8.png") - validateScreen("test-screen-features-9.png") - validateScreen("test-screen-features-10.png") - validateScreen("test-screen-features-11.png") - validateScreen("test-screen-features-12.png") - validateScreen("test-screen-features-13.png") - validateScreen("test-screen-features-14.png") - validateScreen("test-screen-features-15.png") + validateScreen("test-screen-features-1.png", true) + validateScreen("test-screen-features-2.png", true) + validateScreen("test-screen-features-3.png", true) + validateScreen("test-screen-features-4.png", true) + validateScreen("test-screen-features-5.png", true) + validateScreen("test-screen-features-6.png", true) + validateScreen("test-screen-features-7.png", true) + validateScreen("test-screen-features-8.png", true) + validateScreen("test-screen-features-9.png", true) + validateScreen("test-screen-features-10.png", true) + validateScreen("test-screen-features-11.png", true) + validateScreen("test-screen-features-12.png", true) + validateScreen("test-screen-features-13.png", true) + validateScreen("test-screen-features-14.png", true) + validateScreen("test-screen-features-15.png", false) g.Close() } - initialize(testFunc) + initialize(testFunc, testConfig()) }) } @@ -176,15 +217,14 @@ func TestSixel(t *testing.T) { send(term, "clear\n") sleep() send(term, "cat example.sixel\n") - sleep(4) + sleep(10) // Displaying SIXEL graphics *sometimes* takes long time. Excessive synchronization??? - guiRef.Screenshot("test-sixel.png") - validateScreen("test-sixel.png") + validateScreen("test-sixel.png", false) g.Close() } - initialize(testFunc) + initialize(testFunc, testConfig()) }) } @@ -192,3 +232,17 @@ func TestSixel(t *testing.T) { func TestExit(t *testing.T) { os.Exit(0) } + +func testConfig() *config.Config { + c := config.DefaultConfig() + + // Force the scrollbar not showing when running unit tests + c.ShowVerticalScrollbar = false + + // Use a vanilla shell on POSIX to help ensure consistency. + if runtime.GOOS != "windows" { + c.Shell = "/bin/sh" + } + + return c +} diff --git a/platform/winpty.go b/platform/winpty.go index 4bc1dc2..386ec79 100644 --- a/platform/winpty.go +++ b/platform/winpty.go @@ -4,12 +4,8 @@ package platform import ( "errors" - "syscall" - "time" - - "fmt" - "github.com/MaxRis/w32" + "syscall" ) // #include "windows.h" @@ -144,42 +140,7 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) { pty.processID = process.processID - err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT) - if err != nil { - process.Close() - return nil, err - } - - return process, err -} - -func setupChildConsole(processID C.DWORD, nStdHandle C.DWORD, mode uint) error { - C.FreeConsole() - defer C.AttachConsole(^C.DWORD(0)) // attach to parent process console - - // process may not be ready so we'll do retries - const maxWaitMilliSeconds = 5000 - const waitStepMilliSeconds = 200 - count := maxWaitMilliSeconds / waitStepMilliSeconds - - for { - if r := C.AttachConsole(processID); r != 0 { - break // success - } - lastError := C.GetLastError() - if lastError != C.ERROR_GEN_FAILURE || count <= 0 { - return fmt.Errorf("Was not able to attach to the child prosess' console") - } - - time.Sleep(time.Millisecond * time.Duration(waitStepMilliSeconds)) - count-- - } - - h := C.GetStdHandle(nStdHandle) - C.SetConsoleMode(h, C.DWORD(mode)) - C.FreeConsole() - - return nil + return process, nil } func (pty *winConPty) Resize(x, y int) error { diff --git a/terminal/csi.go b/terminal/csi.go index 6a0e3f8..63275b2 100644 --- a/terminal/csi.go +++ b/terminal/csi.go @@ -425,10 +425,6 @@ func csiSetModeHandler(params []string, terminal *Terminal) error { return csiSetModes(params, true, terminal) } -func csiWindowManipulation(params []string, terminal *Terminal) error { - return fmt.Errorf("Window manipulation is not yet supported") -} - func csiLinePositionAbsolute(params []string, terminal *Terminal) error { row := 1 if len(params) > 0 { diff --git a/terminal/notify.go b/terminal/notify.go new file mode 100644 index 0000000..96b14e1 --- /dev/null +++ b/terminal/notify.go @@ -0,0 +1,19 @@ +package terminal + +func newNotifier() *notifier { + return ¬ifier{ + 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: + } +} diff --git a/terminal/output.go b/terminal/output.go index 3425c9f..4bec2a3 100644 --- a/terminal/output.go +++ b/terminal/output.go @@ -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 } diff --git a/terminal/terminal.go b/terminal/terminal.go index f9c6ce0..170031c 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -33,6 +33,20 @@ const ( MouseExtSGR ) +type WindowManipulationInterface interface { + RestoreWindow(term *Terminal) error + IconifyWindow(term *Terminal) error + MoveWindow(term *Terminal, pixelX int, pixelY int) error + ResizeWindowByPixels(term *Terminal, pixelsHeight int, pixelsWidth int) error + BringWindowToFront(term *Terminal) error + ResizeWindowByChars(term *Terminal, charsHeight int, charsWidth int) error + MaximizeWindow(term *Terminal) error + ReportWindowState(term *Terminal) error + ReportWindowPosition(term *Terminal) error + ReportWindowSizeInPixels(term *Terminal) error + ReportWindowSizeInChars(term *Terminal) error +} + type Terminal struct { program uint32 buffers []*buffer.Buffer @@ -50,12 +64,14 @@ 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 + + WindowManipulation WindowManipulationInterface } type Modes struct { @@ -85,15 +101,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 +133,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 +190,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 +218,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 +242,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 +317,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 +395,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 +442,7 @@ func (terminal *Terminal) SetScreenMode(enabled bool) { buffer.ReverseVideo() } terminal.emitReverse(enabled) + terminal.NotifyDirty() } func (terminal *Terminal) Lock() { @@ -433,12 +452,3 @@ func (terminal *Terminal) Lock() { func (terminal *Terminal) Unlock() { terminal.lock.Unlock() } - -// SetDirtyLocked sets dirty flag locking the terminal to prevent data race warnings -// @todo remove when switching to event-driven architecture -func (terminal *Terminal) SetDirtyLocked() { - terminal.Lock() - defer terminal.Unlock() - - terminal.SetDirty() -} diff --git a/terminal/windowManipulation.go b/terminal/windowManipulation.go new file mode 100644 index 0000000..d604b07 --- /dev/null +++ b/terminal/windowManipulation.go @@ -0,0 +1,153 @@ +package terminal + +import ( + "fmt" + "strconv" +) + +func getOptionalIntegerParam(params []string, paramNo int, defValue int) (int, error) { + result := defValue + if len(params) >= paramNo+1 { + var err error + result, err = strconv.Atoi(params[paramNo]) + if err != nil { + return defValue, err + } + } + + return result, nil +} + +func getMandatoryIntegerParam(params []string, paramNo int) (int, error) { + if len(params) < paramNo+1 { + return 0, fmt.Errorf("no mandatory parameter") + } + + result, err := strconv.Atoi(params[paramNo]) + if err != nil { + return 0, err + } + + return result, nil +} + +func csiWindowManipulation(params []string, terminal *Terminal) error { + if terminal.WindowManipulation == nil { + return fmt.Errorf("Handler for CSI window manipulation commands is not set") + } + + operation, err := getMandatoryIntegerParam(params, 0) + if err != nil { + return fmt.Errorf("CSI t ignored: %s", err.Error()) + } + + switch operation { + case 1: + terminal.logger.Debug("De-iconify window") + return terminal.WindowManipulation.RestoreWindow(terminal) + + case 2: + terminal.logger.Debug("Iconify window") + return terminal.WindowManipulation.IconifyWindow(terminal) + + case 3: + terminal.logger.Debug("Move window") + { + x, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + y, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + + return terminal.WindowManipulation.MoveWindow(terminal, x, y) + } + + case 4: + terminal.logger.Debug("Resize the window in pixels") + { + height, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + width, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + + return terminal.WindowManipulation.ResizeWindowByPixels(terminal, height, width) + } + + case 5: + terminal.logger.Debug("Raise the window to the front") + return terminal.WindowManipulation.BringWindowToFront(terminal) + + case 6: + return fmt.Errorf("Lowering the window to the bottom is not implemented") + + case 7: + // NB: On Windows this sequence seem handled by the system + return fmt.Errorf("Refreshing the window is not implemented") + + case 8: + terminal.logger.Debug("Resize the text area in characters") + { + height, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + width, err := getMandatoryIntegerParam(params, 2) + if err != nil { + return err + } + return terminal.WindowManipulation.ResizeWindowByChars(terminal, height, width) + } + + case 9: + { + p, err := getMandatoryIntegerParam(params, 1) + if err != nil { + return err + } + if p == 0 { + terminal.logger.Debug("Restore maximized window") + return terminal.WindowManipulation.RestoreWindow(terminal) + } else if p == 1 { + terminal.logger.Debug("Maximize window") + return terminal.WindowManipulation.MaximizeWindow(terminal) + } + } + + case 11: + terminal.logger.Debug("Report the window state") + return terminal.WindowManipulation.ReportWindowState(terminal) + + case 13: + terminal.logger.Debug("Report the window position") + return terminal.WindowManipulation.ReportWindowPosition(terminal) + + case 14: + terminal.logger.Debug("Report the window size in pixels") + return terminal.WindowManipulation.ReportWindowSizeInPixels(terminal) + + case 18: + terminal.logger.Debug("Report the window size in characters as CSI 8") + return terminal.WindowManipulation.ReportWindowSizeInChars(terminal) + + case 19: + return fmt.Errorf("Reporting the screen size in characters is not implemented") + + case 20: + return fmt.Errorf("Reporting the window icon label is not implemented") + + case 21: + return fmt.Errorf("Reporting the window title is not implemented") + + default: + return fmt.Errorf("not supported CSI t") + } + + return nil +} diff --git a/windows/installer/installer.go b/windows/installer/installer.go index 1d47f4b..9aba6d7 100644 --- a/windows/installer/installer.go +++ b/windows/installer/installer.go @@ -5,18 +5,18 @@ package main import ( "bufio" "errors" - "golang.org/x/sys/windows/registry" - "os" - "os/user" - "strings" - "path/filepath" - "github.com/liamg/aminal/windows/winutil" - "github.com/liamg/aminal/generated-src/installer/data" - "text/template" - "io/ioutil" - "os/exec" - "syscall" "flag" + "github.com/liamg/aminal/generated-src/installer/data" + "github.com/liamg/aminal/windows/winutil" + "golang.org/x/sys/windows/registry" + "io/ioutil" + "os" + "os/exec" + "os/user" + "path/filepath" + "strings" + "syscall" + "text/template" ) const Version = "VERSION" @@ -116,7 +116,7 @@ func createRegistryKeysForUninstaller(installDir string, isUserInstall bool) { func updateVersionInRegistry(isUserInstall bool) { regRoot := getRegistryRoot(isUserInstall) updateKey := `Software\Aminal\Update\Clients\` + ProductId - writeRegStr(regRoot, updateKey, "pv", Version + ".0") + writeRegStr(regRoot, updateKey, "pv", Version+".0") writeRegStr(regRoot, updateKey, "name", "Aminal") } @@ -128,7 +128,7 @@ func getRegistryRoot(isUserInstall bool) registry.Key { } func writeRegStr(regRoot registry.Key, keyPath string, valueName string, value string) { - const mode = registry.WRITE|registry.WOW64_32KEY + const mode = registry.WRITE | registry.WOW64_32KEY key, _, err := registry.CreateKey(regRoot, keyPath, mode) check(err) defer key.Close() @@ -154,7 +154,7 @@ func getStartMenuDir(isUserInstall bool) string { func createShortcut(linkPath, targetPath string) { type Shortcut struct { - LinkPath string + LinkPath string TargetPath string } tmpl := template.New("createLnk.vbs") @@ -215,4 +215,4 @@ func check(e error) { if e != nil { panic(e) } -} \ No newline at end of file +} diff --git a/windows/launcher/launcher.go b/windows/launcher/launcher.go index 133cf65..cfb3075 100644 --- a/windows/launcher/launcher.go +++ b/windows/launcher/launcher.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "github.com/liamg/aminal/windows/winutil" "io/ioutil" "os" "os/exec" @@ -27,13 +28,12 @@ import ( "sort" "strconv" "strings" - "github.com/liamg/aminal/windows/winutil" "syscall" ) type Version struct { number [3]int - name string + name string } type Versions []Version @@ -138,4 +138,4 @@ func check(e error) { if e != nil { panic(e) } -} \ No newline at end of file +} diff --git a/windows/winutil/winutil.go b/windows/winutil/winutil.go index 99e7c5b..7536aaa 100644 --- a/windows/winutil/winutil.go +++ b/windows/winutil/winutil.go @@ -24,4 +24,4 @@ func GetExecutablePath() (string, error) { return "", e1 } return string(utf16.Decode(b[0:n])), nil -} \ No newline at end of file +}