Merge branch 'develop' into hyperlinks

This commit is contained in:
rrrooommmaaa 2019-03-15 17:05:52 +03:00 committed by GitHub
commit d5653da656
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1522 additions and 281 deletions

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @liamg * @liamg @MaxRis

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ aminal
aminal.exe aminal.exe
*.syso *.syso
.idea .idea
generated-src/

View File

@ -29,7 +29,7 @@ script:
- if [[ $TRAVIS_OS_NAME == 'osx' ]]; then make build-darwin-native-travis; fi - 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 build-linux-travis; fi
- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make windows-cross-compile-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: env:
global: global:
- secure: "pdRpTOGQSUgbC9tK37voxUYJHMWDPJEmdMhNBsljpP9VnxxbR6JEFwvOQEmUHGlsYv8jma6a17jE60ngVQk8QP12cPh48i2bdbVgym/zTUOKFawCtPAzs8i7evh0di5eZ3uoyc42kG4skc+ePuVHbXC8jDxwaPpMqSHD7QyQc1/6ckI9LLkyWUqhnJJXkVwhmI74Aa1Im6QhywAWFMeTBRRL02cwr6k7VKSYOn6yrtzJRCALFGpZ/n58lPrpDxN7W8o+HRQP89wIDy8FyNeEPdmqGFNfMHDvI3oJRN4dGC4H9EkKf/iGuNJia1Bs+MgaG9kKlMHsI6Fkh5uw9KNTvC1llx43VRQJzm26cn1CpRxxRtF4F8lqkpY4tHjxxCitV+98ddW8jdmQYyx+LeueC5wqlO9g2M5L3oXsGMqZ++mDRDa8oQoQAVUSVtimeO8ODXFuVNR8TlupP0Cthgucil63VUZfAD8EHc2zpRSFxfYByDH53uMEinn20uovL6W42fqgboC43HOnR6aVfSANPsBFDlcpZFa2BY5RkcKyYdaLkucy0DKJ946UDfhOu6FNm0GPHq5HcgWkLojNF0dEFgG6J+SGQGiPjxTlHP/zoe61qMlWu+fYRXQnKWZN5Kk0T1TbAk6pKSE6wRLG8ddxvMg+eVpGLT+gAvQdrrkMFvs=" - secure: "pdRpTOGQSUgbC9tK37voxUYJHMWDPJEmdMhNBsljpP9VnxxbR6JEFwvOQEmUHGlsYv8jma6a17jE60ngVQk8QP12cPh48i2bdbVgym/zTUOKFawCtPAzs8i7evh0di5eZ3uoyc42kG4skc+ePuVHbXC8jDxwaPpMqSHD7QyQc1/6ckI9LLkyWUqhnJJXkVwhmI74Aa1Im6QhywAWFMeTBRRL02cwr6k7VKSYOn6yrtzJRCALFGpZ/n58lPrpDxN7W8o+HRQP89wIDy8FyNeEPdmqGFNfMHDvI3oJRN4dGC4H9EkKf/iGuNJia1Bs+MgaG9kKlMHsI6Fkh5uw9KNTvC1llx43VRQJzm26cn1CpRxxRtF4F8lqkpY4tHjxxCitV+98ddW8jdmQYyx+LeueC5wqlO9g2M5L3oXsGMqZ++mDRDa8oQoQAVUSVtimeO8ODXFuVNR8TlupP0Cthgucil63VUZfAD8EHc2zpRSFxfYByDH53uMEinn20uovL6W42fqgboC43HOnR6aVfSANPsBFDlcpZFa2BY5RkcKyYdaLkucy0DKJ946UDfhOu6FNm0GPHq5HcgWkLojNF0dEFgG6J+SGQGiPjxTlHP/zoe61qMlWu+fYRXQnKWZN5Kk0T1TbAk6pKSE6wRLG8ddxvMg+eVpGLT+gAvQdrrkMFvs="

View File

@ -1,6 +1,11 @@
SHELL := /bin/bash SHELL := /bin/bash
BINARY := aminal BINARY := aminal
FONTPATH := ./gui/packed-fonts FONTPATH := ./gui/packed-fonts
GEN_SRC_DIR := ./generated-src
VERSION_MAJOR := 0
VERSION_MINOR := 9
VERSION_PATCH := 0
VERSION := ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
.PHONY: build .PHONY: build
build: build:
@ -8,7 +13,7 @@ build:
.PHONY: test .PHONY: test
test: test:
go test -v ./... go test -v -timeout=8m ./...
go vet -v go vet -v
.PHONY: check-gofmt .PHONY: check-gofmt
@ -59,6 +64,50 @@ build-windows:
windres -o aminal.syso aminal.rc windres -o aminal.syso aminal.rc
go build -o ${BINARY}-windows-amd64.exe go build -o ${BINARY}-windows-amd64.exe
.PHONY: launcher-windows
launcher-windows: build-windows
if exist "${GEN_SRC_DIR}\launcher" rmdir /S /Q "${GEN_SRC_DIR}\launcher"
xcopy "windows\launcher\*.*" "${GEN_SRC_DIR}\launcher" /K /H /Y /Q /I
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MAJOR', '${VERSION_MAJOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MINOR', '${VERSION_MINOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_PATCH', '${VERSION_PATCH}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'YEAR', (Get-Date -UFormat '%Y') | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
copy aminal.ico "${GEN_SRC_DIR}\launcher" /Y
go generate "${GEN_SRC_DIR}\launcher"
if exist "bin\windows\Aminal" rmdir /S /Q "bin\windows\Aminal"
mkdir "bin\windows\Aminal\Versions\${VERSION}"
go build -o "bin\windows\Aminal\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher"
copy ${BINARY}-windows-amd64.exe "bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe" /Y
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\${BINARY}.exe
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\${BINARY}.exe
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
.PHONY: uninstaller-windows
uninstaller-windows: launcher-windows
makensis "/XOutFile bin/windows/UninstallerSetup.exe" /NOCD windows\Uninstaller.nsi
cmd /c "bin\windows\UninstallerSetup.exe /S /D=%cd%\bin\windows\Aminal"
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\uninstall.exe
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\uninstall.exe
.PHONY: installer-windows
installer-windows: uninstaller-windows
if exist "${GEN_SRC_DIR}\installer" rmdir /S /Q "${GEN_SRC_DIR}\installer"
xcopy "windows\installer\*.*" "${GEN_SRC_DIR}\installer" /K /H /Y /Q /I
powershell -Command "(gc ${GEN_SRC_DIR}\installer\installer.go) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\installer.go"
go-bindata -prefix "bin\windows\Aminal" -o "${GEN_SRC_DIR}/installer/data/data.go" "./bin/windows/Aminal/..."
powershell -Command "(gc ${GEN_SRC_DIR}\installer\data\data.go) -creplace 'package main', 'package data' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\data\data.go"
go build -o bin/windows/AminalSetup.exe -ldflags "-H windowsgui" "${GEN_SRC_DIR}/installer/installer.go"
rem If an .exe name contains "installer", "setup" etc., then at least Windows 10 automatically
rem opens a UAC prompt upon opening it. To avoid this, we add a compatibility manifest to the .exe.
mt -manifest windows\installer\AminalSetup.exe.manifest -outputresource:bin\windows\AminalSetup.exe;1
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\AminalSetup.exe
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\AminalSetup.exe
.PHONY: build-darwin-native-travis .PHONY: build-darwin-native-travis
build-darwin-native-travis: build-darwin-native-travis:
mkdir -p bin/darwin mkdir -p bin/darwin

View File

@ -29,6 +29,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
## Checklist: ## Checklist:
- [ ] I have run `gofmt` on the project
- [ ] My code follows the style guidelines of this project - [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code - [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have commented my code, particularly in hard-to-understand areas

View File

@ -21,13 +21,17 @@ const (
SelectionRegionRectangular SelectionRegionRectangular
) )
type Notifier interface {
Notify()
}
type Buffer struct { type Buffer struct {
lines []Line lines []Line
displayChangeHandlers []chan bool displayChangeHandlers []chan bool
savedX uint16 savedX uint16
savedY uint16 savedY uint16
savedCursorAttr *CellAttributes savedCursorAttr *CellAttributes
dirty bool dirty Notifier
selectionStart *Position selectionStart *Position
selectionEnd *Position selectionEnd *Position
selectionMode SelectionMode selectionMode SelectionMode
@ -54,7 +58,7 @@ func comparePositions(pos1 *Position, pos2 *Position) int {
} }
// NewBuffer creates a new terminal buffer // NewBuffer creates a new terminal buffer
func NewBuffer(terminalState *TerminalState) *Buffer { func NewBuffer(terminalState *TerminalState, dirty Notifier) *Buffer {
b := &Buffer{ b := &Buffer{
lines: []Line{}, lines: []Line{},
selectionStart: nil, selectionStart: nil,
@ -62,6 +66,7 @@ func NewBuffer(terminalState *TerminalState) *Buffer {
selectionMode: SelectionChar, selectionMode: SelectionChar,
isSelectionComplete: true, isSelectionComplete: true,
terminalState: terminalState, terminalState: terminalState,
dirty: dirty,
} }
return b return b
} }
@ -255,17 +260,15 @@ func (buffer *Buffer) StartSelection(col uint16, viewRow uint16, mode SelectionM
} }
buffer.isSelectionComplete = false buffer.isSelectionComplete = false
buffer.dirty.Notify()
buffer.emitDisplayChange()
} }
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) { func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
if buffer.isSelectionComplete { if buffer.isSelectionComplete {
return return
} }
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
if buffer.selectionStart == nil { if buffer.selectionStart == nil {
buffer.selectionEnd = nil buffer.selectionEnd = nil
@ -289,7 +292,7 @@ func (buffer *Buffer) ClearSelection() {
buffer.selectionEnd = nil buffer.selectionEnd = nil
buffer.isSelectionComplete = true buffer.isSelectionComplete = true
buffer.emitDisplayChange() buffer.dirty.Notify()
} }
func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) { func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) {
@ -372,14 +375,6 @@ func (buffer *Buffer) InSelection(col uint16, row uint16, selectionRegionMode Se
(rawY < end.Line || (rawY == end.Line && int(col) <= end.Col)) (rawY < end.Line || (rawY == end.Line && int(col) <= end.Col))
} }
func (buffer *Buffer) IsDirty() bool {
if !buffer.dirty {
return false
}
buffer.dirty = false
return true
}
func (buffer *Buffer) HasScrollableRegion() bool { func (buffer *Buffer) HasScrollableRegion() bool {
return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1 return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1
} }
@ -399,7 +394,7 @@ func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) {
} }
func (buffer *Buffer) AreaScrollDown(lines uint16) { func (buffer *Buffer) AreaScrollDown(lines uint16) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
// NOTE: bottom is exclusive // NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange() top, bottom := buffer.getAreaScrollRange()
@ -415,7 +410,7 @@ func (buffer *Buffer) AreaScrollDown(lines uint16) {
} }
func (buffer *Buffer) AreaScrollUp(lines uint16) { func (buffer *Buffer) AreaScrollUp(lines uint16) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
// NOTE: bottom is exclusive // NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange() top, bottom := buffer.getAreaScrollRange()
@ -475,10 +470,6 @@ func (buffer *Buffer) GetRawCell(viewCol uint16, rawLine uint64) *Cell {
return &line.cells[viewCol] return &line.cells[viewCol]
} }
func (buffer *Buffer) emitDisplayChange() {
buffer.dirty = true
}
// Column returns cursor column // Column returns cursor column
func (buffer *Buffer) CursorColumn() uint16 { func (buffer *Buffer) CursorColumn() uint16 {
// @todo originMode and left margin // @todo originMode and left margin
@ -554,8 +545,7 @@ func (buffer *Buffer) deleteLine() {
} }
func (buffer *Buffer) insertLine() { func (buffer *Buffer) insertLine() {
defer buffer.dirty.Notify()
defer buffer.emitDisplayChange()
if !buffer.InScrollableRegion() { if !buffer.InScrollableRegion() {
pos := buffer.RawLine() pos := buffer.RawLine()
@ -640,7 +630,7 @@ func (buffer *Buffer) Index() {
// This sequence causes the active position to move downward one line without changing the column position. // This sequence causes the active position to move downward one line without changing the column position.
// If the active position is at the bottom margin, a scroll up is performed." // If the active position is at the bottom margin, a scroll up is performed."
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
if buffer.InScrollableRegion() { if buffer.InScrollableRegion() {
@ -666,8 +656,7 @@ func (buffer *Buffer) Index() {
} }
func (buffer *Buffer) ReverseIndex() { func (buffer *Buffer) ReverseIndex() {
defer buffer.dirty.Notify()
defer buffer.emitDisplayChange()
if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin { if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin {
buffer.AreaScrollDown(1) buffer.AreaScrollDown(1)
@ -678,7 +667,8 @@ func (buffer *Buffer) ReverseIndex() {
// Write will write a rune to the terminal at the position of the cursor, and increment the cursor position // Write will write a rune to the terminal at the position of the cursor, and increment the cursor position
func (buffer *Buffer) Write(runes ...rune) { func (buffer *Buffer) Write(runes ...rune) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
// scroll to bottom on input // scroll to bottom on input
buffer.terminalState.scrollLinesFromBottom = 0 buffer.terminalState.scrollLinesFromBottom = 0
@ -841,7 +831,7 @@ func (buffer *Buffer) MovePosition(x int16, y int16) {
} }
func (buffer *Buffer) SetPosition(col uint16, line uint16) { func (buffer *Buffer) SetPosition(col uint16, line uint16) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
useCol := col useCol := col
useLine := line useLine := line
@ -880,13 +870,20 @@ func (buffer *Buffer) GetVisibleLines() []Line {
// tested to here // tested to here
func (buffer *Buffer) Clear() { func (buffer *Buffer) Clear() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
for i := 0; i < int(buffer.ViewHeight()); i++ { for i := 0; i < int(buffer.ViewHeight()); i++ {
buffer.lines = append(buffer.lines, newLine()) buffer.lines = append(buffer.lines, newLine())
} }
buffer.SetPosition(0, 0) // do we need to set position? buffer.SetPosition(0, 0) // do we need to set position?
} }
func (buffer *Buffer) ReallyClear() {
defer buffer.dirty.Notify()
buffer.lines = []Line{}
buffer.terminalState.SetScrollOffset(0)
buffer.SetPosition(0, 0)
}
// creates if necessary // creates if necessary
func (buffer *Buffer) getCurrentLine() *Line { func (buffer *Buffer) getCurrentLine() *Line {
return buffer.getViewLine(buffer.terminalState.cursorY) return buffer.getViewLine(buffer.terminalState.cursorY)
@ -913,13 +910,13 @@ func (buffer *Buffer) getViewLine(index uint16) *Line {
} }
func (buffer *Buffer) EraseLine() { func (buffer *Buffer) EraseLine() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
line.cells = []Cell{} line.cells = []Cell{}
} }
func (buffer *Buffer) EraseLineToCursor() { func (buffer *Buffer) EraseLineToCursor() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.terminalState.cursorX); i++ { for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
if i < len(line.cells) { if i < len(line.cells) {
@ -929,7 +926,7 @@ func (buffer *Buffer) EraseLineToCursor() {
} }
func (buffer *Buffer) EraseLineFromCursor() { func (buffer *Buffer) EraseLineFromCursor() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
if len(line.cells) > 0 { if len(line.cells) > 0 {
@ -947,7 +944,7 @@ func (buffer *Buffer) EraseLineFromCursor() {
} }
func (buffer *Buffer) EraseDisplay() { func (buffer *Buffer) EraseDisplay() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
for i := uint16(0); i < (buffer.ViewHeight()); i++ { for i := uint16(0); i < (buffer.ViewHeight()); i++ {
rawLine := buffer.convertViewLineToRawLine(i) rawLine := buffer.convertViewLineToRawLine(i)
if int(rawLine) < len(buffer.lines) { if int(rawLine) < len(buffer.lines) {
@ -957,7 +954,7 @@ func (buffer *Buffer) EraseDisplay() {
} }
func (buffer *Buffer) DeleteChars(n int) { func (buffer *Buffer) DeleteChars(n int) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
if int(buffer.terminalState.cursorX) >= len(line.cells) { if int(buffer.terminalState.cursorX) >= len(line.cells) {
@ -972,7 +969,7 @@ func (buffer *Buffer) DeleteChars(n int) {
} }
func (buffer *Buffer) EraseCharacters(n int) { func (buffer *Buffer) EraseCharacters(n int) {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
@ -987,7 +984,7 @@ func (buffer *Buffer) EraseCharacters(n int) {
} }
func (buffer *Buffer) EraseDisplayFromCursor() { func (buffer *Buffer) EraseDisplayFromCursor() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
max := int(buffer.terminalState.cursorX) max := int(buffer.terminalState.cursorX)
@ -1003,7 +1000,7 @@ func (buffer *Buffer) EraseDisplayFromCursor() {
} }
func (buffer *Buffer) EraseDisplayToCursor() { func (buffer *Buffer) EraseDisplayToCursor() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
line := buffer.getCurrentLine() line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.terminalState.cursorX); i++ { for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
@ -1021,8 +1018,7 @@ func (buffer *Buffer) EraseDisplayToCursor() {
} }
func (buffer *Buffer) ResizeView(width uint16, height uint16) { func (buffer *Buffer) ResizeView(width uint16, height uint16) {
defer buffer.dirty.Notify()
defer buffer.emitDisplayChange()
if buffer.terminalState.viewHeight == 0 { if buffer.terminalState.viewHeight == 0 {
buffer.terminalState.viewWidth = width buffer.terminalState.viewWidth = width
@ -1159,7 +1155,7 @@ func (buffer *Buffer) CompareViewLines(path string) bool {
} }
func (buffer *Buffer) ReverseVideo() { func (buffer *Buffer) ReverseVideo() {
defer buffer.emitDisplayChange() defer buffer.dirty.Notify()
for _, line := range buffer.lines { for _, line := range buffer.lines {
line.ReverseVideo() line.ReverseVideo()

View File

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

View File

@ -22,12 +22,20 @@ func getActuallyProvidedFlags() map[string]bool {
return result return result
} }
func maybeGetConfig(override *config.Config) *config.Config {
if override != nil {
return override
}
return getConfig()
}
func getConfig() *config.Config { func getConfig() *config.Config {
showVersion := false showVersion := false
ignoreConfig := false ignoreConfig := false
shell := "" shell := ""
debugMode := false debugMode := false
slomo := false slomo := false
cpuProfile := ""
if flag.Parsed() == false { if flag.Parsed() == false {
flag.BoolVar(&showVersion, "version", showVersion, "Output version information") flag.BoolVar(&showVersion, "version", showVersion, "Output version information")
@ -35,6 +43,7 @@ func getConfig() *config.Config {
flag.StringVar(&shell, "shell", shell, "Specify the shell to use") flag.StringVar(&shell, "shell", shell, "Specify the shell to use")
flag.BoolVar(&debugMode, "debug", debugMode, "Enable debug logging") flag.BoolVar(&debugMode, "debug", debugMode, "Enable debug logging")
flag.BoolVar(&slomo, "slomo", slomo, "Render in slow motion (useful for debugging)") flag.BoolVar(&slomo, "slomo", slomo, "Render in slow motion (useful for debugging)")
flag.StringVar(&cpuProfile, "cpuprofile", cpuProfile, "Write a CPU profile to this file")
flag.Parse() // actual parsing and fetching flags from the command line flag.Parse() // actual parsing and fetching flags from the command line
} }
@ -51,7 +60,7 @@ func getConfig() *config.Config {
var conf *config.Config var conf *config.Config
if ignoreConfig { if ignoreConfig {
conf = &config.DefaultConfig conf = config.DefaultConfig()
} else { } else {
conf = loadConfigFile() conf = loadConfigFile()
} }
@ -69,6 +78,10 @@ func getConfig() *config.Config {
conf.Slomo = slomo conf.Slomo = slomo
} }
if actuallyProvidedFlags["cpuprofile"] {
conf.CPUProfile = cpuProfile
}
return conf return conf
} }
@ -77,12 +90,12 @@ func loadConfigFile() *config.Config {
usr, err := user.Current() usr, err := user.Current()
if err != nil { if err != nil {
fmt.Printf("Failed to get current user information: %s\n", err) fmt.Printf("Failed to get current user information: %s\n", err)
return &config.DefaultConfig return config.DefaultConfig()
} }
home := usr.HomeDir home := usr.HomeDir
if home == "" { if home == "" {
return &config.DefaultConfig return config.DefaultConfig()
} }
places := []string{} places := []string{}
@ -105,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) fmt.Printf("Failed to encode config file: %s\n", err)
} else { } else {
err = os.MkdirAll(filepath.Dir(places[0]), 0744) err = os.MkdirAll(filepath.Dir(places[0]), 0744)
@ -118,5 +131,5 @@ func loadConfigFile() *config.Config {
} }
} }
return &config.DefaultConfig return config.DefaultConfig()
} }

View File

@ -9,4 +9,5 @@ const (
ActionReportBug UserAction = "report" ActionReportBug UserAction = "report"
ActionToggleDebug UserAction = "debug" ActionToggleDebug UserAction = "debug"
ActionToggleSlomo UserAction = "slomo" ActionToggleSlomo UserAction = "slomo"
ActionBufferClear UserAction = "buffer-clear"
) )

View File

@ -7,8 +7,6 @@ import (
) )
type Config struct { type Config struct {
DebugMode bool `toml:"debug"`
Slomo bool `toml:"slomo"`
ColourScheme ColourScheme `toml:"colours"` ColourScheme ColourScheme `toml:"colours"`
DPIScale float32 `toml:"dpi-scale"` DPIScale float32 `toml:"dpi-scale"`
Shell string `toml:"shell"` Shell string `toml:"shell"`
@ -16,17 +14,22 @@ type Config struct {
SearchURL string `toml:"search_url"` SearchURL string `toml:"search_url"`
MaxLines uint64 `toml:"max_lines"` MaxLines uint64 `toml:"max_lines"`
CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"` CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"`
// Developer options.
DebugMode bool `toml:"debug"`
Slomo bool `toml:"slomo"`
CPUProfile string `toml:"cpu_profile"`
} }
type KeyMappingConfig map[string]string type KeyMappingConfig map[string]string
func Parse(data []byte) (*Config, error) { func Parse(data []byte) (*Config, error) {
c := DefaultConfig c := DefaultConfig()
err := toml.Unmarshal(data, &c) err := toml.Unmarshal(data, c)
if c.KeyMapping == nil { if c.KeyMapping == nil {
c.KeyMapping = KeyMappingConfig(map[string]string{}) c.KeyMapping = KeyMappingConfig(map[string]string{})
} }
return &c, err return c, err
} }
func (c *Config) Encode() ([]byte, error) { func (c *Config) Encode() ([]byte, error) {

View File

@ -2,7 +2,8 @@ package config
import "runtime" import "runtime"
var DefaultConfig = Config{ func DefaultConfig() *Config {
return &Config{
DebugMode: false, DebugMode: false,
ColourScheme: ColourScheme{ ColourScheme: ColourScheme{
Cursor: strToColourNoErr("#e8dfd6"), Cursor: strToColourNoErr("#e8dfd6"),
@ -26,27 +27,25 @@ var DefaultConfig = Config{
White: strToColourNoErr("#ffffff"), White: strToColourNoErr("#ffffff"),
Selection: strToColourNoErr("#333366"), Selection: strToColourNoErr("#333366"),
}, },
KeyMapping: KeyMappingConfig(map[string]string{}), 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", SearchURL: "https://www.google.com/search?q=$QUERY",
MaxLines: 1000, MaxLines: 1000,
CopyAndPasteWithMouse: true, 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")
} }
func addMod(keys string) string { func addMod(keys string) string {
standardMod := "ctrl + shift + " standardMod := "ctrl + shift + "
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
standardMod = "super + " standardMod = "super + "
} }
return standardMod + keys return standardMod + keys
} }

View File

@ -48,17 +48,15 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error)
gl.BindVertexArray(f.vao) gl.BindVertexArray(f.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo) 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"))) vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
gl.EnableVertexAttribArray(vertAttrib) gl.EnableVertexAttribArray(vertAttrib)
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0)) gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
defer gl.DisableVertexAttribArray(vertAttrib)
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00"))) texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
gl.EnableVertexAttribArray(texCoordAttrib) gl.EnableVertexAttribArray(texCoordAttrib)
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4)) gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
defer gl.DisableVertexAttribArray(texCoordAttrib)
gl.BindBuffer(gl.ARRAY_BUFFER, 0) gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gl.BindVertexArray(0) gl.BindVertexArray(0)

View File

@ -15,6 +15,7 @@ var actionMap = map[config.UserAction]func(gui *GUI){
config.ActionSearch: actionSearchSelection, config.ActionSearch: actionSearchSelection,
config.ActionToggleSlomo: actionToggleSlomo, config.ActionToggleSlomo: actionToggleSlomo,
config.ActionReportBug: actionReportBug, config.ActionReportBug: actionReportBug,
config.ActionBufferClear: actionBufferClear,
} }
func actionCopy(gui *GUI) { func actionCopy(gui *GUI) {
@ -33,7 +34,7 @@ func actionPaste(gui *GUI) {
func actionToggleDebug(gui *GUI) { func actionToggleDebug(gui *GUI) {
gui.showDebugInfo = !gui.showDebugInfo gui.showDebugInfo = !gui.showDebugInfo
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
} }
func actionSearchSelection(gui *GUI) { func actionSearchSelection(gui *GUI) {
@ -50,3 +51,7 @@ func actionToggleSlomo(gui *GUI) {
func actionReportBug(gui *GUI) { func actionReportBug(gui *GUI) {
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose") gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
} }
func actionBufferClear(gui *GUI) {
gui.terminal.ReallyClear()
}

View File

@ -24,6 +24,12 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// wakePeriod controls how often the main loop is woken up. This has
// significant impact on how Aminal feels to use. Adjust with care and
// test changes on all supported platforms.
const wakePeriod = time.Second / 120
const halfWakePeriod = wakePeriod / 2
type GUI struct { type GUI struct {
window *glfw.Window window *glfw.Window
logger *zap.SugaredLogger logger *zap.SugaredLogger
@ -59,6 +65,8 @@ type GUI struct {
mouseMovedAfterSelectionStarted bool mouseMovedAfterSelectionStarted bool
internalResize bool internalResize bool
selectionRegionMode buffer.SelectionRegionMode selectionRegionMode buffer.SelectionRegionMode
mainThreadFunc chan func()
} }
func Min(x, y int) int { func Min(x, y int) int {
@ -161,6 +169,8 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
keyboardShortcuts: shortcuts, keyboardShortcuts: shortcuts,
resizeLock: &sync.Mutex{}, resizeLock: &sync.Mutex{},
internalResize: false, internalResize: false,
mainThreadFunc: make(chan func()),
}, nil }, nil
} }
@ -173,15 +183,23 @@ func (gui *GUI) scale() float32 {
} }
// can only be called on OS thread // can only be called on OS thread
func (gui *GUI) resizeToTerminal(newCols uint, newRows uint) { func (gui *GUI) resizeToTerminal() {
if gui.window.GetAttrib(glfw.Iconified) != 0 { if gui.window.GetAttrib(glfw.Iconified) != 0 {
return return
} }
// Order of locking:
// 1. resizeLock
// 2. terminal's lock
gui.resizeLock.Lock() gui.resizeLock.Lock()
defer gui.resizeLock.Unlock() defer gui.resizeLock.Unlock()
gui.terminal.Lock()
defer gui.terminal.Unlock()
termCols, termRows := gui.terminal.GetSize()
newCols := uint(termCols)
newRows := uint(termRows)
cols, rows := gui.renderer.GetTermSize() cols, rows := gui.renderer.GetTermSize()
if cols == newCols && rows == newRows { if cols == newCols && rows == newRows {
return return
@ -244,9 +262,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
return return
} }
// Order of locking:
// 1. resizeLock
// 2. terminal's lock
terminalAlreadyLocked := false
if gui.internalResize == false { if gui.internalResize == false {
gui.resizeLock.Lock() gui.resizeLock.Lock()
defer gui.resizeLock.Unlock() defer gui.resizeLock.Unlock()
// No need to lock the terminal right away, we can lock it later
} else {
terminalAlreadyLocked = true
} }
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height) gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
@ -268,6 +293,11 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.logger.Debugf("Calculating size in cols/rows...") gui.logger.Debugf("Calculating size in cols/rows...")
cols, rows := gui.renderer.GetTermSize() cols, rows := gui.renderer.GetTermSize()
gui.logger.Debugf("Resizing internal terminal...") gui.logger.Debugf("Resizing internal terminal...")
if !terminalAlreadyLocked {
gui.terminal.Lock()
defer gui.terminal.Unlock()
terminalAlreadyLocked = true
}
if err := gui.terminal.SetSize(cols, rows); err != nil { if err := gui.terminal.SetSize(cols, rows); err != nil {
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err) gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
} }
@ -278,11 +308,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.logger.Debugf("Setting viewport size...") gui.logger.Debugf("Setting viewport size...")
gl.Viewport(0, 0, int32(gui.width), int32(gui.height)) gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
if !terminalAlreadyLocked {
gui.terminal.Lock()
defer gui.terminal.Unlock()
terminalAlreadyLocked = true
}
gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight) gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight)
gui.logger.Debugf("Resize complete!") gui.logger.Debugf("Resize complete!")
gui.redraw() gui.redraw(!terminalAlreadyLocked)
gui.window.SwapBuffers() gui.window.SwapBuffers()
} }
@ -295,6 +330,7 @@ func (gui *GUI) getTermSize() (uint, uint) {
func (gui *GUI) Close() { func (gui *GUI) Close() {
gui.window.SetShouldClose(true) gui.window.SetShouldClose(true)
glfw.PostEmptyEvent() // wake up main loop so it notices close request
} }
func (gui *GUI) Render() error { func (gui *GUI) Render() error {
@ -337,11 +373,11 @@ func (gui *GUI) Render() error {
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback) gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
gui.window.SetCursorPosCallback(gui.mouseMoveCallback) gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
gui.window.SetRefreshCallback(func(w *glfw.Window) { gui.window.SetRefreshCallback(func(w *glfw.Window) {
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
}) })
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) { gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
if focused { if focused {
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
} }
}) })
gui.window.SetPosCallback(gui.windowPosChangeCallback) gui.window.SetPosCallback(gui.windowPosChangeCallback)
@ -354,6 +390,10 @@ func (gui *GUI) Render() error {
gui.resize(gui.window, w, h) gui.resize(gui.window, w, h)
} }
gui.terminal.AttachTitleChangeHandler(titleChan)
gui.terminal.AttachResizeHandler(resizeChan)
gui.terminal.AttachReverseHandler(reverseChan)
gui.logger.Debugf("Starting pty read handling...") gui.logger.Debugf("Starting pty read handling...")
go func() { go func() {
@ -372,10 +412,6 @@ func (gui *GUI) Render() error {
gl.Disable(gl.DEPTH_TEST) gl.Disable(gl.DEPTH_TEST)
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gui.terminal.AttachTitleChangeHandler(titleChan)
gui.terminal.AttachResizeHandler(resizeChan)
gui.terminal.AttachReverseHandler(reverseChan)
ticker := time.NewTicker(time.Second) ticker := time.NewTicker(time.Second)
defer ticker.Stop() defer ticker.Stop()
@ -394,34 +430,18 @@ func (gui *GUI) Render() error {
r, err := version.GetNewerRelease() r, err := version.GetNewerRelease()
if err == nil && r != nil { if err == nil && r != nil {
latestVersion = r.TagName latestVersion = r.TagName
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
} }
}() }()
startTime := time.Now() startTime := time.Now()
showMessage := true showMessage := true
stop := make(chan struct{})
go gui.waker(stop)
for !gui.window.ShouldClose() { for !gui.window.ShouldClose() {
gui.redraw(true)
forceRedraw := false
select {
case <-titleChan:
gui.window.SetTitle(gui.terminal.GetTitle())
case <-resizeChan:
cols, rows := gui.terminal.GetSize()
gui.resizeToTerminal(uint(cols), uint(rows))
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()
if gui.showDebugInfo { if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
@ -441,7 +461,7 @@ Buffer Size: %d lines
if showMessage { if showMessage {
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 { if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.SetDirty) time.AfterFunc(time.Second, gui.terminal.NotifyDirty)
_, h := gui.terminal.GetSize() _, h := gui.terminal.GetSize()
var msg string var msg string
if version.Version == "" { if version.Version == "" {
@ -462,17 +482,74 @@ Buffer Size: %d lines
} }
gui.SwapBuffers() gui.SwapBuffers()
glfw.WaitEvents() // Go to sleep until next event.
// Process any terminal events since the last wakeup.
terminalEvents:
for {
select {
case <-titleChan:
gui.window.SetTitle(gui.terminal.GetTitle())
case <-resizeChan:
gui.resizeToTerminal()
case reverse := <-reverseChan:
gui.generateDefaultCell(reverse)
case funcForMainThread := <-gui.mainThreadFunc:
funcForMainThread()
default:
break terminalEvents
}
}
} }
} close(stop) // Tell waker to end.
gui.logger.Debugf("Stopping render...") gui.logger.Debugf("Stopping render...")
return nil return nil
} }
func (gui *GUI) redraw() { // waker is a goroutine which listens to the terminal's dirty channel,
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT) // waking up the main thread when the GUI needs to be
// redrawn. Limiting is applied on wakeups to avoid excessive CPU
// usage when the terminal is being updated rapidly.
func (gui *GUI) waker(stop <-chan struct{}) {
dirty := gui.terminal.Dirty()
var nextWake <-chan time.Time
var last time.Time
for {
select {
case <-dirty:
if nextWake == nil {
if time.Since(last) > wakePeriod {
// There hasn't been a wakeup recently so schedule
// the next one sooner.
nextWake = time.After(halfWakePeriod)
} else {
nextWake = time.After(wakePeriod)
}
}
case last = <-nextWake:
// TODO(mjs) - This is somewhat of a voodoo sleep but it
// avoid various rendering issues on Windows in some
// situations. Suspect that this will become unnecessary
// once various goroutine synchronisation issues have been
// resolved.
time.Sleep(halfWakePeriod)
glfw.PostEmptyEvent()
nextWake = nil
case <-stop:
return
}
}
}
func (gui *GUI) renderTerminalData(shouldLock bool) {
if shouldLock {
gui.terminal.Lock()
defer gui.terminal.Unlock()
}
lines := gui.terminal.GetVisibleLines() lines := gui.terminal.GetVisibleLines()
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight()) lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
colCount := int(gui.terminal.ActiveBuffer().ViewWidth()) colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
@ -632,6 +709,11 @@ func (gui *GUI) redraw() {
} }
}
func (gui *GUI) redraw(shouldLock bool) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
gui.renderTerminalData(shouldLock)
gui.renderOverlay() gui.renderOverlay()
} }
@ -779,3 +861,14 @@ func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) {
func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorEvent) { func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorEvent) {
gui.SetDPIScale() gui.SetDPIScale()
} }
// 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
}

View File

@ -52,7 +52,7 @@ func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) {
} }
if gui.selectionRegionMode != mode { if gui.selectionRegionMode != mode {
gui.selectionRegionMode = mode gui.selectionRegionMode = mode
gui.terminal.SetDirty() gui.terminal.NotifyDirty()
} }
} }

View File

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

View File

@ -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
}

33
main.go
View File

@ -2,12 +2,16 @@ package main
import ( import (
"fmt" "fmt"
"log"
"os"
"runtime"
"runtime/pprof"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/gui" "github.com/liamg/aminal/gui"
"github.com/liamg/aminal/platform" "github.com/liamg/aminal/platform"
"github.com/liamg/aminal/terminal" "github.com/liamg/aminal/terminal"
"github.com/riywo/loginshell" "github.com/riywo/loginshell"
"os"
"runtime"
) )
type callback func(terminal *terminal.Terminal, g *gui.GUI) type callback func(terminal *terminal.Terminal, g *gui.GUI)
@ -17,11 +21,12 @@ func init() {
} }
func main() { func main() {
initialize(nil) initialize(nil, nil)
} }
func initialize(unitTestfunc callback) { func initialize(unitTestfunc callback, configOverride *config.Config) {
conf := getConfig() conf := maybeGetConfig(configOverride)
logger, err := getLogger(conf) logger, err := getLogger(conf)
if err != nil { if err != nil {
fmt.Printf("Failed to create logger: %s\n", err) fmt.Printf("Failed to create logger: %s\n", err)
@ -29,8 +34,13 @@ func initialize(unitTestfunc callback) {
} }
defer logger.Sync() defer logger.Sync()
logger.Infof("Allocating pty...") if conf.CPUProfile != "" {
logger.Infof("Starting CPU profiling...")
stop := startCPUProf(conf.CPUProfile)
defer stop()
}
logger.Infof("Allocating pty...")
pty, err := platform.NewPty(80, 25) pty, err := platform.NewPty(80, 25)
if err != nil { if err != nil {
logger.Fatalf("Failed to allocate pty: %s", err) logger.Fatalf("Failed to allocate pty: %s", err)
@ -63,6 +73,8 @@ func initialize(unitTestfunc callback) {
logger.Fatalf("Cannot start: %s", err) logger.Fatalf("Cannot start: %s", err)
} }
terminal.WindowManipulation = g
if unitTestfunc != nil { if unitTestfunc != nil {
go unitTestfunc(terminal, g) go unitTestfunc(terminal, g)
} else { } else {
@ -78,3 +90,12 @@ func initialize(unitTestfunc callback) {
logger.Fatalf("Render error: %s", err) logger.Fatalf("Render error: %s", err)
} }
} }
func startCPUProf(filename string) func() {
profileFile, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(profileFile)
return pprof.StopCPUProfile
}

View File

@ -6,10 +6,12 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/gui" "github.com/liamg/aminal/gui"
"github.com/liamg/aminal/terminal" "github.com/liamg/aminal/terminal"
@ -105,7 +107,11 @@ func TestCursorMovement(t *testing.T) {
send(term, "1\n") send(term, "1\n")
sleep() sleep()
if term.ActiveBuffer().CompareViewLines("vttest/test-cursor-movement-1") == false { term.Lock()
compareResult := term.ActiveBuffer().CompareViewLines("vttest/test-cursor-movement-1")
term.Unlock()
if compareResult == false {
os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1"))) os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1")))
} }
@ -119,7 +125,7 @@ func TestCursorMovement(t *testing.T) {
g.Close() g.Close()
} }
initialize(testFunc) initialize(testFunc, testConfig())
}) })
} }
@ -146,14 +152,7 @@ func TestScreenFeatures(t *testing.T) {
validateScreen("test-screen-features-8.png") validateScreen("test-screen-features-8.png")
validateScreen("test-screen-features-9.png") validateScreen("test-screen-features-9.png")
validateScreen("test-screen-features-10.png") validateScreen("test-screen-features-10.png")
validateScreen("test-screen-features-11.png")
// 11th screen test is not passing https://github.com/liamg/aminal/issues/207
//g.Screenshot("vttest/test-screen-features-11.png")
//compareImages("vttest/test-screen-features-11.png", "vttest/test-screen-features-11.png")
enter(term)
sleep()
validateScreen("test-screen-features-12.png") validateScreen("test-screen-features-12.png")
validateScreen("test-screen-features-13.png") validateScreen("test-screen-features-13.png")
validateScreen("test-screen-features-14.png") validateScreen("test-screen-features-14.png")
@ -162,7 +161,7 @@ func TestScreenFeatures(t *testing.T) {
g.Close() g.Close()
} }
initialize(testFunc) initialize(testFunc, testConfig())
}) })
} }
@ -187,7 +186,7 @@ func TestSixel(t *testing.T) {
g.Close() g.Close()
} }
initialize(testFunc) initialize(testFunc, testConfig())
}) })
} }
@ -195,3 +194,14 @@ func TestSixel(t *testing.T) {
func TestExit(t *testing.T) { func TestExit(t *testing.T) {
os.Exit(0) os.Exit(0)
} }
func testConfig() *config.Config {
c := config.DefaultConfig()
// Use a vanilla shell on POSIX to help ensure consistency.
if runtime.GOOS != "windows" {
c.Shell = "/bin/sh"
}
return c
}

View File

@ -19,4 +19,5 @@ type Pty interface {
Resize(x int, y int) error Resize(x int, y int) error
CreateGuestProcess(imagePath string) (Process, error) CreateGuestProcess(imagePath string) (Process, error)
GetPlatformDependentSettings() PlatformDependentSettings GetPlatformDependentSettings() PlatformDependentSettings
Clear()
} }

View File

@ -86,6 +86,10 @@ func (pty *unixPty) GetPlatformDependentSettings() PlatformDependentSettings {
return pty.platformDependentSettings return pty.platformDependentSettings
} }
func (pty *unixPty) Clear() {
// do nothing for unix
}
func NewPty(x, y int) (Pty, error) { func NewPty(x, y int) (Pty, error) {
innerPty, innerTty, err := pty.Open() innerPty, innerTty, err := pty.Open()
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"fmt" "fmt"
"github.com/MaxRis/w32" "github.com/MaxRis/w32"
) )
@ -98,6 +99,7 @@ type winConPty struct {
innerInPipe syscall.Handle innerInPipe syscall.Handle
innerOutPipe syscall.Handle innerOutPipe syscall.Handle
hcon uintptr hcon uintptr
processID uint32 // required to obtain old-style console handle (for standard console functions)
platformDependentSettings PlatformDependentSettings platformDependentSettings PlatformDependentSettings
} }
@ -140,6 +142,8 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) {
return nil, err return nil, err
} }
pty.processID = process.processID
err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT) err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT)
if err != nil { if err != nil {
process.Close() process.Close()
@ -192,6 +196,50 @@ func (pty *winConPty) GetPlatformDependentSettings() PlatformDependentSettings {
return pty.platformDependentSettings return pty.platformDependentSettings
} }
func (pty *winConPty) Clear() {
C.FreeConsole()
defer C.AttachConsole(^C.DWORD(0)) // attach to parent process console
if C.AttachConsole(C.DWORD(pty.processID)) == 0 {
return
}
defer C.FreeConsole()
hConsole := C.GetStdHandle(C.STD_OUTPUT_HANDLE)
var coordScreen C.COORD
coordScreen.X = 0
coordScreen.Y = 0
var cCharsWritten C.DWORD
var csbi C.CONSOLE_SCREEN_BUFFER_INFO
// Get the number of character cells in the current buffer.
if C.GetConsoleScreenBufferInfo(hConsole, &csbi) != C.BOOL(0) {
dwConSize := C.DWORD(csbi.dwSize.X * csbi.dwSize.Y)
// Fill the entire screen with blanks.
C.FillConsoleOutputCharacterA(hConsole, // Handle to console screen buffer
' ', // Character to write to the buffer
dwConSize, // Number of cells to write
coordScreen, // Coordinates of first cell
&cCharsWritten) // Receive number of characters written
// Get the current text attribute.
if C.GetConsoleScreenBufferInfo(hConsole, &csbi) != C.BOOL(0) {
// Set the buffer's attributes accordingly.
C.FillConsoleOutputAttribute(hConsole, // Handle to console screen buffer
csbi.wAttributes, // Character attributes to use
dwConSize, // Number of cells to set attribute
coordScreen, // Coordinates of first cell
&cCharsWritten) // Receive number of characters written
}
}
// Put the cursor at its home coordinates.
C.SetConsoleCursorPosition(hConsole, coordScreen)
}
// NewPty creates a new instance of a Pty implementation for Windows on a newly allocated ConPTY // NewPty creates a new instance of a Pty implementation for Windows on a newly allocated ConPTY
func NewPty(x, y int) (pty Pty, err error) { func NewPty(x, y int) (pty Pty, err error) {
if !ptyInitSucceeded { if !ptyInitSucceeded {

View File

@ -35,26 +35,42 @@ func swallowHandler(n int) func(pty chan rune, terminal *Terminal) error {
} }
func risHandler(pty chan rune, terminal *Terminal) error { func risHandler(pty chan rune, terminal *Terminal) error {
terminal.Lock()
defer terminal.Unlock()
terminal.ActiveBuffer().Clear() terminal.ActiveBuffer().Clear()
return nil return nil
} }
func indexHandler(pty chan rune, terminal *Terminal) error { func indexHandler(pty chan rune, terminal *Terminal) error {
terminal.Lock()
defer terminal.Unlock()
terminal.ActiveBuffer().Index() terminal.ActiveBuffer().Index()
return nil return nil
} }
func reverseIndexHandler(pty chan rune, terminal *Terminal) error { func reverseIndexHandler(pty chan rune, terminal *Terminal) error {
terminal.Lock()
defer terminal.Unlock()
terminal.ActiveBuffer().ReverseIndex() terminal.ActiveBuffer().ReverseIndex()
return nil return nil
} }
func saveCursorHandler(pty chan rune, terminal *Terminal) error { func saveCursorHandler(pty chan rune, terminal *Terminal) error {
// Handler should lock the terminal if there will be write operations to any data read by the renderer
// terminal.Lock()
// defer terminal.Unlock()
terminal.ActiveBuffer().SaveCursor() terminal.ActiveBuffer().SaveCursor()
return nil return nil
} }
func restoreCursorHandler(pty chan rune, terminal *Terminal) error { func restoreCursorHandler(pty chan rune, terminal *Terminal) error {
terminal.Lock()
defer terminal.Unlock()
terminal.ActiveBuffer().RestoreCursor() terminal.ActiveBuffer().RestoreCursor()
return nil return nil
} }
@ -73,11 +89,18 @@ func ansiHandler(pty chan rune, terminal *Terminal) error {
} }
func nextLineHandler(pty chan rune, terminal *Terminal) error { func nextLineHandler(pty chan rune, terminal *Terminal) error {
terminal.Lock()
defer terminal.Unlock()
terminal.ActiveBuffer().NewLineEx(true) terminal.ActiveBuffer().NewLineEx(true)
return nil return nil
} }
func tabSetHandler(pty chan rune, terminal *Terminal) error { func tabSetHandler(pty chan rune, terminal *Terminal) error {
// Handler should lock the terminal if there will be write operations to any data read by the renderer
// terminal.Lock()
// defer terminal.Unlock()
terminal.terminalState.TabSetAtCursor() terminal.terminalState.TabSetAtCursor()
return nil return nil
} }

View File

@ -54,6 +54,10 @@ func scs1Handler(pty chan rune, terminal *Terminal) error {
func scsHandler(pty chan rune, terminal *Terminal, which int) error { func scsHandler(pty chan rune, terminal *Terminal, which int) error {
b := <-pty b := <-pty
// Handler should lock the terminal if there will be write operations to any data read by the renderer
// terminal.Lock()
// defer terminal.Unlock()
cs, ok := charSets[b] cs, ok := charSets[b]
if ok { if ok {
terminal.logger.Debugf("Selected charset %v into G%v", string(b), which) terminal.logger.Debugf("Selected charset %v into G%v", string(b), which)

View File

@ -89,6 +89,9 @@ func splitParams(paramString string) []string {
func csiHandler(pty chan rune, terminal *Terminal) error { func csiHandler(pty chan rune, terminal *Terminal) error {
final, param, intermediate := loadCSI(pty) final, param, intermediate := loadCSI(pty)
terminal.Lock()
defer terminal.Unlock()
// process intermediate control codes before the CSI // process intermediate control codes before the CSI
for _, b := range intermediate { for _, b := range intermediate {
terminal.processRune(b) terminal.processRune(b)
@ -422,10 +425,6 @@ func csiSetModeHandler(params []string, terminal *Terminal) error {
return csiSetModes(params, true, terminal) 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 { func csiLinePositionAbsolute(params []string, terminal *Terminal) error {
row := 1 row := 1
if len(params) > 0 { if len(params) > 0 {

19
terminal/notify.go Normal file
View File

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

View File

@ -28,25 +28,25 @@ var runeMap = map[rune]runeHandler{
func newLineHandler(terminal *Terminal) error { func newLineHandler(terminal *Terminal) error {
terminal.ActiveBuffer().NewLine() terminal.ActiveBuffer().NewLine()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func tabHandler(terminal *Terminal) error { func tabHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Tab() terminal.ActiveBuffer().Tab()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func carriageReturnHandler(terminal *Terminal) error { func carriageReturnHandler(terminal *Terminal) error {
terminal.ActiveBuffer().CarriageReturn() terminal.ActiveBuffer().CarriageReturn()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
func backspaceHandler(terminal *Terminal) error { func backspaceHandler(terminal *Terminal) error {
terminal.ActiveBuffer().Backspace() terminal.ActiveBuffer().Backspace()
terminal.isDirty = true terminal.NotifyDirty()
return nil return nil
} }
@ -72,17 +72,24 @@ func shiftInHandler(terminal *Terminal) error {
return nil return nil
} }
func (terminal *Terminal) processRuneLocked(b rune) {
terminal.Lock()
defer terminal.Unlock()
terminal.processRune(b)
}
func (terminal *Terminal) processRune(b rune) { func (terminal *Terminal) processRune(b rune) {
defer terminal.NotifyDirty()
if handler, ok := runeMap[b]; ok { if handler, ok := runeMap[b]; ok {
if err := handler(terminal); err != nil { if err := handler(terminal); err != nil {
terminal.logger.Errorf("Error handling control code: %s", err) terminal.logger.Errorf("Error handling control code: %s", err)
} }
terminal.isDirty = true
return return
} }
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b)) //terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
terminal.ActiveBuffer().Write(terminal.translateRune(b)) terminal.ActiveBuffer().Write(terminal.translateRune(b))
terminal.isDirty = true
} }
func (terminal *Terminal) translateRune(b rune) rune { func (terminal *Terminal) translateRune(b rune) rune {
@ -103,6 +110,8 @@ func (terminal *Terminal) processInput(pty chan rune) {
var b rune var b rune
// debug := ""
for { for {
if terminal.config.Slomo { if terminal.config.Slomo {
@ -111,15 +120,19 @@ func (terminal *Terminal) processInput(pty chan rune) {
b = <-pty b = <-pty
// debug += fmt.Sprintf("0x%x ", b)
if b == 0x1b { if b == 0x1b {
// terminal.logger.Debug(debug)
// debug = ""
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b) //terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
if err := ansiHandler(pty, terminal); err != nil { if err := ansiHandler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err) terminal.logger.Errorf("Error handling escape sequence: %s", err)
} }
terminal.isDirty = true terminal.NotifyDirty()
continue continue
} }
terminal.processRune(b) terminal.processRuneLocked(b)
} }
} }

View File

@ -6,6 +6,9 @@ func screenStateHandler(pty chan rune, terminal *Terminal) error {
b := <-pty b := <-pty
switch b { switch b {
case '8': // DECALN -- Screen Alignment Pattern case '8': // DECALN -- Screen Alignment Pattern
terminal.Lock()
defer terminal.Unlock()
// hide cursor? // hide cursor?
buffer := terminal.ActiveBuffer() buffer := terminal.ActiveBuffer()
terminal.ResetVerticalMargins() terminal.ResetVerticalMargins()

View File

@ -137,6 +137,9 @@ func sixelHandler(pty chan rune, terminal *Terminal) error {
defer terminal.SetLineFeedMode() defer terminal.SetLineFeedMode()
} }
terminal.Lock()
defer terminal.Unlock()
drawSixel(six, terminal) drawSixel(six, terminal)
return nil return nil

View File

@ -33,6 +33,20 @@ const (
MouseExtSGR 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 { type Terminal struct {
program uint32 program uint32
buffers []*buffer.Buffer buffers []*buffer.Buffer
@ -50,12 +64,14 @@ type Terminal struct {
mouseMode MouseMode mouseMode MouseMode
mouseExtMode MouseExtMode mouseExtMode MouseExtMode
bracketedPasteMode bool bracketedPasteMode bool
isDirty bool
charWidth float32 charWidth float32
charHeight float32 charHeight float32
lastBuffer uint8 lastBuffer uint8
terminalState *buffer.TerminalState terminalState *buffer.TerminalState
platformDependentSettings platform.PlatformDependentSettings platformDependentSettings platform.PlatformDependentSettings
dirty *notifier
WindowManipulation WindowManipulationInterface
} }
type Modes struct { type Modes struct {
@ -85,15 +101,28 @@ func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Te
ShowCursor: true, ShowCursor: true,
}, },
platformDependentSettings: pty.GetPlatformDependentSettings(), platformDependentSettings: pty.GetPlatformDependentSettings(),
dirty: newNotifier(),
} }
t.buffers = []*buffer.Buffer{ t.buffers = []*buffer.Buffer{
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
buffer.NewBuffer(t.terminalState), buffer.NewBuffer(t.terminalState, t.dirty),
} }
t.activeBuffer = t.buffers[0] t.activeBuffer = t.buffers[0]
return t
return t
}
// Dirty returns a channel that receives an empty struct whenever the
// terminal becomes dirty.
func (terminal *Terminal) Dirty() <-chan struct{} {
return terminal.dirty.C
}
// NotifyDirty is used to signal that the terminal is dirty and the
// screen must be redrawn.
func (terminal *Terminal) NotifyDirty() {
terminal.dirty.Notify()
} }
func (terminal *Terminal) SetProgram(program uint32) { func (terminal *Terminal) SetProgram(program uint32) {
@ -104,16 +133,6 @@ func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
terminal.bracketedPasteMode = enabled terminal.bracketedPasteMode = enabled
} }
func (terminal *Terminal) CheckDirty() bool {
d := terminal.isDirty
terminal.isDirty = false
return d || terminal.ActiveBuffer().IsDirty()
}
func (terminal *Terminal) SetDirty() {
terminal.isDirty = true
}
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool { func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
return terminal.modes.ApplicationCursorKeys return terminal.modes.ApplicationCursorKeys
} }
@ -172,7 +191,7 @@ func (terminal *Terminal) GetScrollOffset() uint {
} }
func (terminal *Terminal) ScreenScrollDown(lines uint16) { func (terminal *Terminal) ScreenScrollDown(lines uint16) {
defer terminal.SetDirty() defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer() buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) { if buffer.Height() < int(buffer.ViewHeight()) {
@ -200,7 +219,7 @@ func (terminal *Terminal) AreaScrollDown(lines uint16) {
} }
func (terminal *Terminal) ScreenScrollUp(lines uint16) { func (terminal *Terminal) ScreenScrollUp(lines uint16) {
defer terminal.SetDirty() defer terminal.NotifyDirty()
buffer := terminal.ActiveBuffer() buffer := terminal.ActiveBuffer()
if buffer.Height() < int(buffer.ViewHeight()) { if buffer.Height() < int(buffer.ViewHeight()) {
@ -224,8 +243,8 @@ func (terminal *Terminal) ScrollPageUp() {
} }
func (terminal *Terminal) ScrollToEnd() { func (terminal *Terminal) ScrollToEnd() {
defer terminal.SetDirty()
terminal.terminalState.SetScrollOffset(0) terminal.terminalState.SetScrollOffset(0)
terminal.NotifyDirty()
} }
func (terminal *Terminal) GetVisibleLines() []buffer.Line { func (terminal *Terminal) GetVisibleLines() []buffer.Line {
@ -299,6 +318,7 @@ func (terminal *Terminal) GetTitle() string {
func (terminal *Terminal) SetTitle(title string) { func (terminal *Terminal) SetTitle(title string) {
terminal.title = title terminal.title = title
terminal.emitTitleChange() terminal.emitTitleChange()
terminal.NotifyDirty()
} }
// Write sends data, i.e. locally typed keystrokes to the pty // Write sends data, i.e. locally typed keystrokes to the pty
@ -351,14 +371,16 @@ func (terminal *Terminal) Clear() {
terminal.ActiveBuffer().Clear() terminal.ActiveBuffer().Clear()
} }
func (terminal *Terminal) ReallyClear() {
terminal.pty.Clear()
terminal.ActiveBuffer().ReallyClear()
}
func (terminal *Terminal) GetSize() (int, int) { func (terminal *Terminal) GetSize() (int, int) {
return int(terminal.size.Width), int(terminal.size.Height) return int(terminal.size.Width), int(terminal.size.Height)
} }
func (terminal *Terminal) SetSize(newCols uint, newLines uint) error { func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
terminal.lock.Lock()
defer terminal.lock.Unlock()
if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) { if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) {
return nil return nil
} }
@ -374,6 +396,7 @@ func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height) terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
terminal.emitResize() terminal.emitResize()
terminal.NotifyDirty()
return nil return nil
} }
@ -420,4 +443,13 @@ func (terminal *Terminal) SetScreenMode(enabled bool) {
buffer.ReverseVideo() buffer.ReverseVideo()
} }
terminal.emitReverse(enabled) terminal.emitReverse(enabled)
terminal.NotifyDirty()
}
func (terminal *Terminal) Lock() {
terminal.lock.Lock()
}
func (terminal *Terminal) Unlock() {
terminal.lock.Unlock()
} }

View File

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -12,8 +12,7 @@
cd %YOUR_PROJECT_WORKING_DIR% cd %YOUR_PROJECT_WORKING_DIR%
mkdir go\src\github.com\liamg mkdir go\src\github.com\liamg
cd go\src\github.com\liamg cd go\src\github.com\liamg
git clone git@github.com:jumptrading/aminal-mirror.git git clone https://github.com/liamg/aminal.git
move aminal-mirror aminal
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
set GOBIN=%GOPATH%/bin set GOBIN=%GOPATH%/bin
@ -28,3 +27,95 @@ go install
Look for the aminal.exe built binary under your %GOBIN% path Look for the aminal.exe built binary under your %GOBIN% path
### Building an installer for automatic updates:
In addition to the above commands:
```
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
go get golang.org/x/sys/windows
go get github.com/jteeuwen/go-bindata/...
```
Install NSIS and place it on the `PATH`:
```
choco install nsis
set PATH=%PATH%;%ProgramFiles(x86)%\NSIS\Bin
```
Ensure `signtool.exe` is on the `PATH`. For instance, on Windows 10:
```
set PATH=%PATH%;%ProgramFiles(x86)%\Windows Kits\10\bin\10.0.17763.0\x64
```
Copy your code signing certificate to `go\src\github.com\liamg\windows\codesigning_certificate.pfx`.
Set the `WINDOWS_CODESIGNING_CERT_PW` to the password of your code signing certificate:
```
set WINDOWS_CODESIGNING_CERT_PW=PASSWORD
```
Compile Aminal and build the installer:
```
mingw32-make installer-windows
```
This produces several files in `bin/windows`. Their purpose is explained below.
### How Aminal's automatic update mechanism works (on Windows)
Aminal uses a technology called [Google Omaha](https://github.com/google/omaha) for automatic updates on Windows. It's the same technology which Google use to auto-update Chrome. For a quick introduction, see [this Google Omaha tutorial](https://fman.io/blog/google-omaha-tutorial/).
Aminal has an online installer. This is a 1 MB executable, which was created using Google Omaha. Suppose it's called `OnlineInstaller.exe`. When a user runs it, the following things happen:
* `OnlineInstaller.exe` installs Aminal's version of Google Omaha on the user's system. In particular, this creates two `AminalUpdateTask...` tasks in the Windows Task Scheduler. They're set up to run once per day in the background and check for updates.
* `OnlineInstaller.exe` contacts Aminal's update server and asks "what is the latest version?". The server responds with the URL to an _offline_ installer and some command line parameters. Say this offline installer is called `install-aminal-0.9.0.exe`.
* `OnlineInstaller.exe` downloads `install-aminal-0.9.0.exe` and invokes it with the given command line arguments, typically `-install`.
* `install-aminal-0.9.0.exe` performs the following steps:
* It installs Aminal 0.9.0 into `%LOCALAPPDATA%\Aminal`.
* It sets some registry keys in `HKCU\Software\Aminal`. This lets the `AminalUpdateTask...` tasks know which version of Aminal is installed.
* It creates a Start menu shortcut for starting Aminal.
When the update tasks run, you will see `AminalUpdate.exe` in the Windows Task Manager. They use the registry to send the current Aminal version to the update server and ask "is there a new version?". If yes, the server again responds with the URL to an `.exe` and some command line parameters. In Aminal's current setup, this too is `install-aminal-0.9.0.exe` (if the user had, say, Aminal 0.8.9 installed). But this time the command line flag is `-update`. The `.exe` again installs the current version of Aminal and updates the registry.
The offline installer `install-aminal-0.9.0.exe` is actually what's produced by the `mingw32-make installer-windows` command in the previous section. It is placed at `bin/windows/AminalSetup.exe` and supports the command line flags `-install`, `-update` or none. In the last case (i.e. when invoked without arguments), it acts as a normal offline installer to be invoked by the user, and does not set Omaha's registry keys. The source code for this installer lies in `windows/installer/installer.go`.
Due to the asynchronous nature of the update tasks, it can happen that Aminal is running while a new version is being downloaded / installed. To prevent this from breaking Aminal's running instance, Aminal's install dir `%LOCALAPPDATA%\Aminal` contains the following hierarchy:
* `Aminal.exe`
* `Versions/`
* `0.9.0/`
* `Aminal.exe`
The top-level `Aminal.exe` is a launcher that always invokes the latest version. When an update is downloaded / installed, it is placed in a new subfolder of `Versions/`. For instance:
* `Aminal.exe`
* `Versions/`
* `0.9.0/`
* `0.9.1/`
The next time the top-level `Aminal.exe` is invoked, it runs `Versions/0.9.1/Aminal.exe`.
The code for this top-level launcher is in `windows/launcher/launcher.go`. Its binary (and the current version subdirectory) is produced in `bin/windows` when you do `mingw32-make installer-windows`.
#### Forcing updates
By default, Aminal's (/Omaha's) update tasks only run once per day. To force an immediate update, perform the following steps:
* Delete the registry key `HKCU\Software\Aminal\Updated\LastChecked`.
* Run the task `AminalUpdateTask...UA` in the Windows Task Scheduler. Press `F5`. You'll see its result change to `0x41301`. This means it's currently running. You'll also see `AminalUpdate.exe` in the Task _Scheduler_. Keep refreshing with `F5` until both disappear.
#### Uninstalling Aminal
The installer above adds an uninstaller to the user's _Add or remove programs_ panel in Windows. When the user goes to the Control Panel and uninstalls Aminal this way, the install directory and start menu entries are removed. Further, the registry key `HKCU\Software\Aminal\Clients\{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}` is removed. What's not removed immediately is Omaha. (So you'll still see the update tasks in the Task Scheduler.) But! The next time the update tasks run, they realize by looking at the registry that Aminal is no longer installed and uninstall themselves (and Omaha).
To work around some potential permission issues, the uninstaller is not implemented in Go (like the installer and launcher above). But via NSIS. The source code is in `windows/Uninstaller.nsi`. It's an "installer" whose sole purpose is to generate `bin/windows/Aminal/uninstall.exe`.
#### Releasing a new version via automatic updates
To release a new version, update the `VERSION` fields in `Makefile`. Then, invoke `mingw32-make installer-windows`. Log into the Omaha update server, add a new Version for Aminal that mirrors the one you set in `Makefile`. You'll need to add a trailing `.0` to the version number on the server, because Omaha uses four-tuples for versions (`0.9.1.0` instead of `0.9.1`). As the "File" for the version, upload `bin/windows/AminalSetup.exe`. Add two "Action"s: One for Event _install_, one for event _update_. In both cases, instruct the server to _Run_ `AminalSetup.exe`. For the install Event, supply Arguments `-install`. For the update Event, supply Arguments `-update`.
Once you have done this, the background update tasks on your users' systems will (over the next 24 hours) download and install the new version of Aminal from the server.

70
windows/Uninstaller.nsi Normal file
View File

@ -0,0 +1,70 @@
!include MUI2.nsh
;--------------------------------
!define MULTIUSER_EXECUTIONLEVEL Standard
;Add support for command-line args that let uninstaller know whether to
;uninstall machine- or user installation:
!define MULTIUSER_INSTALLMODE_COMMANDLINE
!include MultiUser.nsh
!include LogicLib.nsh
Function .onInit
!insertmacro MULTIUSER_INIT
FunctionEnd
Function un.onInit
!insertmacro MULTIUSER_UNINIT
FunctionEnd
;--------------------------------
;General
Name "Aminal"
;--------------------------------
;Pages
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
;--------------------------------
;Languages
!insertmacro MUI_LANGUAGE "English"
;--------------------------------
;Installer Sections
Section
SetOutPath "$InstDir"
WriteUninstaller "$InstDir\uninstall.exe"
SectionEnd
;--------------------------------
;Uninstaller Section
!define UNINST_KEY \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\Aminal"
!define ROOT_KEY "Software\Aminal"
!define UPDATE_KEY \
"${ROOT_KEY}\Update\Clients\{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}"
Section "Uninstall"
RMDir /r "$InstDir\Versions"
Delete "$InstDir\Aminal.exe"
Delete "$InstDir\uninstall.exe"
;Omaha leaves this directory behind. Delete if empty:
RMDir "$InstDir\CrashReports"
RMDir "$InstDir"
Delete "$SMPROGRAMS\Aminal.lnk"
DeleteRegKey SHCTX "${UNINST_KEY}"
DeleteRegKey SHCTX "${UPDATE_KEY}"
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}\Update\Clients"
;Try to speed up uninstall of Omaha:
DeleteRegValue SHCTX "${ROOT_KEY}\Update" "LastChecked"
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}\Update"
WriteRegStr SHCTX "${ROOT_KEY}" "" ""
DeleteRegKey /ifempty SHCTX "${ROOT_KEY}"
SectionEnd

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>True/PM</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,218 @@
// +build windows
package main
import (
"bufio"
"errors"
"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"
const ProductId = `{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}`
func main() {
doInstallPtr := flag.Bool("install", false, "Install Aminal")
doUpdatePtr := flag.Bool("update", false, "Update Aminal")
flag.Parse()
var installDir string
isUserInstall := strings.HasPrefix(os.Args[0], os.Getenv("LOCALAPPDATA"))
if *doInstallPtr {
installDir = getInstallDirWhenManagedByOmaha()
extractAssets(installDir)
createRegistryKeysForUninstaller(installDir, isUserInstall)
updateVersionInRegistry(isUserInstall)
createStartMenuShortcut(installDir, isUserInstall)
launchAminal(installDir)
} else if *doUpdatePtr {
installDir = getInstallDirWhenManagedByOmaha()
extractAssets(installDir)
updateVersionInRegistry(isUserInstall)
removeOldVersions(installDir)
} else {
// Offline installer.
// We don't know whether we're being executed with Admin privileges.
// It's also not easy to determine. So perform user install:
isUserInstall = true
installDir = getDefaultInstallDir()
extractAssets(installDir)
createRegistryKeysForUninstaller(installDir, isUserInstall)
createStartMenuShortcut(installDir, isUserInstall)
launchAminal(installDir)
}
}
func getInstallDirWhenManagedByOmaha() string {
executablePath, err := winutil.GetExecutablePath()
check(err)
result := executablePath
prevResult := ""
for filepath.Base(result) != "Aminal" {
prevResult = result
result = filepath.Dir(result)
if result == prevResult {
break
}
}
if result == prevResult {
msg := "Could not find parent directory 'Aminal' above " + executablePath
check(errors.New(msg))
}
return result
}
func getDefaultInstallDir() string {
localAppData := os.Getenv("LOCALAPPDATA")
if localAppData == "" {
panic("Environment variable LOCALAPPDATA is not set.")
}
return filepath.Join(localAppData, "Aminal")
}
func extractAssets(installDir string) {
for _, relPath := range data.AssetNames() {
bytes, err := data.Asset(relPath)
check(err)
absPath := filepath.Join(installDir, relPath)
check(os.MkdirAll(filepath.Dir(absPath), 0755))
f, err := os.OpenFile(absPath, os.O_CREATE, 0755)
check(err)
defer f.Close()
w := bufio.NewWriter(f)
_, err = w.Write(bytes)
check(err)
w.Flush()
}
}
func createRegistryKeysForUninstaller(installDir string, isUserInstall bool) {
regRoot := getRegistryRoot(isUserInstall)
uninstKey := `Software\Microsoft\Windows\CurrentVersion\Uninstall\Aminal`
writeRegStr(regRoot, uninstKey, "", installDir)
writeRegStr(regRoot, uninstKey, "DisplayName", "Aminal")
writeRegStr(regRoot, uninstKey, "Publisher", "Liam Galvin")
uninstaller := filepath.Join(installDir, "uninstall.exe")
uninstString := `"` + uninstaller + `"`
if isUserInstall {
uninstString += " /CurrentUser"
} else {
uninstString += " /AllUsers"
}
writeRegStr(regRoot, uninstKey, "UninstallString", uninstString)
}
func updateVersionInRegistry(isUserInstall bool) {
regRoot := getRegistryRoot(isUserInstall)
updateKey := `Software\Aminal\Update\Clients\` + ProductId
writeRegStr(regRoot, updateKey, "pv", Version+".0")
writeRegStr(regRoot, updateKey, "name", "Aminal")
}
func getRegistryRoot(isUserInstall bool) registry.Key {
if isUserInstall {
return registry.CURRENT_USER
}
return registry.LOCAL_MACHINE
}
func writeRegStr(regRoot registry.Key, keyPath string, valueName string, value string) {
const mode = registry.WRITE | registry.WOW64_32KEY
key, _, err := registry.CreateKey(regRoot, keyPath, mode)
check(err)
defer key.Close()
check(key.SetStringValue(valueName, value))
}
func createStartMenuShortcut(installDir string, isUserInstall bool) {
startMenuDir := getStartMenuDir(isUserInstall)
linkPath := filepath.Join(startMenuDir, "Programs", "Aminal.lnk")
targetPath := filepath.Join(installDir, "Aminal.exe")
createShortcut(linkPath, targetPath)
}
func getStartMenuDir(isUserInstall bool) string {
if isUserInstall {
usr, err := user.Current()
check(err)
return usr.HomeDir + `\AppData\Roaming\Microsoft\Windows\Start Menu`
} else {
return os.Getenv("ProgramData") + `\Microsoft\Windows\Start Menu`
}
}
func createShortcut(linkPath, targetPath string) {
type Shortcut struct {
LinkPath string
TargetPath string
}
tmpl := template.New("createLnk.vbs")
tmpl, err := tmpl.Parse(`Set oWS = WScript.CreateObject("WScript.Shell")
sLinkFile = "{{.LinkPath}}"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = "{{.TargetPath}}"
oLink.Save
WScript.Quit 0`)
check(err)
tmpDir, err := ioutil.TempDir("", "Aminal")
check(err)
createLnk := filepath.Join(tmpDir, "createLnk.vbs")
defer os.RemoveAll(tmpDir)
f, err := os.Create(createLnk)
check(err)
defer f.Close()
w := bufio.NewWriter(f)
shortcut := Shortcut{linkPath, targetPath}
check(tmpl.Execute(w, shortcut))
w.Flush()
f.Close()
cmd := exec.Command("cscript", f.Name())
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
check(cmd.Run())
}
func launchAminal(installDir string) {
cmd := exec.Command(getAminalExePath(installDir))
check(cmd.Start())
}
func getAminalExePath(installDir string) string {
return filepath.Join(installDir, "Aminal.exe")
}
func removeOldVersions(installDir string) {
versionsDir := filepath.Join(installDir, "Versions")
versions, err := ioutil.ReadDir(versionsDir)
check(err)
for _, version := range versions {
if version.Name() == Version {
continue
}
versionPath := filepath.Join(versionsDir, version.Name())
// Try deleting the main executable first. We do this to prevent a
// version that is still running from being deleted.
mainExecutable := filepath.Join(versionPath, "Aminal.exe")
err = os.Remove(mainExecutable)
if err == nil {
// Remove the rest:
check(os.RemoveAll(versionPath))
}
}
}
func check(e error) {
if e != nil {
panic(e)
}
}

View File

@ -0,0 +1,124 @@
// +build windows
//go:generate goversioninfo -icon=aminal.ico
/*
Looks at directory "Versions" next to this executable. Finds the latest version
and runs the executable with the same name as this executable in that directory.
Eg.:
Aminal.exe (=launcher.exe)
Versions/
1.0.0/
Aminal.exe
1.0.1
Aminal.exe
-> Launches Versions/1.0.1/Aminal.exe.
*/
package main
import (
"errors"
"fmt"
"github.com/liamg/aminal/windows/winutil"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
)
type Version struct {
number [3]int
name string
}
type Versions []Version
func main() {
executable, err := winutil.GetExecutablePath()
check(err)
executableDir, executableName := filepath.Split(executable)
versionsDir := filepath.Join(executableDir, "Versions")
latestVersion, err := getLatestVersion(versionsDir)
check(err)
target := filepath.Join(versionsDir, latestVersion, executableName)
cmd := exec.Command(target, os.Args[1:]...)
check(cmd.Start())
}
func getLatestVersion(versionsDir string) (string, error) {
potentialVersions, err := ioutil.ReadDir(versionsDir)
if err != nil {
return "", err
}
var versions Versions
for _, file := range potentialVersions {
if !file.IsDir() {
continue
}
version, err := parseVersionString(file.Name())
if err != nil {
continue
}
versions = append(versions, version)
}
if len(versions) == 0 {
errMsg := fmt.Sprintf("No valid version in %s.", versionsDir)
return "", errors.New(errMsg)
}
sort.Sort(versions)
return versions[len(versions)-1].String(), nil
}
func parseVersionString(version string) (Version, error) {
var result Version
result.name = version
err := error(nil)
version = strings.TrimSuffix(version, "-SNAPSHOT")
parts := strings.Split(version, ".")
if len(parts) != len(result.number) {
err = errors.New("Wrong number of parts.")
} else {
for i, partStr := range parts {
result.number[i], err = strconv.Atoi(partStr)
if err != nil {
break
}
}
}
return result, err
}
func (arr Versions) Len() int {
return len(arr)
}
func (arr Versions) Less(i, j int) bool {
for k, left := range arr[i].number {
right := arr[j].number[k]
if left > right {
return false
} else if left < right {
return true
}
}
fmt.Printf("%s < %s\n", arr[i], arr[j])
return true
}
func (arr Versions) Swap(i, j int) {
tmp := arr[j]
arr[j] = arr[i]
arr[i] = tmp
}
func (version Version) String() string {
return version.name
}
func check(e error) {
if e != nil {
panic(e)
}
}

View File

@ -0,0 +1,35 @@
{
"FixedFileInfo":
{
"FileVersion": {
"Major": VERSION_MAJOR,
"Minor": VERSION_MINOR,
"Patch": VERSION_PATCH,
"Build": 0
},
"ProductVersion": {
"Major": VERSION_MAJOR,
"Minor": VERSION_MINOR,
"Patch": VERSION_PATCH,
"Build": 0
},
"FileFlagsMask": "3f",
"FileFlags ": "00",
"FileOS": "040004",
"FileType": "01",
"FileSubType": "00"
},
"StringFileInfo":
{
"ProductName": "Aminal",
"ProductVersion": "VERSION",
"LegalCopyright": "Copyright 2018-YEAR Liam Galvin"
},
"VarFileInfo":
{
"Translation": {
"LangID": "0409",
"CharsetID": "04B0"
}
}
}

View File

@ -0,0 +1,27 @@
// +build windows
package winutil
import (
"golang.org/x/sys/windows"
"unicode/utf16"
"unsafe"
)
var (
kernel = windows.MustLoadDLL("kernel32.dll")
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
)
func GetExecutablePath() (string, error) {
var n uint32
b := make([]uint16, windows.MAX_PATH)
size := uint32(len(b))
bPtr := uintptr(unsafe.Pointer(&b[0]))
r0, _, e1 := getModuleFileNameProc.Call(0, bPtr, uintptr(size))
n = uint32(r0)
if n == 0 {
return "", e1
}
return string(utf16.Decode(b[0:n])), nil
}