mirror of https://github.com/liamg/aminal.git
Merge remote-tracking branch 'upstream/develop' into fix-windows-launcher
This commit is contained in:
commit
ab5c4b8b8a
|
@ -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="
|
||||
|
|
2
Makefile
2
Makefile
|
@ -13,7 +13,7 @@ build:
|
|||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v ./...
|
||||
go test -v -timeout=8m ./...
|
||||
go vet -v
|
||||
|
||||
.PHONY: check-gofmt
|
||||
|
|
|
@ -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
|
||||
|
|
33
README.md
33
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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
17
config.go
17
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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
10
gui/fonts.go
10
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
|
||||
}
|
||||
|
|
341
gui/gui.go
341
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
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) {
|
|||
}
|
||||
if gui.selectionRegionMode != mode {
|
||||
gui.selectionRegionMode = mode
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
95
gui/mouse.go
95
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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
150
gui/renderer.go
150
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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
26
main.go
26
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)
|
||||
|
|
128
main_test.go
128
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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,4 @@ func GetExecutablePath() (string, error) {
|
|||
return "", e1
|
||||
}
|
||||
return string(utf16.Decode(b[0:n])), nil
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue