mirror of https://github.com/liamg/aminal.git
Merge branch 'develop' into hyperlinks
This commit is contained in:
commit
d5653da656
|
@ -1 +1 @@
|
|||
* @liamg
|
||||
* @liamg @MaxRis
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
aminal
|
||||
aminal.exe
|
||||
*.syso
|
||||
.idea
|
||||
.idea
|
||||
generated-src/
|
|
@ -29,7 +29,7 @@ script:
|
|||
- if [[ $TRAVIS_OS_NAME == 'osx' ]]; then make build-darwin-native-travis; fi
|
||||
- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make build-linux-travis; fi
|
||||
- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make windows-cross-compile-travis; fi
|
||||
- if [[ $TRAVIS_OS_NAME == 'linux' ]]; then make check-gofmt; fi
|
||||
- if [[ $TRAVIS_OS_NAME == 'linux' && $TRAVIS_GO_VERSION =~ ^1\.11\. ]]; then echo 'check-gofmt'; make check-gofmt; fi
|
||||
env:
|
||||
global:
|
||||
- secure: "pdRpTOGQSUgbC9tK37voxUYJHMWDPJEmdMhNBsljpP9VnxxbR6JEFwvOQEmUHGlsYv8jma6a17jE60ngVQk8QP12cPh48i2bdbVgym/zTUOKFawCtPAzs8i7evh0di5eZ3uoyc42kG4skc+ePuVHbXC8jDxwaPpMqSHD7QyQc1/6ckI9LLkyWUqhnJJXkVwhmI74Aa1Im6QhywAWFMeTBRRL02cwr6k7VKSYOn6yrtzJRCALFGpZ/n58lPrpDxN7W8o+HRQP89wIDy8FyNeEPdmqGFNfMHDvI3oJRN4dGC4H9EkKf/iGuNJia1Bs+MgaG9kKlMHsI6Fkh5uw9KNTvC1llx43VRQJzm26cn1CpRxxRtF4F8lqkpY4tHjxxCitV+98ddW8jdmQYyx+LeueC5wqlO9g2M5L3oXsGMqZ++mDRDa8oQoQAVUSVtimeO8ODXFuVNR8TlupP0Cthgucil63VUZfAD8EHc2zpRSFxfYByDH53uMEinn20uovL6W42fqgboC43HOnR6aVfSANPsBFDlcpZFa2BY5RkcKyYdaLkucy0DKJ946UDfhOu6FNm0GPHq5HcgWkLojNF0dEFgG6J+SGQGiPjxTlHP/zoe61qMlWu+fYRXQnKWZN5Kk0T1TbAk6pKSE6wRLG8ddxvMg+eVpGLT+gAvQdrrkMFvs="
|
||||
|
|
51
Makefile
51
Makefile
|
@ -1,6 +1,11 @@
|
|||
SHELL := /bin/bash
|
||||
BINARY := aminal
|
||||
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
|
||||
build:
|
||||
|
@ -8,7 +13,7 @@ build:
|
|||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v ./...
|
||||
go test -v -timeout=8m ./...
|
||||
go vet -v
|
||||
|
||||
.PHONY: check-gofmt
|
||||
|
@ -59,6 +64,50 @@ build-windows:
|
|||
windres -o aminal.syso aminal.rc
|
||||
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
|
||||
build-darwin-native-travis:
|
||||
mkdir -p bin/darwin
|
||||
|
|
|
@ -29,6 +29,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
|
|||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have run `gofmt` on the project
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
|
|
|
@ -21,13 +21,17 @@ const (
|
|||
SelectionRegionRectangular
|
||||
)
|
||||
|
||||
type Notifier interface {
|
||||
Notify()
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
lines []Line
|
||||
displayChangeHandlers []chan bool
|
||||
savedX uint16
|
||||
savedY uint16
|
||||
savedCursorAttr *CellAttributes
|
||||
dirty bool
|
||||
dirty Notifier
|
||||
selectionStart *Position
|
||||
selectionEnd *Position
|
||||
selectionMode SelectionMode
|
||||
|
@ -54,7 +58,7 @@ func comparePositions(pos1 *Position, pos2 *Position) int {
|
|||
}
|
||||
|
||||
// NewBuffer creates a new terminal buffer
|
||||
func NewBuffer(terminalState *TerminalState) *Buffer {
|
||||
func NewBuffer(terminalState *TerminalState, dirty Notifier) *Buffer {
|
||||
b := &Buffer{
|
||||
lines: []Line{},
|
||||
selectionStart: nil,
|
||||
|
@ -62,6 +66,7 @@ func NewBuffer(terminalState *TerminalState) *Buffer {
|
|||
selectionMode: SelectionChar,
|
||||
isSelectionComplete: true,
|
||||
terminalState: terminalState,
|
||||
dirty: dirty,
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
@ -255,17 +260,15 @@ func (buffer *Buffer) StartSelection(col uint16, viewRow uint16, mode SelectionM
|
|||
}
|
||||
|
||||
buffer.isSelectionComplete = false
|
||||
|
||||
buffer.emitDisplayChange()
|
||||
buffer.dirty.Notify()
|
||||
}
|
||||
|
||||
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
|
||||
|
||||
if buffer.isSelectionComplete {
|
||||
return
|
||||
}
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
if buffer.selectionStart == nil {
|
||||
buffer.selectionEnd = nil
|
||||
|
@ -289,7 +292,7 @@ func (buffer *Buffer) ClearSelection() {
|
|||
buffer.selectionEnd = nil
|
||||
buffer.isSelectionComplete = true
|
||||
|
||||
buffer.emitDisplayChange()
|
||||
buffer.dirty.Notify()
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
func (buffer *Buffer) IsDirty() bool {
|
||||
if !buffer.dirty {
|
||||
return false
|
||||
}
|
||||
buffer.dirty = false
|
||||
return true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) HasScrollableRegion() bool {
|
||||
return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1
|
||||
}
|
||||
|
@ -399,7 +394,7 @@ func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) AreaScrollDown(lines uint16) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
// NOTE: bottom is exclusive
|
||||
top, bottom := buffer.getAreaScrollRange()
|
||||
|
@ -415,7 +410,7 @@ func (buffer *Buffer) AreaScrollDown(lines uint16) {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) AreaScrollUp(lines uint16) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
// NOTE: bottom is exclusive
|
||||
top, bottom := buffer.getAreaScrollRange()
|
||||
|
@ -475,10 +470,6 @@ func (buffer *Buffer) GetRawCell(viewCol uint16, rawLine uint64) *Cell {
|
|||
return &line.cells[viewCol]
|
||||
}
|
||||
|
||||
func (buffer *Buffer) emitDisplayChange() {
|
||||
buffer.dirty = true
|
||||
}
|
||||
|
||||
// Column returns cursor column
|
||||
func (buffer *Buffer) CursorColumn() uint16 {
|
||||
// @todo originMode and left margin
|
||||
|
@ -554,8 +545,7 @@ func (buffer *Buffer) deleteLine() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) insertLine() {
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
if !buffer.InScrollableRegion() {
|
||||
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.
|
||||
// If the active position is at the bottom margin, a scroll up is performed."
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
if buffer.InScrollableRegion() {
|
||||
|
||||
|
@ -666,8 +656,7 @@ func (buffer *Buffer) Index() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) ReverseIndex() {
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin {
|
||||
buffer.AreaScrollDown(1)
|
||||
|
@ -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
|
||||
func (buffer *Buffer) Write(runes ...rune) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
// scroll to bottom on input
|
||||
buffer.terminalState.scrollLinesFromBottom = 0
|
||||
|
||||
|
@ -841,7 +831,7 @@ func (buffer *Buffer) MovePosition(x int16, y int16) {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) SetPosition(col uint16, line uint16) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
useCol := col
|
||||
useLine := line
|
||||
|
@ -880,13 +870,20 @@ func (buffer *Buffer) GetVisibleLines() []Line {
|
|||
// tested to here
|
||||
|
||||
func (buffer *Buffer) Clear() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
for i := 0; i < int(buffer.ViewHeight()); i++ {
|
||||
buffer.lines = append(buffer.lines, newLine())
|
||||
}
|
||||
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
|
||||
func (buffer *Buffer) getCurrentLine() *Line {
|
||||
return buffer.getViewLine(buffer.terminalState.cursorY)
|
||||
|
@ -913,13 +910,13 @@ func (buffer *Buffer) getViewLine(index uint16) *Line {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseLine() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
line := buffer.getCurrentLine()
|
||||
line.cells = []Cell{}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) EraseLineToCursor() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
line := buffer.getCurrentLine()
|
||||
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
||||
if i < len(line.cells) {
|
||||
|
@ -929,7 +926,7 @@ func (buffer *Buffer) EraseLineToCursor() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseLineFromCursor() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
line := buffer.getCurrentLine()
|
||||
|
||||
if len(line.cells) > 0 {
|
||||
|
@ -947,7 +944,7 @@ func (buffer *Buffer) EraseLineFromCursor() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseDisplay() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
for i := uint16(0); i < (buffer.ViewHeight()); i++ {
|
||||
rawLine := buffer.convertViewLineToRawLine(i)
|
||||
if int(rawLine) < len(buffer.lines) {
|
||||
|
@ -957,7 +954,7 @@ func (buffer *Buffer) EraseDisplay() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) DeleteChars(n int) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
line := buffer.getCurrentLine()
|
||||
if int(buffer.terminalState.cursorX) >= len(line.cells) {
|
||||
|
@ -972,7 +969,7 @@ func (buffer *Buffer) DeleteChars(n int) {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseCharacters(n int) {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
line := buffer.getCurrentLine()
|
||||
|
||||
|
@ -987,7 +984,7 @@ func (buffer *Buffer) EraseCharacters(n int) {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseDisplayFromCursor() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
line := buffer.getCurrentLine()
|
||||
|
||||
max := int(buffer.terminalState.cursorX)
|
||||
|
@ -1003,7 +1000,7 @@ func (buffer *Buffer) EraseDisplayFromCursor() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) EraseDisplayToCursor() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
line := buffer.getCurrentLine()
|
||||
|
||||
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
||||
|
@ -1021,8 +1018,7 @@ func (buffer *Buffer) EraseDisplayToCursor() {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) ResizeView(width uint16, height uint16) {
|
||||
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
if buffer.terminalState.viewHeight == 0 {
|
||||
buffer.terminalState.viewWidth = width
|
||||
|
@ -1159,7 +1155,7 @@ func (buffer *Buffer) CompareViewLines(path string) bool {
|
|||
}
|
||||
|
||||
func (buffer *Buffer) ReverseVideo() {
|
||||
defer buffer.emitDisplayChange()
|
||||
defer buffer.dirty.Notify()
|
||||
|
||||
for _, line := range buffer.lines {
|
||||
line.ReverseVideo()
|
||||
|
|
|
@ -9,8 +9,24 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBufferCreation(t *testing.T) {
|
||||
b, n := makeBufferForTesting(10, 20)
|
||||
assert.Equal(t, uint16(10), b.Width())
|
||||
assert.Equal(t, uint16(20), b.ViewHeight())
|
||||
assert.Equal(t, uint16(0), b.CursorColumn())
|
||||
assert.Equal(t, uint16(0), b.CursorLine())
|
||||
assert.NotNil(t, b.lines)
|
||||
n.AssertNotNotified(t)
|
||||
}
|
||||
|
||||
func TestWriteNotify(t *testing.T) {
|
||||
b, n := makeBufferForTesting(30, 3)
|
||||
b.Write(rune('x'))
|
||||
n.AssertNotified(t)
|
||||
}
|
||||
|
||||
func TestTabbing(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(30, 3, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(30, 3)
|
||||
b.Write([]rune("hello")...)
|
||||
b.Tab()
|
||||
b.Write([]rune("x")...)
|
||||
|
@ -39,7 +55,7 @@ hell xxx good
|
|||
}
|
||||
|
||||
func TestOffsets(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(10, 3, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(10, 3)
|
||||
b.Write([]rune("hello")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -59,18 +75,8 @@ func TestOffsets(t *testing.T) {
|
|||
assert.Equal(t, 5, b.Height())
|
||||
}
|
||||
|
||||
func TestBufferCreation(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(10, 20, CellAttributes{}, 1000))
|
||||
assert.Equal(t, uint16(10), b.Width())
|
||||
assert.Equal(t, uint16(20), b.ViewHeight())
|
||||
assert.Equal(t, uint16(0), b.CursorColumn())
|
||||
assert.Equal(t, uint16(0), b.CursorLine())
|
||||
assert.NotNil(t, b.lines)
|
||||
}
|
||||
|
||||
func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
|
||||
|
||||
b := NewBuffer(NewTerminalState(5, 4, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(5, 4)
|
||||
|
||||
/*01234
|
||||
|-----
|
||||
|
@ -117,7 +123,7 @@ func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(3, 20)
|
||||
b.terminalState.LineFeedMode = false
|
||||
|
||||
b.Write('a', 'b', 'c')
|
||||
|
@ -142,7 +148,7 @@ func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(3, 20)
|
||||
b.terminalState.LineFeedMode = false
|
||||
/*
|
||||
|abc
|
||||
|
@ -170,46 +176,59 @@ func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSetPosition(t *testing.T) {
|
||||
|
||||
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
|
||||
b, n := makeBufferForTesting(120, 80)
|
||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||
assert.Equal(t, 0, int(b.CursorLine()))
|
||||
|
||||
b.SetPosition(60, 10)
|
||||
assert.Equal(t, 60, int(b.CursorColumn()))
|
||||
assert.Equal(t, 10, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.SetPosition(0, 0)
|
||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||
assert.Equal(t, 0, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.SetPosition(120, 90)
|
||||
assert.Equal(t, 119, int(b.CursorColumn()))
|
||||
assert.Equal(t, 79, int(b.CursorLine()))
|
||||
|
||||
n.AssertNotified(t)
|
||||
}
|
||||
|
||||
func TestMovePosition(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
|
||||
b, n := makeBufferForTesting(120, 80)
|
||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||
assert.Equal(t, 0, int(b.CursorLine()))
|
||||
|
||||
b.MovePosition(-1, -1)
|
||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||
assert.Equal(t, 0, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.MovePosition(30, 20)
|
||||
assert.Equal(t, 30, int(b.CursorColumn()))
|
||||
assert.Equal(t, 20, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.MovePosition(30, 20)
|
||||
assert.Equal(t, 60, int(b.CursorColumn()))
|
||||
assert.Equal(t, 40, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.MovePosition(-1, -1)
|
||||
assert.Equal(t, 59, int(b.CursorColumn()))
|
||||
assert.Equal(t, 39, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.MovePosition(100, 100)
|
||||
assert.Equal(t, 119, int(b.CursorColumn()))
|
||||
assert.Equal(t, 79, int(b.CursorLine()))
|
||||
n.AssertNotified(t)
|
||||
}
|
||||
|
||||
func TestVisibleLines(t *testing.T) {
|
||||
|
||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 10)
|
||||
b.Write([]rune("hello 1")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -259,7 +278,7 @@ func TestVisibleLines(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClearWithoutFullView(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 10)
|
||||
b.Write([]rune("hello 1")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -275,7 +294,7 @@ func TestClearWithoutFullView(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClearWithFullView(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello 1")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -306,7 +325,7 @@ func TestClearWithFullView(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCarriageReturn(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 20)
|
||||
b.Write([]rune("hello!")...)
|
||||
b.CarriageReturn()
|
||||
b.Write([]rune("secret")...)
|
||||
|
@ -315,7 +334,7 @@ func TestCarriageReturn(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCarriageReturnOnFullLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(20, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(20, 20)
|
||||
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
||||
b.CarriageReturn()
|
||||
b.Write([]rune("xxxxxxxxxxxxxxxxxxxx")...)
|
||||
|
@ -324,7 +343,7 @@ func TestCarriageReturnOnFullLine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCarriageReturnOnFullLastLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(20, 2, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(20, 2)
|
||||
b.NewLine()
|
||||
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
||||
b.CarriageReturn()
|
||||
|
@ -335,7 +354,7 @@ func TestCarriageReturnOnFullLastLine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCarriageReturnOnWrappedLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 6, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 6)
|
||||
b.Write([]rune("hello!")...)
|
||||
b.CarriageReturn()
|
||||
b.Write([]rune("secret")...)
|
||||
|
@ -345,7 +364,7 @@ func TestCarriageReturnOnWrappedLine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(6, 10, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(6, 10)
|
||||
b.terminalState.cursorY = 3
|
||||
b.CarriageReturn()
|
||||
assert.Equal(t, uint16(0), b.terminalState.cursorX)
|
||||
|
@ -353,7 +372,7 @@ func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetCell(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 20)
|
||||
b.Write([]rune("Hello")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -369,7 +388,7 @@ func TestGetCell(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetCellWithHistory(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 2)
|
||||
|
||||
b.Write([]rune("Hello")...)
|
||||
b.CarriageReturn()
|
||||
|
@ -387,7 +406,7 @@ func TestGetCellWithHistory(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGetCellWithBadCursor(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 2)
|
||||
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
|
||||
require.Nil(t, b.GetCell(8, 3))
|
||||
require.Nil(t, b.GetCell(90, 0))
|
||||
|
@ -395,12 +414,12 @@ func TestGetCellWithBadCursor(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCursorAttr(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 2)
|
||||
assert.Equal(t, &b.terminalState.CursorAttr, b.CursorAttr())
|
||||
}
|
||||
|
||||
func TestCursorPositionQuerying(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 20)
|
||||
b.terminalState.cursorX = 17
|
||||
b.terminalState.cursorY = 9
|
||||
assert.Equal(t, b.terminalState.cursorX, b.CursorColumn())
|
||||
|
@ -408,7 +427,7 @@ func TestCursorPositionQuerying(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRawPositionQuerying(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("a")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -445,7 +464,7 @@ func TestRawPositionQuerying(t *testing.T) {
|
|||
|
||||
// CSI 2 K
|
||||
func TestEraseLine(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello, this is a test")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -457,7 +476,7 @@ func TestEraseLine(t *testing.T) {
|
|||
|
||||
// CSI 1 K
|
||||
func TestEraseLineToCursor(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello, this is a test")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -471,7 +490,7 @@ func TestEraseLineToCursor(t *testing.T) {
|
|||
|
||||
// CSI 0 K
|
||||
func TestEraseLineAfterCursor(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello, this is a test")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -482,7 +501,7 @@ func TestEraseLineAfterCursor(t *testing.T) {
|
|||
assert.Equal(t, "dele", b.lines[1].String())
|
||||
}
|
||||
func TestEraseDisplay(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -498,7 +517,7 @@ func TestEraseDisplay(t *testing.T) {
|
|||
}
|
||||
}
|
||||
func TestEraseDisplayToCursor(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -516,7 +535,7 @@ func TestEraseDisplayToCursor(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEraseDisplayFromCursor(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello")...)
|
||||
b.CarriageReturn()
|
||||
b.NewLine()
|
||||
|
@ -532,7 +551,7 @@ func TestEraseDisplayFromCursor(t *testing.T) {
|
|||
assert.Equal(t, "", lines[2].String())
|
||||
}
|
||||
func TestBackspace(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 5)
|
||||
b.Write([]rune("hello")...)
|
||||
b.Backspace()
|
||||
b.Backspace()
|
||||
|
@ -542,7 +561,7 @@ func TestBackspace(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHorizontalResizeView(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
||||
b, _ := makeBufferForTesting(80, 10)
|
||||
|
||||
// 60 characters
|
||||
b.Write([]rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
|
||||
|
@ -626,7 +645,7 @@ dbye
|
|||
*/
|
||||
|
||||
func TestBufferMaxLines(t *testing.T) {
|
||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2))
|
||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2), new(testNotifier))
|
||||
b.terminalState.LineFeedMode = false
|
||||
|
||||
b.Write([]rune("hello")...)
|
||||
|
@ -640,30 +659,20 @@ func TestBufferMaxLines(t *testing.T) {
|
|||
assert.Equal(t, "world", b.lines[1].String())
|
||||
}
|
||||
|
||||
func makeBufferForTestingSelection() *Buffer {
|
||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 10))
|
||||
b.terminalState.LineFeedMode = false
|
||||
|
||||
b.Write([]rune("The quick brown")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("fox jumps over")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("the lazy dog")...)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func TestSelectingChars(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, n := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(2, 0, SelectionChar)
|
||||
n.AssertNotified(t)
|
||||
|
||||
b.ExtendSelection(4, 1, true)
|
||||
n.AssertNotified(t)
|
||||
|
||||
assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText(SelectionRegionNormal))
|
||||
}
|
||||
|
||||
func TestSelectingWordsDown(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, _ := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 1, SelectionWord)
|
||||
b.ExtendSelection(5, 2, true)
|
||||
|
@ -672,7 +681,7 @@ func TestSelectingWordsDown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelectingWordsUp(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, _ := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(5, 2, SelectionWord)
|
||||
b.ExtendSelection(6, 1, true)
|
||||
|
@ -681,7 +690,7 @@ func TestSelectingWordsUp(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelectingLinesDown(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, _ := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 1, SelectionLine)
|
||||
b.ExtendSelection(4, 2, true)
|
||||
|
@ -690,7 +699,7 @@ func TestSelectingLinesDown(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelectingLineUp(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, _ := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(8, 2, SelectionLine)
|
||||
b.ExtendSelection(3, 1, true)
|
||||
|
@ -699,7 +708,7 @@ func TestSelectingLineUp(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelectingAfterText(t *testing.T) {
|
||||
b := makeBufferForTestingSelection()
|
||||
b, _ := makeBufferForTestingSelection()
|
||||
|
||||
b.StartSelection(6, 3, SelectionChar)
|
||||
b.ExtendSelection(6, 3, true)
|
||||
|
@ -711,3 +720,41 @@ func TestSelectingAfterText(t *testing.T) {
|
|||
assert.Equal(t, end.Col, 79)
|
||||
assert.Equal(t, end.Line, 3)
|
||||
}
|
||||
|
||||
func makeBufferForTestingSelection() (*Buffer, *testNotifier) {
|
||||
b, n := makeBufferForTesting(80, 10)
|
||||
b.terminalState.LineFeedMode = false
|
||||
|
||||
b.Write([]rune("The quick brown")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("fox jumps over")...)
|
||||
b.NewLine()
|
||||
b.Write([]rune("the lazy dog")...)
|
||||
|
||||
return b, n
|
||||
}
|
||||
|
||||
func makeBufferForTesting(cols, rows uint16) (*Buffer, *testNotifier) {
|
||||
n := new(testNotifier)
|
||||
b := NewBuffer(NewTerminalState(cols, rows, CellAttributes{}, 100), n)
|
||||
return b, n
|
||||
}
|
||||
|
||||
// testNotifier implements the Notifier interface and provides helpers
|
||||
// for checking it Notify was called (or not).
|
||||
type testNotifier struct {
|
||||
notified bool
|
||||
}
|
||||
|
||||
func (n *testNotifier) Notify() {
|
||||
n.notified = true
|
||||
}
|
||||
|
||||
func (n *testNotifier) AssertNotified(t *testing.T) {
|
||||
assert.True(t, n.notified)
|
||||
n.notified = false
|
||||
}
|
||||
|
||||
func (n *testNotifier) AssertNotNotified(t *testing.T) {
|
||||
assert.False(t, n.notified)
|
||||
}
|
||||
|
|
23
config.go
23
config.go
|
@ -22,12 +22,20 @@ func getActuallyProvidedFlags() map[string]bool {
|
|||
return result
|
||||
}
|
||||
|
||||
func maybeGetConfig(override *config.Config) *config.Config {
|
||||
if override != nil {
|
||||
return override
|
||||
}
|
||||
return getConfig()
|
||||
}
|
||||
|
||||
func getConfig() *config.Config {
|
||||
showVersion := false
|
||||
ignoreConfig := false
|
||||
shell := ""
|
||||
debugMode := false
|
||||
slomo := false
|
||||
cpuProfile := ""
|
||||
|
||||
if flag.Parsed() == false {
|
||||
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.BoolVar(&debugMode, "debug", debugMode, "Enable debug logging")
|
||||
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
|
||||
}
|
||||
|
@ -51,7 +60,7 @@ func getConfig() *config.Config {
|
|||
|
||||
var conf *config.Config
|
||||
if ignoreConfig {
|
||||
conf = &config.DefaultConfig
|
||||
conf = config.DefaultConfig()
|
||||
} else {
|
||||
conf = loadConfigFile()
|
||||
}
|
||||
|
@ -69,6 +78,10 @@ func getConfig() *config.Config {
|
|||
conf.Slomo = slomo
|
||||
}
|
||||
|
||||
if actuallyProvidedFlags["cpuprofile"] {
|
||||
conf.CPUProfile = cpuProfile
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
|
||||
|
@ -77,12 +90,12 @@ func loadConfigFile() *config.Config {
|
|||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get current user information: %s\n", err)
|
||||
return &config.DefaultConfig
|
||||
return config.DefaultConfig()
|
||||
}
|
||||
|
||||
home := usr.HomeDir
|
||||
if home == "" {
|
||||
return &config.DefaultConfig
|
||||
return config.DefaultConfig()
|
||||
}
|
||||
|
||||
places := []string{}
|
||||
|
@ -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)
|
||||
} else {
|
||||
err = os.MkdirAll(filepath.Dir(places[0]), 0744)
|
||||
|
@ -118,5 +131,5 @@ func loadConfigFile() *config.Config {
|
|||
}
|
||||
}
|
||||
|
||||
return &config.DefaultConfig
|
||||
return config.DefaultConfig()
|
||||
}
|
||||
|
|
|
@ -9,4 +9,5 @@ const (
|
|||
ActionReportBug UserAction = "report"
|
||||
ActionToggleDebug UserAction = "debug"
|
||||
ActionToggleSlomo UserAction = "slomo"
|
||||
ActionBufferClear UserAction = "buffer-clear"
|
||||
)
|
||||
|
|
|
@ -7,8 +7,6 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
DebugMode bool `toml:"debug"`
|
||||
Slomo bool `toml:"slomo"`
|
||||
ColourScheme ColourScheme `toml:"colours"`
|
||||
DPIScale float32 `toml:"dpi-scale"`
|
||||
Shell string `toml:"shell"`
|
||||
|
@ -16,17 +14,22 @@ type Config struct {
|
|||
SearchURL string `toml:"search_url"`
|
||||
MaxLines uint64 `toml:"max_lines"`
|
||||
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
|
||||
|
||||
func Parse(data []byte) (*Config, error) {
|
||||
c := DefaultConfig
|
||||
err := toml.Unmarshal(data, &c)
|
||||
c := DefaultConfig()
|
||||
err := toml.Unmarshal(data, c)
|
||||
if c.KeyMapping == nil {
|
||||
c.KeyMapping = KeyMappingConfig(map[string]string{})
|
||||
}
|
||||
return &c, err
|
||||
return c, err
|
||||
}
|
||||
|
||||
func (c *Config) Encode() ([]byte, error) {
|
||||
|
|
|
@ -2,51 +2,50 @@ package config
|
|||
|
||||
import "runtime"
|
||||
|
||||
var DefaultConfig = Config{
|
||||
DebugMode: false,
|
||||
ColourScheme: ColourScheme{
|
||||
Cursor: strToColourNoErr("#e8dfd6"),
|
||||
Foreground: strToColourNoErr("#e8dfd6"),
|
||||
Background: strToColourNoErr("#021b21"),
|
||||
Black: strToColourNoErr("#000000"),
|
||||
Red: strToColourNoErr("#800000"),
|
||||
Green: strToColourNoErr("#008000"),
|
||||
Yellow: strToColourNoErr("#808000"),
|
||||
Blue: strToColourNoErr("#000080"),
|
||||
Magenta: strToColourNoErr("#800080"),
|
||||
Cyan: strToColourNoErr("#008080"),
|
||||
LightGrey: strToColourNoErr("#f2f2f2"),
|
||||
DarkGrey: strToColourNoErr("#808080"),
|
||||
LightRed: strToColourNoErr("#ff0000"),
|
||||
LightGreen: strToColourNoErr("#00ff00"),
|
||||
LightYellow: strToColourNoErr("#ffff00"),
|
||||
LightBlue: strToColourNoErr("#0000ff"),
|
||||
LightMagenta: strToColourNoErr("#ff00ff"),
|
||||
LightCyan: strToColourNoErr("#00ffff"),
|
||||
White: strToColourNoErr("#ffffff"),
|
||||
Selection: strToColourNoErr("#333366"),
|
||||
},
|
||||
KeyMapping: KeyMappingConfig(map[string]string{}),
|
||||
SearchURL: "https://www.google.com/search?q=$QUERY",
|
||||
MaxLines: 1000,
|
||||
CopyAndPasteWithMouse: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
DefaultConfig.KeyMapping[string(ActionCopy)] = addMod("c")
|
||||
DefaultConfig.KeyMapping[string(ActionPaste)] = addMod("v")
|
||||
DefaultConfig.KeyMapping[string(ActionSearch)] = addMod("g")
|
||||
DefaultConfig.KeyMapping[string(ActionToggleDebug)] = addMod("d")
|
||||
DefaultConfig.KeyMapping[string(ActionToggleSlomo)] = addMod(";")
|
||||
DefaultConfig.KeyMapping[string(ActionReportBug)] = addMod("r")
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
DebugMode: false,
|
||||
ColourScheme: ColourScheme{
|
||||
Cursor: strToColourNoErr("#e8dfd6"),
|
||||
Foreground: strToColourNoErr("#e8dfd6"),
|
||||
Background: strToColourNoErr("#021b21"),
|
||||
Black: strToColourNoErr("#000000"),
|
||||
Red: strToColourNoErr("#800000"),
|
||||
Green: strToColourNoErr("#008000"),
|
||||
Yellow: strToColourNoErr("#808000"),
|
||||
Blue: strToColourNoErr("#000080"),
|
||||
Magenta: strToColourNoErr("#800080"),
|
||||
Cyan: strToColourNoErr("#008080"),
|
||||
LightGrey: strToColourNoErr("#f2f2f2"),
|
||||
DarkGrey: strToColourNoErr("#808080"),
|
||||
LightRed: strToColourNoErr("#ff0000"),
|
||||
LightGreen: strToColourNoErr("#00ff00"),
|
||||
LightYellow: strToColourNoErr("#ffff00"),
|
||||
LightBlue: strToColourNoErr("#0000ff"),
|
||||
LightMagenta: strToColourNoErr("#ff00ff"),
|
||||
LightCyan: strToColourNoErr("#00ffff"),
|
||||
White: strToColourNoErr("#ffffff"),
|
||||
Selection: strToColourNoErr("#333366"),
|
||||
},
|
||||
KeyMapping: KeyMappingConfig(map[string]string{
|
||||
string(ActionCopy): addMod("c"),
|
||||
string(ActionPaste): addMod("v"),
|
||||
string(ActionSearch): addMod("g"),
|
||||
string(ActionToggleDebug): addMod("d"),
|
||||
string(ActionToggleSlomo): addMod(";"),
|
||||
string(ActionReportBug): addMod("r"),
|
||||
string(ActionBufferClear): addMod("k"),
|
||||
}),
|
||||
SearchURL: "https://www.google.com/search?q=$QUERY",
|
||||
MaxLines: 1000,
|
||||
CopyAndPasteWithMouse: true,
|
||||
}
|
||||
}
|
||||
|
||||
func addMod(keys string) string {
|
||||
standardMod := "ctrl + shift + "
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
standardMod = "super + "
|
||||
}
|
||||
|
||||
return standardMod + keys
|
||||
}
|
||||
|
|
|
@ -48,17 +48,15 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error)
|
|||
gl.BindVertexArray(f.vao)
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, f.vbo)
|
||||
|
||||
gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.STATIC_DRAW)
|
||||
gl.BufferData(gl.ARRAY_BUFFER, 6*4*4, nil, gl.DYNAMIC_DRAW)
|
||||
|
||||
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
|
||||
gl.EnableVertexAttribArray(vertAttrib)
|
||||
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")))
|
||||
gl.EnableVertexAttribArray(texCoordAttrib)
|
||||
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
|
||||
defer gl.DisableVertexAttribArray(texCoordAttrib)
|
||||
|
||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||
gl.BindVertexArray(0)
|
||||
|
|
|
@ -15,6 +15,7 @@ var actionMap = map[config.UserAction]func(gui *GUI){
|
|||
config.ActionSearch: actionSearchSelection,
|
||||
config.ActionToggleSlomo: actionToggleSlomo,
|
||||
config.ActionReportBug: actionReportBug,
|
||||
config.ActionBufferClear: actionBufferClear,
|
||||
}
|
||||
|
||||
func actionCopy(gui *GUI) {
|
||||
|
@ -33,7 +34,7 @@ func actionPaste(gui *GUI) {
|
|||
|
||||
func actionToggleDebug(gui *GUI) {
|
||||
gui.showDebugInfo = !gui.showDebugInfo
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
|
||||
func actionSearchSelection(gui *GUI) {
|
||||
|
@ -50,3 +51,7 @@ func actionToggleSlomo(gui *GUI) {
|
|||
func actionReportBug(gui *GUI) {
|
||||
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
|
||||
}
|
||||
|
||||
func actionBufferClear(gui *GUI) {
|
||||
gui.terminal.ReallyClear()
|
||||
}
|
||||
|
|
227
gui/gui.go
227
gui/gui.go
|
@ -24,6 +24,12 @@ import (
|
|||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// wakePeriod controls how often the main loop is woken up. This has
|
||||
// significant impact on how Aminal feels to use. Adjust with care and
|
||||
// test changes on all supported platforms.
|
||||
const wakePeriod = time.Second / 120
|
||||
const halfWakePeriod = wakePeriod / 2
|
||||
|
||||
type GUI struct {
|
||||
window *glfw.Window
|
||||
logger *zap.SugaredLogger
|
||||
|
@ -59,6 +65,8 @@ type GUI struct {
|
|||
mouseMovedAfterSelectionStarted bool
|
||||
internalResize bool
|
||||
selectionRegionMode buffer.SelectionRegionMode
|
||||
|
||||
mainThreadFunc chan func()
|
||||
}
|
||||
|
||||
func Min(x, y int) int {
|
||||
|
@ -161,6 +169,8 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
|
|||
keyboardShortcuts: shortcuts,
|
||||
resizeLock: &sync.Mutex{},
|
||||
internalResize: false,
|
||||
|
||||
mainThreadFunc: make(chan func()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -173,15 +183,23 @@ func (gui *GUI) scale() float32 {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
// Order of locking:
|
||||
// 1. resizeLock
|
||||
// 2. terminal's lock
|
||||
gui.resizeLock.Lock()
|
||||
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()
|
||||
if cols == newCols && rows == newRows {
|
||||
return
|
||||
|
@ -244,9 +262,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
|||
return
|
||||
}
|
||||
|
||||
// Order of locking:
|
||||
// 1. resizeLock
|
||||
// 2. terminal's lock
|
||||
terminalAlreadyLocked := false
|
||||
if gui.internalResize == false {
|
||||
gui.resizeLock.Lock()
|
||||
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)
|
||||
|
@ -268,6 +293,11 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
|||
gui.logger.Debugf("Calculating size in cols/rows...")
|
||||
cols, rows := gui.renderer.GetTermSize()
|
||||
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 {
|
||||
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...")
|
||||
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.logger.Debugf("Resize complete!")
|
||||
|
||||
gui.redraw()
|
||||
gui.redraw(!terminalAlreadyLocked)
|
||||
gui.window.SwapBuffers()
|
||||
}
|
||||
|
||||
|
@ -295,6 +330,7 @@ func (gui *GUI) getTermSize() (uint, uint) {
|
|||
|
||||
func (gui *GUI) Close() {
|
||||
gui.window.SetShouldClose(true)
|
||||
glfw.PostEmptyEvent() // wake up main loop so it notices close request
|
||||
}
|
||||
|
||||
func (gui *GUI) Render() error {
|
||||
|
@ -337,11 +373,11 @@ func (gui *GUI) Render() error {
|
|||
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
|
||||
gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
|
||||
gui.window.SetRefreshCallback(func(w *glfw.Window) {
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
})
|
||||
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
|
||||
if focused {
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
})
|
||||
gui.window.SetPosCallback(gui.windowPosChangeCallback)
|
||||
|
@ -354,6 +390,10 @@ func (gui *GUI) Render() error {
|
|||
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...")
|
||||
|
||||
go func() {
|
||||
|
@ -372,10 +412,6 @@ func (gui *GUI) Render() error {
|
|||
gl.Disable(gl.DEPTH_TEST)
|
||||
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)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
@ -394,85 +430,126 @@ func (gui *GUI) Render() error {
|
|||
r, err := version.GetNewerRelease()
|
||||
if err == nil && r != nil {
|
||||
latestVersion = r.TagName
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
}()
|
||||
|
||||
startTime := time.Now()
|
||||
showMessage := true
|
||||
|
||||
stop := make(chan struct{})
|
||||
go gui.waker(stop)
|
||||
|
||||
for !gui.window.ShouldClose() {
|
||||
gui.redraw(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 {
|
||||
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
|
||||
if gui.showDebugInfo {
|
||||
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
|
||||
View Size: %d,%d
|
||||
Buffer Size: %d lines
|
||||
`,
|
||||
gui.terminal.GetLogicalCursorX(),
|
||||
gui.terminal.GetLogicalCursorY(),
|
||||
gui.terminal.ActiveBuffer().ViewWidth(),
|
||||
gui.terminal.ActiveBuffer().ViewHeight(),
|
||||
gui.terminal.ActiveBuffer().Height(),
|
||||
),
|
||||
[3]float32{1, 1, 1},
|
||||
[3]float32{0.8, 0, 0},
|
||||
)
|
||||
}
|
||||
|
||||
if showMessage {
|
||||
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
|
||||
time.AfterFunc(time.Second, gui.terminal.SetDirty)
|
||||
_, h := gui.terminal.GetSize()
|
||||
var msg string
|
||||
if version.Version == "" {
|
||||
msg = "You are using a development build of Aminal."
|
||||
} else {
|
||||
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
|
||||
}
|
||||
gui.textbox(
|
||||
2,
|
||||
uint16(h-3),
|
||||
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
|
||||
[3]float32{1, 1, 1},
|
||||
[3]float32{0, 0.5, 0},
|
||||
)
|
||||
} else {
|
||||
showMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
gui.SwapBuffers()
|
||||
gui.terminal.GetLogicalCursorX(),
|
||||
gui.terminal.GetLogicalCursorY(),
|
||||
gui.terminal.ActiveBuffer().ViewWidth(),
|
||||
gui.terminal.ActiveBuffer().ViewHeight(),
|
||||
gui.terminal.ActiveBuffer().Height(),
|
||||
),
|
||||
[3]float32{1, 1, 1},
|
||||
[3]float32{0.8, 0, 0},
|
||||
)
|
||||
}
|
||||
|
||||
if showMessage {
|
||||
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
|
||||
time.AfterFunc(time.Second, gui.terminal.NotifyDirty)
|
||||
_, h := gui.terminal.GetSize()
|
||||
var msg string
|
||||
if version.Version == "" {
|
||||
msg = "You are using a development build of Aminal."
|
||||
} else {
|
||||
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
|
||||
}
|
||||
gui.textbox(
|
||||
2,
|
||||
uint16(h-3),
|
||||
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
|
||||
[3]float32{1, 1, 1},
|
||||
[3]float32{0, 0.5, 0},
|
||||
)
|
||||
} else {
|
||||
showMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
gui.SwapBuffers()
|
||||
glfw.WaitEvents() // Go to sleep until next event.
|
||||
|
||||
// Process any terminal events since the last wakeup.
|
||||
terminalEvents:
|
||||
for {
|
||||
select {
|
||||
case <-titleChan:
|
||||
gui.window.SetTitle(gui.terminal.GetTitle())
|
||||
case <-resizeChan:
|
||||
gui.resizeToTerminal()
|
||||
case reverse := <-reverseChan:
|
||||
gui.generateDefaultCell(reverse)
|
||||
case funcForMainThread := <-gui.mainThreadFunc:
|
||||
funcForMainThread()
|
||||
default:
|
||||
break terminalEvents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(stop) // Tell waker to end.
|
||||
|
||||
gui.logger.Debugf("Stopping render...")
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (gui *GUI) redraw() {
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||
// waker is a goroutine which listens to the terminal's dirty channel,
|
||||
// waking up the main thread when the GUI needs to be
|
||||
// redrawn. Limiting is applied on wakeups to avoid excessive CPU
|
||||
// usage when the terminal is being updated rapidly.
|
||||
func (gui *GUI) waker(stop <-chan struct{}) {
|
||||
dirty := gui.terminal.Dirty()
|
||||
var nextWake <-chan time.Time
|
||||
var last time.Time
|
||||
for {
|
||||
select {
|
||||
case <-dirty:
|
||||
if nextWake == nil {
|
||||
if time.Since(last) > wakePeriod {
|
||||
// There hasn't been a wakeup recently so schedule
|
||||
// the next one sooner.
|
||||
nextWake = time.After(halfWakePeriod)
|
||||
} else {
|
||||
nextWake = time.After(wakePeriod)
|
||||
}
|
||||
}
|
||||
case last = <-nextWake:
|
||||
// TODO(mjs) - This is somewhat of a voodoo sleep but it
|
||||
// avoid various rendering issues on Windows in some
|
||||
// situations. Suspect that this will become unnecessary
|
||||
// once various goroutine synchronisation issues have been
|
||||
// resolved.
|
||||
time.Sleep(halfWakePeriod)
|
||||
|
||||
glfw.PostEmptyEvent()
|
||||
nextWake = nil
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *GUI) renderTerminalData(shouldLock bool) {
|
||||
if shouldLock {
|
||||
gui.terminal.Lock()
|
||||
defer gui.terminal.Unlock()
|
||||
}
|
||||
lines := gui.terminal.GetVisibleLines()
|
||||
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) {
|
|||
}
|
||||
if gui.selectionRegionMode != mode {
|
||||
gui.selectionRegionMode = mode
|
||||
gui.terminal.SetDirty()
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ type overlay interface {
|
|||
}
|
||||
|
||||
func (gui *GUI) setOverlay(m overlay) {
|
||||
defer gui.terminal.SetDirty()
|
||||
gui.overlay = m
|
||||
gui.terminal.NotifyDirty()
|
||||
}
|
||||
|
||||
func (gui *GUI) renderOverlay() {
|
||||
|
|
|
@ -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
33
main.go
|
@ -2,12 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/liamg/aminal/config"
|
||||
"github.com/liamg/aminal/gui"
|
||||
"github.com/liamg/aminal/platform"
|
||||
"github.com/liamg/aminal/terminal"
|
||||
"github.com/riywo/loginshell"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type callback func(terminal *terminal.Terminal, g *gui.GUI)
|
||||
|
@ -17,11 +21,12 @@ func init() {
|
|||
}
|
||||
|
||||
func main() {
|
||||
initialize(nil)
|
||||
initialize(nil, nil)
|
||||
}
|
||||
|
||||
func initialize(unitTestfunc callback) {
|
||||
conf := getConfig()
|
||||
func initialize(unitTestfunc callback, configOverride *config.Config) {
|
||||
conf := maybeGetConfig(configOverride)
|
||||
|
||||
logger, err := getLogger(conf)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create logger: %s\n", err)
|
||||
|
@ -29,8 +34,13 @@ func initialize(unitTestfunc callback) {
|
|||
}
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to allocate pty: %s", err)
|
||||
|
@ -63,6 +73,8 @@ func initialize(unitTestfunc callback) {
|
|||
logger.Fatalf("Cannot start: %s", err)
|
||||
}
|
||||
|
||||
terminal.WindowManipulation = g
|
||||
|
||||
if unitTestfunc != nil {
|
||||
go unitTestfunc(terminal, g)
|
||||
} else {
|
||||
|
@ -78,3 +90,12 @@ func initialize(unitTestfunc callback) {
|
|||
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
|
||||
}
|
||||
|
|
34
main_test.go
34
main_test.go
|
@ -6,10 +6,12 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/liamg/aminal/config"
|
||||
"github.com/liamg/aminal/gui"
|
||||
"github.com/liamg/aminal/terminal"
|
||||
|
||||
|
@ -105,7 +107,11 @@ func TestCursorMovement(t *testing.T) {
|
|||
send(term, "1\n")
|
||||
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")))
|
||||
}
|
||||
|
||||
|
@ -119,7 +125,7 @@ func TestCursorMovement(t *testing.T) {
|
|||
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-9.png")
|
||||
validateScreen("test-screen-features-10.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-11.png")
|
||||
validateScreen("test-screen-features-12.png")
|
||||
validateScreen("test-screen-features-13.png")
|
||||
validateScreen("test-screen-features-14.png")
|
||||
|
@ -162,7 +161,7 @@ func TestScreenFeatures(t *testing.T) {
|
|||
g.Close()
|
||||
}
|
||||
|
||||
initialize(testFunc)
|
||||
initialize(testFunc, testConfig())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -187,7 +186,7 @@ func TestSixel(t *testing.T) {
|
|||
g.Close()
|
||||
}
|
||||
|
||||
initialize(testFunc)
|
||||
initialize(testFunc, testConfig())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -195,3 +194,14 @@ func TestSixel(t *testing.T) {
|
|||
func TestExit(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@ type Pty interface {
|
|||
Resize(x int, y int) error
|
||||
CreateGuestProcess(imagePath string) (Process, error)
|
||||
GetPlatformDependentSettings() PlatformDependentSettings
|
||||
Clear()
|
||||
}
|
||||
|
|
|
@ -86,6 +86,10 @@ func (pty *unixPty) GetPlatformDependentSettings() PlatformDependentSettings {
|
|||
return pty.platformDependentSettings
|
||||
}
|
||||
|
||||
func (pty *unixPty) Clear() {
|
||||
// do nothing for unix
|
||||
}
|
||||
|
||||
func NewPty(x, y int) (Pty, error) {
|
||||
innerPty, innerTty, err := pty.Open()
|
||||
if err != nil {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/MaxRis/w32"
|
||||
)
|
||||
|
||||
|
@ -98,6 +99,7 @@ type winConPty struct {
|
|||
innerInPipe syscall.Handle
|
||||
innerOutPipe syscall.Handle
|
||||
hcon uintptr
|
||||
processID uint32 // required to obtain old-style console handle (for standard console functions)
|
||||
platformDependentSettings PlatformDependentSettings
|
||||
}
|
||||
|
||||
|
@ -140,6 +142,8 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) {
|
|||
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)
|
||||
if err != nil {
|
||||
process.Close()
|
||||
|
@ -192,6 +196,50 @@ func (pty *winConPty) GetPlatformDependentSettings() 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
|
||||
func NewPty(x, y int) (pty Pty, err error) {
|
||||
if !ptyInitSucceeded {
|
||||
|
|
|
@ -35,26 +35,42 @@ func swallowHandler(n int) func(pty chan rune, terminal *Terminal) error {
|
|||
}
|
||||
|
||||
func risHandler(pty chan rune, terminal *Terminal) error {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.ActiveBuffer().Clear()
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexHandler(pty chan rune, terminal *Terminal) error {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.ActiveBuffer().Index()
|
||||
return nil
|
||||
}
|
||||
|
||||
func reverseIndexHandler(pty chan rune, terminal *Terminal) error {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.ActiveBuffer().ReverseIndex()
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreCursorHandler(pty chan rune, terminal *Terminal) error {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.ActiveBuffer().RestoreCursor()
|
||||
return nil
|
||||
}
|
||||
|
@ -73,11 +89,18 @@ func ansiHandler(pty chan rune, terminal *Terminal) error {
|
|||
}
|
||||
|
||||
func nextLineHandler(pty chan rune, terminal *Terminal) error {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.ActiveBuffer().NewLineEx(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -54,6 +54,10 @@ func scs1Handler(pty chan rune, terminal *Terminal) error {
|
|||
func scsHandler(pty chan rune, terminal *Terminal, which int) error {
|
||||
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]
|
||||
if ok {
|
||||
terminal.logger.Debugf("Selected charset %v into G%v", string(b), which)
|
||||
|
|
|
@ -89,6 +89,9 @@ func splitParams(paramString string) []string {
|
|||
func csiHandler(pty chan rune, terminal *Terminal) error {
|
||||
final, param, intermediate := loadCSI(pty)
|
||||
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
// process intermediate control codes before the CSI
|
||||
for _, b := range intermediate {
|
||||
terminal.processRune(b)
|
||||
|
@ -422,10 +425,6 @@ func csiSetModeHandler(params []string, terminal *Terminal) error {
|
|||
return csiSetModes(params, true, terminal)
|
||||
}
|
||||
|
||||
func csiWindowManipulation(params []string, terminal *Terminal) error {
|
||||
return fmt.Errorf("Window manipulation is not yet supported")
|
||||
}
|
||||
|
||||
func csiLinePositionAbsolute(params []string, terminal *Terminal) error {
|
||||
row := 1
|
||||
if len(params) > 0 {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package terminal
|
||||
|
||||
func newNotifier() *notifier {
|
||||
return ¬ifier{
|
||||
C: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
type notifier struct {
|
||||
C chan struct{}
|
||||
}
|
||||
|
||||
// Notify is used to signal an event in a non-blocking way.
|
||||
func (n *notifier) Notify() {
|
||||
select {
|
||||
case n.C <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
|
@ -28,25 +28,25 @@ var runeMap = map[rune]runeHandler{
|
|||
|
||||
func newLineHandler(terminal *Terminal) error {
|
||||
terminal.ActiveBuffer().NewLine()
|
||||
terminal.isDirty = true
|
||||
terminal.NotifyDirty()
|
||||
return nil
|
||||
}
|
||||
|
||||
func tabHandler(terminal *Terminal) error {
|
||||
terminal.ActiveBuffer().Tab()
|
||||
terminal.isDirty = true
|
||||
terminal.NotifyDirty()
|
||||
return nil
|
||||
}
|
||||
|
||||
func carriageReturnHandler(terminal *Terminal) error {
|
||||
terminal.ActiveBuffer().CarriageReturn()
|
||||
terminal.isDirty = true
|
||||
terminal.NotifyDirty()
|
||||
return nil
|
||||
}
|
||||
|
||||
func backspaceHandler(terminal *Terminal) error {
|
||||
terminal.ActiveBuffer().Backspace()
|
||||
terminal.isDirty = true
|
||||
terminal.NotifyDirty()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -72,17 +72,24 @@ func shiftInHandler(terminal *Terminal) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (terminal *Terminal) processRuneLocked(b rune) {
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
terminal.processRune(b)
|
||||
}
|
||||
|
||||
func (terminal *Terminal) processRune(b rune) {
|
||||
defer terminal.NotifyDirty()
|
||||
|
||||
if handler, ok := runeMap[b]; ok {
|
||||
if err := handler(terminal); err != nil {
|
||||
terminal.logger.Errorf("Error handling control code: %s", err)
|
||||
}
|
||||
terminal.isDirty = true
|
||||
return
|
||||
}
|
||||
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
|
||||
terminal.ActiveBuffer().Write(terminal.translateRune(b))
|
||||
terminal.isDirty = true
|
||||
}
|
||||
|
||||
func (terminal *Terminal) translateRune(b rune) rune {
|
||||
|
@ -103,6 +110,8 @@ func (terminal *Terminal) processInput(pty chan rune) {
|
|||
|
||||
var b rune
|
||||
|
||||
// debug := ""
|
||||
|
||||
for {
|
||||
|
||||
if terminal.config.Slomo {
|
||||
|
@ -111,15 +120,19 @@ func (terminal *Terminal) processInput(pty chan rune) {
|
|||
|
||||
b = <-pty
|
||||
|
||||
// debug += fmt.Sprintf("0x%x ", b)
|
||||
|
||||
if b == 0x1b {
|
||||
// terminal.logger.Debug(debug)
|
||||
// debug = ""
|
||||
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
|
||||
if err := ansiHandler(pty, terminal); err != nil {
|
||||
terminal.logger.Errorf("Error handling escape sequence: %s", err)
|
||||
}
|
||||
terminal.isDirty = true
|
||||
terminal.NotifyDirty()
|
||||
continue
|
||||
}
|
||||
|
||||
terminal.processRune(b)
|
||||
terminal.processRuneLocked(b)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ func screenStateHandler(pty chan rune, terminal *Terminal) error {
|
|||
b := <-pty
|
||||
switch b {
|
||||
case '8': // DECALN -- Screen Alignment Pattern
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
// hide cursor?
|
||||
buffer := terminal.ActiveBuffer()
|
||||
terminal.ResetVerticalMargins()
|
||||
|
|
|
@ -137,6 +137,9 @@ func sixelHandler(pty chan rune, terminal *Terminal) error {
|
|||
defer terminal.SetLineFeedMode()
|
||||
}
|
||||
|
||||
terminal.Lock()
|
||||
defer terminal.Unlock()
|
||||
|
||||
drawSixel(six, terminal)
|
||||
|
||||
return nil
|
||||
|
|
|
@ -33,6 +33,20 @@ const (
|
|||
MouseExtSGR
|
||||
)
|
||||
|
||||
type WindowManipulationInterface interface {
|
||||
RestoreWindow(term *Terminal) error
|
||||
IconifyWindow(term *Terminal) error
|
||||
MoveWindow(term *Terminal, pixelX int, pixelY int) error
|
||||
ResizeWindowByPixels(term *Terminal, pixelsHeight int, pixelsWidth int) error
|
||||
BringWindowToFront(term *Terminal) error
|
||||
ResizeWindowByChars(term *Terminal, charsHeight int, charsWidth int) error
|
||||
MaximizeWindow(term *Terminal) error
|
||||
ReportWindowState(term *Terminal) error
|
||||
ReportWindowPosition(term *Terminal) error
|
||||
ReportWindowSizeInPixels(term *Terminal) error
|
||||
ReportWindowSizeInChars(term *Terminal) error
|
||||
}
|
||||
|
||||
type Terminal struct {
|
||||
program uint32
|
||||
buffers []*buffer.Buffer
|
||||
|
@ -50,12 +64,14 @@ type Terminal struct {
|
|||
mouseMode MouseMode
|
||||
mouseExtMode MouseExtMode
|
||||
bracketedPasteMode bool
|
||||
isDirty bool
|
||||
charWidth float32
|
||||
charHeight float32
|
||||
lastBuffer uint8
|
||||
terminalState *buffer.TerminalState
|
||||
platformDependentSettings platform.PlatformDependentSettings
|
||||
dirty *notifier
|
||||
|
||||
WindowManipulation WindowManipulationInterface
|
||||
}
|
||||
|
||||
type Modes struct {
|
||||
|
@ -85,15 +101,28 @@ func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Te
|
|||
ShowCursor: true,
|
||||
},
|
||||
platformDependentSettings: pty.GetPlatformDependentSettings(),
|
||||
dirty: newNotifier(),
|
||||
}
|
||||
t.buffers = []*buffer.Buffer{
|
||||
buffer.NewBuffer(t.terminalState),
|
||||
buffer.NewBuffer(t.terminalState),
|
||||
buffer.NewBuffer(t.terminalState),
|
||||
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||
}
|
||||
t.activeBuffer = t.buffers[0]
|
||||
return t
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Dirty returns a channel that receives an empty struct whenever the
|
||||
// terminal becomes dirty.
|
||||
func (terminal *Terminal) Dirty() <-chan struct{} {
|
||||
return terminal.dirty.C
|
||||
}
|
||||
|
||||
// NotifyDirty is used to signal that the terminal is dirty and the
|
||||
// screen must be redrawn.
|
||||
func (terminal *Terminal) NotifyDirty() {
|
||||
terminal.dirty.Notify()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) SetProgram(program uint32) {
|
||||
|
@ -104,16 +133,6 @@ func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
|
|||
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 {
|
||||
return terminal.modes.ApplicationCursorKeys
|
||||
}
|
||||
|
@ -172,7 +191,7 @@ func (terminal *Terminal) GetScrollOffset() uint {
|
|||
}
|
||||
|
||||
func (terminal *Terminal) ScreenScrollDown(lines uint16) {
|
||||
defer terminal.SetDirty()
|
||||
defer terminal.NotifyDirty()
|
||||
buffer := terminal.ActiveBuffer()
|
||||
|
||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||
|
@ -200,7 +219,7 @@ func (terminal *Terminal) AreaScrollDown(lines uint16) {
|
|||
}
|
||||
|
||||
func (terminal *Terminal) ScreenScrollUp(lines uint16) {
|
||||
defer terminal.SetDirty()
|
||||
defer terminal.NotifyDirty()
|
||||
buffer := terminal.ActiveBuffer()
|
||||
|
||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||
|
@ -224,8 +243,8 @@ func (terminal *Terminal) ScrollPageUp() {
|
|||
}
|
||||
|
||||
func (terminal *Terminal) ScrollToEnd() {
|
||||
defer terminal.SetDirty()
|
||||
terminal.terminalState.SetScrollOffset(0)
|
||||
terminal.NotifyDirty()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) GetVisibleLines() []buffer.Line {
|
||||
|
@ -299,6 +318,7 @@ func (terminal *Terminal) GetTitle() string {
|
|||
func (terminal *Terminal) SetTitle(title string) {
|
||||
terminal.title = title
|
||||
terminal.emitTitleChange()
|
||||
terminal.NotifyDirty()
|
||||
}
|
||||
|
||||
// Write sends data, i.e. locally typed keystrokes to the pty
|
||||
|
@ -351,14 +371,16 @@ func (terminal *Terminal) Clear() {
|
|||
terminal.ActiveBuffer().Clear()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) ReallyClear() {
|
||||
terminal.pty.Clear()
|
||||
terminal.ActiveBuffer().ReallyClear()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) GetSize() (int, int) {
|
||||
return int(terminal.size.Width), int(terminal.size.Height)
|
||||
}
|
||||
|
||||
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) {
|
||||
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.emitResize()
|
||||
terminal.NotifyDirty()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -420,4 +443,13 @@ func (terminal *Terminal) SetScreenMode(enabled bool) {
|
|||
buffer.ReverseVideo()
|
||||
}
|
||||
terminal.emitReverse(enabled)
|
||||
terminal.NotifyDirty()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) Lock() {
|
||||
terminal.lock.Lock()
|
||||
}
|
||||
|
||||
func (terminal *Terminal) Unlock() {
|
||||
terminal.lock.Unlock()
|
||||
}
|
||||
|
|
|
@ -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 |
95
windows.md
95
windows.md
|
@ -12,8 +12,7 @@
|
|||
cd %YOUR_PROJECT_WORKING_DIR%
|
||||
mkdir go\src\github.com\liamg
|
||||
cd go\src\github.com\liamg
|
||||
git clone git@github.com:jumptrading/aminal-mirror.git
|
||||
move aminal-mirror aminal
|
||||
git clone https://github.com/liamg/aminal.git
|
||||
|
||||
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
|
||||
set GOBIN=%GOPATH%/bin
|
||||
|
@ -28,3 +27,95 @@ go install
|
|||
|
||||
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.
|
|
@ -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
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue