mirror of https://github.com/liamg/aminal.git
Merge branch 'develop' into add-scrollbar
This commit is contained in:
commit
017066f67b
|
@ -1 +1 @@
|
||||||
* @liamg
|
* @liamg @MaxRis
|
||||||
|
|
|
@ -2,3 +2,4 @@ aminal
|
||||||
aminal.exe
|
aminal.exe
|
||||||
*.syso
|
*.syso
|
||||||
.idea
|
.idea
|
||||||
|
generated-src/
|
51
Makefile
51
Makefile
|
@ -1,6 +1,11 @@
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
BINARY := aminal
|
BINARY := aminal
|
||||||
FONTPATH := ./gui/packed-fonts
|
FONTPATH := ./gui/packed-fonts
|
||||||
|
GEN_SRC_DIR := ./generated-src
|
||||||
|
VERSION_MAJOR := 0
|
||||||
|
VERSION_MINOR := 9
|
||||||
|
VERSION_PATCH := 0
|
||||||
|
VERSION := ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build:
|
||||||
|
@ -8,7 +13,7 @@ build:
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test -v ./...
|
go test -v -timeout=8m ./...
|
||||||
go vet -v
|
go vet -v
|
||||||
|
|
||||||
.PHONY: check-gofmt
|
.PHONY: check-gofmt
|
||||||
|
@ -59,6 +64,50 @@ build-windows:
|
||||||
windres -o aminal.syso aminal.rc
|
windres -o aminal.syso aminal.rc
|
||||||
go build -o ${BINARY}-windows-amd64.exe
|
go build -o ${BINARY}-windows-amd64.exe
|
||||||
|
|
||||||
|
.PHONY: launcher-windows
|
||||||
|
launcher-windows: build-windows
|
||||||
|
if exist "${GEN_SRC_DIR}\launcher" rmdir /S /Q "${GEN_SRC_DIR}\launcher"
|
||||||
|
xcopy "windows\launcher\*.*" "${GEN_SRC_DIR}\launcher" /K /H /Y /Q /I
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MAJOR', '${VERSION_MAJOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_MINOR', '${VERSION_MINOR}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION_PATCH', '${VERSION_PATCH}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\launcher\versioninfo.json) -creplace 'YEAR', (Get-Date -UFormat '%Y') | Out-File -Encoding default ${GEN_SRC_DIR}\launcher\versioninfo.json"
|
||||||
|
copy aminal.ico "${GEN_SRC_DIR}\launcher" /Y
|
||||||
|
go generate "${GEN_SRC_DIR}\launcher"
|
||||||
|
if exist "bin\windows\Aminal" rmdir /S /Q "bin\windows\Aminal"
|
||||||
|
mkdir "bin\windows\Aminal\Versions\${VERSION}"
|
||||||
|
go build -o "bin\windows\Aminal\${BINARY}.exe" -ldflags "-H windowsgui" "${GEN_SRC_DIR}\launcher"
|
||||||
|
copy ${BINARY}-windows-amd64.exe "bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe" /Y
|
||||||
|
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\${BINARY}.exe
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\${BINARY}.exe
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\Versions\${VERSION}\${BINARY}.exe
|
||||||
|
|
||||||
|
.PHONY: uninstaller-windows
|
||||||
|
uninstaller-windows: launcher-windows
|
||||||
|
makensis "/XOutFile bin/windows/UninstallerSetup.exe" /NOCD windows\Uninstaller.nsi
|
||||||
|
cmd /c "bin\windows\UninstallerSetup.exe /S /D=%cd%\bin\windows\Aminal"
|
||||||
|
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\Aminal\uninstall.exe
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\Aminal\uninstall.exe
|
||||||
|
|
||||||
|
.PHONY: installer-windows
|
||||||
|
installer-windows: uninstaller-windows
|
||||||
|
if exist "${GEN_SRC_DIR}\installer" rmdir /S /Q "${GEN_SRC_DIR}\installer"
|
||||||
|
xcopy "windows\installer\*.*" "${GEN_SRC_DIR}\installer" /K /H /Y /Q /I
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\installer\installer.go) -creplace 'VERSION', '${VERSION}' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\installer.go"
|
||||||
|
go-bindata -prefix "bin\windows\Aminal" -o "${GEN_SRC_DIR}/installer/data/data.go" "./bin/windows/Aminal/..."
|
||||||
|
powershell -Command "(gc ${GEN_SRC_DIR}\installer\data\data.go) -creplace 'package main', 'package data' | Out-File -Encoding default ${GEN_SRC_DIR}\installer\data\data.go"
|
||||||
|
go build -o bin/windows/AminalSetup.exe -ldflags "-H windowsgui" "${GEN_SRC_DIR}/installer/installer.go"
|
||||||
|
rem If an .exe name contains "installer", "setup" etc., then at least Windows 10 automatically
|
||||||
|
rem opens a UAC prompt upon opening it. To avoid this, we add a compatibility manifest to the .exe.
|
||||||
|
mt -manifest windows\installer\AminalSetup.exe.manifest -outputresource:bin\windows\AminalSetup.exe;1
|
||||||
|
IF "${WINDOWS_CODESIGNING_CERT_PW}"=="" ECHO Environment variable WINDOWS_CODESIGNING_CERT_PW is not defined. & exit 1
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp bin\windows\AminalSetup.exe
|
||||||
|
signtool sign /f windows\codesigning_certificate.pfx /p "${WINDOWS_CODESIGNING_CERT_PW}" /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /as /fd sha256 /td sha256 bin\windows\AminalSetup.exe
|
||||||
|
|
||||||
.PHONY: build-darwin-native-travis
|
.PHONY: build-darwin-native-travis
|
||||||
build-darwin-native-travis:
|
build-darwin-native-travis:
|
||||||
mkdir -p bin/darwin
|
mkdir -p bin/darwin
|
||||||
|
|
|
@ -29,6 +29,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
|
||||||
|
|
||||||
## Checklist:
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] I have run `gofmt` on the project
|
||||||
- [ ] My code follows the style guidelines of this project
|
- [ ] My code follows the style guidelines of this project
|
||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
|
|
@ -21,13 +21,17 @@ const (
|
||||||
SelectionRegionRectangular
|
SelectionRegionRectangular
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Notifier interface {
|
||||||
|
Notify()
|
||||||
|
}
|
||||||
|
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
lines []Line
|
lines []Line
|
||||||
displayChangeHandlers []chan bool
|
displayChangeHandlers []chan bool
|
||||||
savedX uint16
|
savedX uint16
|
||||||
savedY uint16
|
savedY uint16
|
||||||
savedCursorAttr *CellAttributes
|
savedCursorAttr *CellAttributes
|
||||||
dirty bool
|
dirty Notifier
|
||||||
selectionStart *Position
|
selectionStart *Position
|
||||||
selectionEnd *Position
|
selectionEnd *Position
|
||||||
selectionMode SelectionMode
|
selectionMode SelectionMode
|
||||||
|
@ -54,7 +58,7 @@ func comparePositions(pos1 *Position, pos2 *Position) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuffer creates a new terminal buffer
|
// NewBuffer creates a new terminal buffer
|
||||||
func NewBuffer(terminalState *TerminalState) *Buffer {
|
func NewBuffer(terminalState *TerminalState, dirty Notifier) *Buffer {
|
||||||
b := &Buffer{
|
b := &Buffer{
|
||||||
lines: []Line{},
|
lines: []Line{},
|
||||||
selectionStart: nil,
|
selectionStart: nil,
|
||||||
|
@ -62,6 +66,7 @@ func NewBuffer(terminalState *TerminalState) *Buffer {
|
||||||
selectionMode: SelectionChar,
|
selectionMode: SelectionChar,
|
||||||
isSelectionComplete: true,
|
isSelectionComplete: true,
|
||||||
terminalState: terminalState,
|
terminalState: terminalState,
|
||||||
|
dirty: dirty,
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
@ -251,17 +256,15 @@ func (buffer *Buffer) StartSelection(col uint16, viewRow uint16, mode SelectionM
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.isSelectionComplete = false
|
buffer.isSelectionComplete = false
|
||||||
|
buffer.dirty.Notify()
|
||||||
buffer.emitDisplayChange()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
|
func (buffer *Buffer) ExtendSelection(col uint16, viewRow uint16, complete bool) {
|
||||||
|
|
||||||
if buffer.isSelectionComplete {
|
if buffer.isSelectionComplete {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
if buffer.selectionStart == nil {
|
if buffer.selectionStart == nil {
|
||||||
buffer.selectionEnd = nil
|
buffer.selectionEnd = nil
|
||||||
|
@ -285,7 +288,7 @@ func (buffer *Buffer) ClearSelection() {
|
||||||
buffer.selectionEnd = nil
|
buffer.selectionEnd = nil
|
||||||
buffer.isSelectionComplete = true
|
buffer.isSelectionComplete = true
|
||||||
|
|
||||||
buffer.emitDisplayChange()
|
buffer.dirty.Notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) {
|
func (buffer *Buffer) getActualSelection(selectionRegionMode SelectionRegionMode) (*Position, *Position) {
|
||||||
|
@ -368,14 +371,6 @@ func (buffer *Buffer) InSelection(col uint16, row uint16, selectionRegionMode Se
|
||||||
(rawY < end.Line || (rawY == end.Line && int(col) <= end.Col))
|
(rawY < end.Line || (rawY == end.Line && int(col) <= end.Col))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) IsDirty() bool {
|
|
||||||
if !buffer.dirty {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
buffer.dirty = false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (buffer *Buffer) HasScrollableRegion() bool {
|
func (buffer *Buffer) HasScrollableRegion() bool {
|
||||||
return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1
|
return buffer.terminalState.topMargin > 0 || buffer.terminalState.bottomMargin < uint(buffer.ViewHeight())-1
|
||||||
}
|
}
|
||||||
|
@ -395,7 +390,7 @@ func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) AreaScrollDown(lines uint16) {
|
func (buffer *Buffer) AreaScrollDown(lines uint16) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
// NOTE: bottom is exclusive
|
// NOTE: bottom is exclusive
|
||||||
top, bottom := buffer.getAreaScrollRange()
|
top, bottom := buffer.getAreaScrollRange()
|
||||||
|
@ -411,7 +406,7 @@ func (buffer *Buffer) AreaScrollDown(lines uint16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) AreaScrollUp(lines uint16) {
|
func (buffer *Buffer) AreaScrollUp(lines uint16) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
// NOTE: bottom is exclusive
|
// NOTE: bottom is exclusive
|
||||||
top, bottom := buffer.getAreaScrollRange()
|
top, bottom := buffer.getAreaScrollRange()
|
||||||
|
@ -471,10 +466,6 @@ func (buffer *Buffer) GetRawCell(viewCol uint16, rawLine uint64) *Cell {
|
||||||
return &line.cells[viewCol]
|
return &line.cells[viewCol]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) emitDisplayChange() {
|
|
||||||
buffer.dirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column returns cursor column
|
// Column returns cursor column
|
||||||
func (buffer *Buffer) CursorColumn() uint16 {
|
func (buffer *Buffer) CursorColumn() uint16 {
|
||||||
// @todo originMode and left margin
|
// @todo originMode and left margin
|
||||||
|
@ -559,8 +550,7 @@ func (buffer *Buffer) deleteLine() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) insertLine() {
|
func (buffer *Buffer) insertLine() {
|
||||||
|
defer buffer.dirty.Notify()
|
||||||
defer buffer.emitDisplayChange()
|
|
||||||
|
|
||||||
if !buffer.InScrollableRegion() {
|
if !buffer.InScrollableRegion() {
|
||||||
pos := buffer.RawLine()
|
pos := buffer.RawLine()
|
||||||
|
@ -645,7 +635,7 @@ func (buffer *Buffer) Index() {
|
||||||
// This sequence causes the active position to move downward one line without changing the column position.
|
// This sequence causes the active position to move downward one line without changing the column position.
|
||||||
// If the active position is at the bottom margin, a scroll up is performed."
|
// If the active position is at the bottom margin, a scroll up is performed."
|
||||||
|
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
if buffer.InScrollableRegion() {
|
if buffer.InScrollableRegion() {
|
||||||
|
|
||||||
|
@ -671,8 +661,7 @@ func (buffer *Buffer) Index() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) ReverseIndex() {
|
func (buffer *Buffer) ReverseIndex() {
|
||||||
|
defer buffer.dirty.Notify()
|
||||||
defer buffer.emitDisplayChange()
|
|
||||||
|
|
||||||
if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin {
|
if uint(buffer.terminalState.cursorY) == buffer.terminalState.topMargin {
|
||||||
buffer.AreaScrollDown(1)
|
buffer.AreaScrollDown(1)
|
||||||
|
@ -683,7 +672,8 @@ func (buffer *Buffer) ReverseIndex() {
|
||||||
|
|
||||||
// Write will write a rune to the terminal at the position of the cursor, and increment the cursor position
|
// Write will write a rune to the terminal at the position of the cursor, and increment the cursor position
|
||||||
func (buffer *Buffer) Write(runes ...rune) {
|
func (buffer *Buffer) Write(runes ...rune) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
// scroll to bottom on input
|
// scroll to bottom on input
|
||||||
buffer.terminalState.scrollLinesFromBottom = 0
|
buffer.terminalState.scrollLinesFromBottom = 0
|
||||||
|
|
||||||
|
@ -846,7 +836,7 @@ func (buffer *Buffer) MovePosition(x int16, y int16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) SetPosition(col uint16, line uint16) {
|
func (buffer *Buffer) SetPosition(col uint16, line uint16) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
useCol := col
|
useCol := col
|
||||||
useLine := line
|
useLine := line
|
||||||
|
@ -885,13 +875,20 @@ func (buffer *Buffer) GetVisibleLines() []Line {
|
||||||
// tested to here
|
// tested to here
|
||||||
|
|
||||||
func (buffer *Buffer) Clear() {
|
func (buffer *Buffer) Clear() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
for i := 0; i < int(buffer.ViewHeight()); i++ {
|
for i := 0; i < int(buffer.ViewHeight()); i++ {
|
||||||
buffer.lines = append(buffer.lines, newLine())
|
buffer.lines = append(buffer.lines, newLine())
|
||||||
}
|
}
|
||||||
buffer.SetPosition(0, 0) // do we need to set position?
|
buffer.SetPosition(0, 0) // do we need to set position?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (buffer *Buffer) ReallyClear() {
|
||||||
|
defer buffer.dirty.Notify()
|
||||||
|
buffer.lines = []Line{}
|
||||||
|
buffer.terminalState.SetScrollOffset(0)
|
||||||
|
buffer.SetPosition(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
// creates if necessary
|
// creates if necessary
|
||||||
func (buffer *Buffer) getCurrentLine() *Line {
|
func (buffer *Buffer) getCurrentLine() *Line {
|
||||||
return buffer.getViewLine(buffer.terminalState.cursorY)
|
return buffer.getViewLine(buffer.terminalState.cursorY)
|
||||||
|
@ -918,13 +915,13 @@ func (buffer *Buffer) getViewLine(index uint16) *Line {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseLine() {
|
func (buffer *Buffer) EraseLine() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
line.cells = []Cell{}
|
line.cells = []Cell{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseLineToCursor() {
|
func (buffer *Buffer) EraseLineToCursor() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
||||||
if i < len(line.cells) {
|
if i < len(line.cells) {
|
||||||
|
@ -934,7 +931,7 @@ func (buffer *Buffer) EraseLineToCursor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseLineFromCursor() {
|
func (buffer *Buffer) EraseLineFromCursor() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
|
|
||||||
if len(line.cells) > 0 {
|
if len(line.cells) > 0 {
|
||||||
|
@ -952,7 +949,7 @@ func (buffer *Buffer) EraseLineFromCursor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseDisplay() {
|
func (buffer *Buffer) EraseDisplay() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
for i := uint16(0); i < (buffer.ViewHeight()); i++ {
|
for i := uint16(0); i < (buffer.ViewHeight()); i++ {
|
||||||
rawLine := buffer.convertViewLineToRawLine(i)
|
rawLine := buffer.convertViewLineToRawLine(i)
|
||||||
if int(rawLine) < len(buffer.lines) {
|
if int(rawLine) < len(buffer.lines) {
|
||||||
|
@ -962,7 +959,7 @@ func (buffer *Buffer) EraseDisplay() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) DeleteChars(n int) {
|
func (buffer *Buffer) DeleteChars(n int) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
if int(buffer.terminalState.cursorX) >= len(line.cells) {
|
if int(buffer.terminalState.cursorX) >= len(line.cells) {
|
||||||
|
@ -977,7 +974,7 @@ func (buffer *Buffer) DeleteChars(n int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseCharacters(n int) {
|
func (buffer *Buffer) EraseCharacters(n int) {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
|
|
||||||
|
@ -992,7 +989,7 @@ func (buffer *Buffer) EraseCharacters(n int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseDisplayFromCursor() {
|
func (buffer *Buffer) EraseDisplayFromCursor() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
|
|
||||||
max := int(buffer.terminalState.cursorX)
|
max := int(buffer.terminalState.cursorX)
|
||||||
|
@ -1008,7 +1005,7 @@ func (buffer *Buffer) EraseDisplayFromCursor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) EraseDisplayToCursor() {
|
func (buffer *Buffer) EraseDisplayToCursor() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
line := buffer.getCurrentLine()
|
line := buffer.getCurrentLine()
|
||||||
|
|
||||||
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
for i := 0; i <= int(buffer.terminalState.cursorX); i++ {
|
||||||
|
@ -1026,8 +1023,7 @@ func (buffer *Buffer) EraseDisplayToCursor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) ResizeView(width uint16, height uint16) {
|
func (buffer *Buffer) ResizeView(width uint16, height uint16) {
|
||||||
|
defer buffer.dirty.Notify()
|
||||||
defer buffer.emitDisplayChange()
|
|
||||||
|
|
||||||
if buffer.terminalState.viewHeight == 0 {
|
if buffer.terminalState.viewHeight == 0 {
|
||||||
buffer.terminalState.viewWidth = width
|
buffer.terminalState.viewWidth = width
|
||||||
|
@ -1164,7 +1160,7 @@ func (buffer *Buffer) CompareViewLines(path string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (buffer *Buffer) ReverseVideo() {
|
func (buffer *Buffer) ReverseVideo() {
|
||||||
defer buffer.emitDisplayChange()
|
defer buffer.dirty.Notify()
|
||||||
|
|
||||||
for _, line := range buffer.lines {
|
for _, line := range buffer.lines {
|
||||||
line.ReverseVideo()
|
line.ReverseVideo()
|
||||||
|
|
|
@ -9,8 +9,24 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestBufferCreation(t *testing.T) {
|
||||||
|
b, n := makeBufferForTesting(10, 20)
|
||||||
|
assert.Equal(t, uint16(10), b.Width())
|
||||||
|
assert.Equal(t, uint16(20), b.ViewHeight())
|
||||||
|
assert.Equal(t, uint16(0), b.CursorColumn())
|
||||||
|
assert.Equal(t, uint16(0), b.CursorLine())
|
||||||
|
assert.NotNil(t, b.lines)
|
||||||
|
n.AssertNotNotified(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteNotify(t *testing.T) {
|
||||||
|
b, n := makeBufferForTesting(30, 3)
|
||||||
|
b.Write(rune('x'))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestTabbing(t *testing.T) {
|
func TestTabbing(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(30, 3, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(30, 3)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.Tab()
|
b.Tab()
|
||||||
b.Write([]rune("x")...)
|
b.Write([]rune("x")...)
|
||||||
|
@ -39,7 +55,7 @@ hell xxx good
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOffsets(t *testing.T) {
|
func TestOffsets(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(10, 3, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(10, 3)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -59,18 +75,8 @@ func TestOffsets(t *testing.T) {
|
||||||
assert.Equal(t, 5, b.Height())
|
assert.Equal(t, 5, b.Height())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBufferCreation(t *testing.T) {
|
|
||||||
b := NewBuffer(NewTerminalState(10, 20, CellAttributes{}, 1000))
|
|
||||||
assert.Equal(t, uint16(10), b.Width())
|
|
||||||
assert.Equal(t, uint16(20), b.ViewHeight())
|
|
||||||
assert.Equal(t, uint16(0), b.CursorColumn())
|
|
||||||
assert.Equal(t, uint16(0), b.CursorLine())
|
|
||||||
assert.NotNil(t, b.lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
|
func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
|
||||||
|
b, _ := makeBufferForTesting(5, 4)
|
||||||
b := NewBuffer(NewTerminalState(5, 4, CellAttributes{}, 1000))
|
|
||||||
|
|
||||||
/*01234
|
/*01234
|
||||||
|-----
|
|-----
|
||||||
|
@ -117,7 +123,7 @@ func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
|
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(3, 20)
|
||||||
b.terminalState.LineFeedMode = false
|
b.terminalState.LineFeedMode = false
|
||||||
|
|
||||||
b.Write('a', 'b', 'c')
|
b.Write('a', 'b', 'c')
|
||||||
|
@ -142,7 +148,7 @@ func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
|
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(3, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(3, 20)
|
||||||
b.terminalState.LineFeedMode = false
|
b.terminalState.LineFeedMode = false
|
||||||
/*
|
/*
|
||||||
|abc
|
|abc
|
||||||
|
@ -170,46 +176,59 @@ func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetPosition(t *testing.T) {
|
func TestSetPosition(t *testing.T) {
|
||||||
|
b, n := makeBufferForTesting(120, 80)
|
||||||
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
|
|
||||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 0, int(b.CursorLine()))
|
assert.Equal(t, 0, int(b.CursorLine()))
|
||||||
|
|
||||||
b.SetPosition(60, 10)
|
b.SetPosition(60, 10)
|
||||||
assert.Equal(t, 60, int(b.CursorColumn()))
|
assert.Equal(t, 60, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 10, int(b.CursorLine()))
|
assert.Equal(t, 10, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.SetPosition(0, 0)
|
b.SetPosition(0, 0)
|
||||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 0, int(b.CursorLine()))
|
assert.Equal(t, 0, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.SetPosition(120, 90)
|
b.SetPosition(120, 90)
|
||||||
assert.Equal(t, 119, int(b.CursorColumn()))
|
assert.Equal(t, 119, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 79, int(b.CursorLine()))
|
assert.Equal(t, 79, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMovePosition(t *testing.T) {
|
func TestMovePosition(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(120, 80, CellAttributes{}, 1000))
|
b, n := makeBufferForTesting(120, 80)
|
||||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 0, int(b.CursorLine()))
|
assert.Equal(t, 0, int(b.CursorLine()))
|
||||||
|
|
||||||
b.MovePosition(-1, -1)
|
b.MovePosition(-1, -1)
|
||||||
assert.Equal(t, 0, int(b.CursorColumn()))
|
assert.Equal(t, 0, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 0, int(b.CursorLine()))
|
assert.Equal(t, 0, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.MovePosition(30, 20)
|
b.MovePosition(30, 20)
|
||||||
assert.Equal(t, 30, int(b.CursorColumn()))
|
assert.Equal(t, 30, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 20, int(b.CursorLine()))
|
assert.Equal(t, 20, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.MovePosition(30, 20)
|
b.MovePosition(30, 20)
|
||||||
assert.Equal(t, 60, int(b.CursorColumn()))
|
assert.Equal(t, 60, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 40, int(b.CursorLine()))
|
assert.Equal(t, 40, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.MovePosition(-1, -1)
|
b.MovePosition(-1, -1)
|
||||||
assert.Equal(t, 59, int(b.CursorColumn()))
|
assert.Equal(t, 59, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 39, int(b.CursorLine()))
|
assert.Equal(t, 39, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.MovePosition(100, 100)
|
b.MovePosition(100, 100)
|
||||||
assert.Equal(t, 119, int(b.CursorColumn()))
|
assert.Equal(t, 119, int(b.CursorColumn()))
|
||||||
assert.Equal(t, 79, int(b.CursorLine()))
|
assert.Equal(t, 79, int(b.CursorLine()))
|
||||||
|
n.AssertNotified(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVisibleLines(t *testing.T) {
|
func TestVisibleLines(t *testing.T) {
|
||||||
|
b, _ := makeBufferForTesting(80, 10)
|
||||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
|
||||||
b.Write([]rune("hello 1")...)
|
b.Write([]rune("hello 1")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -259,7 +278,7 @@ func TestVisibleLines(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClearWithoutFullView(t *testing.T) {
|
func TestClearWithoutFullView(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 10)
|
||||||
b.Write([]rune("hello 1")...)
|
b.Write([]rune("hello 1")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -275,7 +294,7 @@ func TestClearWithoutFullView(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClearWithFullView(t *testing.T) {
|
func TestClearWithFullView(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello 1")...)
|
b.Write([]rune("hello 1")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -306,7 +325,7 @@ func TestClearWithFullView(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCarriageReturn(t *testing.T) {
|
func TestCarriageReturn(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 20)
|
||||||
b.Write([]rune("hello!")...)
|
b.Write([]rune("hello!")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.Write([]rune("secret")...)
|
b.Write([]rune("secret")...)
|
||||||
|
@ -315,7 +334,7 @@ func TestCarriageReturn(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCarriageReturnOnFullLine(t *testing.T) {
|
func TestCarriageReturnOnFullLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(20, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(20, 20)
|
||||||
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.Write([]rune("xxxxxxxxxxxxxxxxxxxx")...)
|
b.Write([]rune("xxxxxxxxxxxxxxxxxxxx")...)
|
||||||
|
@ -324,7 +343,7 @@ func TestCarriageReturnOnFullLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCarriageReturnOnFullLastLine(t *testing.T) {
|
func TestCarriageReturnOnFullLastLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(20, 2, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(20, 2)
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
b.Write([]rune("abcdeabcdeabcdeabcde")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
|
@ -335,7 +354,7 @@ func TestCarriageReturnOnFullLastLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCarriageReturnOnWrappedLine(t *testing.T) {
|
func TestCarriageReturnOnWrappedLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 6, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 6)
|
||||||
b.Write([]rune("hello!")...)
|
b.Write([]rune("hello!")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.Write([]rune("secret")...)
|
b.Write([]rune("secret")...)
|
||||||
|
@ -345,7 +364,7 @@ func TestCarriageReturnOnWrappedLine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
|
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(6, 10, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(6, 10)
|
||||||
b.terminalState.cursorY = 3
|
b.terminalState.cursorY = 3
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
assert.Equal(t, uint16(0), b.terminalState.cursorX)
|
assert.Equal(t, uint16(0), b.terminalState.cursorX)
|
||||||
|
@ -353,7 +372,7 @@ func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCell(t *testing.T) {
|
func TestGetCell(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 20)
|
||||||
b.Write([]rune("Hello")...)
|
b.Write([]rune("Hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -369,7 +388,7 @@ func TestGetCell(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCellWithHistory(t *testing.T) {
|
func TestGetCellWithHistory(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 2)
|
||||||
|
|
||||||
b.Write([]rune("Hello")...)
|
b.Write([]rune("Hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
|
@ -387,7 +406,7 @@ func TestGetCellWithHistory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCellWithBadCursor(t *testing.T) {
|
func TestGetCellWithBadCursor(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 2)
|
||||||
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
|
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
|
||||||
require.Nil(t, b.GetCell(8, 3))
|
require.Nil(t, b.GetCell(8, 3))
|
||||||
require.Nil(t, b.GetCell(90, 0))
|
require.Nil(t, b.GetCell(90, 0))
|
||||||
|
@ -395,12 +414,12 @@ func TestGetCellWithBadCursor(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCursorAttr(t *testing.T) {
|
func TestCursorAttr(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 2)
|
||||||
assert.Equal(t, &b.terminalState.CursorAttr, b.CursorAttr())
|
assert.Equal(t, &b.terminalState.CursorAttr, b.CursorAttr())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCursorPositionQuerying(t *testing.T) {
|
func TestCursorPositionQuerying(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 20, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 20)
|
||||||
b.terminalState.cursorX = 17
|
b.terminalState.cursorX = 17
|
||||||
b.terminalState.cursorY = 9
|
b.terminalState.cursorY = 9
|
||||||
assert.Equal(t, b.terminalState.cursorX, b.CursorColumn())
|
assert.Equal(t, b.terminalState.cursorX, b.CursorColumn())
|
||||||
|
@ -408,7 +427,7 @@ func TestCursorPositionQuerying(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRawPositionQuerying(t *testing.T) {
|
func TestRawPositionQuerying(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("a")...)
|
b.Write([]rune("a")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -445,7 +464,7 @@ func TestRawPositionQuerying(t *testing.T) {
|
||||||
|
|
||||||
// CSI 2 K
|
// CSI 2 K
|
||||||
func TestEraseLine(t *testing.T) {
|
func TestEraseLine(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello, this is a test")...)
|
b.Write([]rune("hello, this is a test")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -457,7 +476,7 @@ func TestEraseLine(t *testing.T) {
|
||||||
|
|
||||||
// CSI 1 K
|
// CSI 1 K
|
||||||
func TestEraseLineToCursor(t *testing.T) {
|
func TestEraseLineToCursor(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello, this is a test")...)
|
b.Write([]rune("hello, this is a test")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -471,7 +490,7 @@ func TestEraseLineToCursor(t *testing.T) {
|
||||||
|
|
||||||
// CSI 0 K
|
// CSI 0 K
|
||||||
func TestEraseLineAfterCursor(t *testing.T) {
|
func TestEraseLineAfterCursor(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello, this is a test")...)
|
b.Write([]rune("hello, this is a test")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -482,7 +501,7 @@ func TestEraseLineAfterCursor(t *testing.T) {
|
||||||
assert.Equal(t, "dele", b.lines[1].String())
|
assert.Equal(t, "dele", b.lines[1].String())
|
||||||
}
|
}
|
||||||
func TestEraseDisplay(t *testing.T) {
|
func TestEraseDisplay(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -498,7 +517,7 @@ func TestEraseDisplay(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestEraseDisplayToCursor(t *testing.T) {
|
func TestEraseDisplayToCursor(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -516,7 +535,7 @@ func TestEraseDisplayToCursor(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEraseDisplayFromCursor(t *testing.T) {
|
func TestEraseDisplayFromCursor(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.CarriageReturn()
|
b.CarriageReturn()
|
||||||
b.NewLine()
|
b.NewLine()
|
||||||
|
@ -532,7 +551,7 @@ func TestEraseDisplayFromCursor(t *testing.T) {
|
||||||
assert.Equal(t, "", lines[2].String())
|
assert.Equal(t, "", lines[2].String())
|
||||||
}
|
}
|
||||||
func TestBackspace(t *testing.T) {
|
func TestBackspace(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 5, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 5)
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
b.Backspace()
|
b.Backspace()
|
||||||
b.Backspace()
|
b.Backspace()
|
||||||
|
@ -542,7 +561,7 @@ func TestBackspace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHorizontalResizeView(t *testing.T) {
|
func TestHorizontalResizeView(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 1000))
|
b, _ := makeBufferForTesting(80, 10)
|
||||||
|
|
||||||
// 60 characters
|
// 60 characters
|
||||||
b.Write([]rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
|
b.Write([]rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
|
||||||
|
@ -626,7 +645,7 @@ dbye
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func TestBufferMaxLines(t *testing.T) {
|
func TestBufferMaxLines(t *testing.T) {
|
||||||
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2))
|
b := NewBuffer(NewTerminalState(80, 2, CellAttributes{}, 2), new(testNotifier))
|
||||||
b.terminalState.LineFeedMode = false
|
b.terminalState.LineFeedMode = false
|
||||||
|
|
||||||
b.Write([]rune("hello")...)
|
b.Write([]rune("hello")...)
|
||||||
|
@ -640,30 +659,20 @@ func TestBufferMaxLines(t *testing.T) {
|
||||||
assert.Equal(t, "world", b.lines[1].String())
|
assert.Equal(t, "world", b.lines[1].String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBufferForTestingSelection() *Buffer {
|
|
||||||
b := NewBuffer(NewTerminalState(80, 10, CellAttributes{}, 10))
|
|
||||||
b.terminalState.LineFeedMode = false
|
|
||||||
|
|
||||||
b.Write([]rune("The quick brown")...)
|
|
||||||
b.NewLine()
|
|
||||||
b.Write([]rune("fox jumps over")...)
|
|
||||||
b.NewLine()
|
|
||||||
b.Write([]rune("the lazy dog")...)
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSelectingChars(t *testing.T) {
|
func TestSelectingChars(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, n := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(2, 0, SelectionChar)
|
b.StartSelection(2, 0, SelectionChar)
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
b.ExtendSelection(4, 1, true)
|
b.ExtendSelection(4, 1, true)
|
||||||
|
n.AssertNotified(t)
|
||||||
|
|
||||||
assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText(SelectionRegionNormal))
|
assert.Equal(t, "e quick brown\nfox j", b.GetSelectedText(SelectionRegionNormal))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectingWordsDown(t *testing.T) {
|
func TestSelectingWordsDown(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, _ := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(6, 1, SelectionWord)
|
b.StartSelection(6, 1, SelectionWord)
|
||||||
b.ExtendSelection(5, 2, true)
|
b.ExtendSelection(5, 2, true)
|
||||||
|
@ -672,7 +681,7 @@ func TestSelectingWordsDown(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectingWordsUp(t *testing.T) {
|
func TestSelectingWordsUp(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, _ := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(5, 2, SelectionWord)
|
b.StartSelection(5, 2, SelectionWord)
|
||||||
b.ExtendSelection(6, 1, true)
|
b.ExtendSelection(6, 1, true)
|
||||||
|
@ -681,7 +690,7 @@ func TestSelectingWordsUp(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectingLinesDown(t *testing.T) {
|
func TestSelectingLinesDown(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, _ := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(6, 1, SelectionLine)
|
b.StartSelection(6, 1, SelectionLine)
|
||||||
b.ExtendSelection(4, 2, true)
|
b.ExtendSelection(4, 2, true)
|
||||||
|
@ -690,7 +699,7 @@ func TestSelectingLinesDown(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectingLineUp(t *testing.T) {
|
func TestSelectingLineUp(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, _ := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(8, 2, SelectionLine)
|
b.StartSelection(8, 2, SelectionLine)
|
||||||
b.ExtendSelection(3, 1, true)
|
b.ExtendSelection(3, 1, true)
|
||||||
|
@ -699,7 +708,7 @@ func TestSelectingLineUp(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectingAfterText(t *testing.T) {
|
func TestSelectingAfterText(t *testing.T) {
|
||||||
b := makeBufferForTestingSelection()
|
b, _ := makeBufferForTestingSelection()
|
||||||
|
|
||||||
b.StartSelection(6, 3, SelectionChar)
|
b.StartSelection(6, 3, SelectionChar)
|
||||||
b.ExtendSelection(6, 3, true)
|
b.ExtendSelection(6, 3, true)
|
||||||
|
@ -711,3 +720,41 @@ func TestSelectingAfterText(t *testing.T) {
|
||||||
assert.Equal(t, end.Col, 79)
|
assert.Equal(t, end.Col, 79)
|
||||||
assert.Equal(t, end.Line, 3)
|
assert.Equal(t, end.Line, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeBufferForTestingSelection() (*Buffer, *testNotifier) {
|
||||||
|
b, n := makeBufferForTesting(80, 10)
|
||||||
|
b.terminalState.LineFeedMode = false
|
||||||
|
|
||||||
|
b.Write([]rune("The quick brown")...)
|
||||||
|
b.NewLine()
|
||||||
|
b.Write([]rune("fox jumps over")...)
|
||||||
|
b.NewLine()
|
||||||
|
b.Write([]rune("the lazy dog")...)
|
||||||
|
|
||||||
|
return b, n
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBufferForTesting(cols, rows uint16) (*Buffer, *testNotifier) {
|
||||||
|
n := new(testNotifier)
|
||||||
|
b := NewBuffer(NewTerminalState(cols, rows, CellAttributes{}, 100), n)
|
||||||
|
return b, n
|
||||||
|
}
|
||||||
|
|
||||||
|
// testNotifier implements the Notifier interface and provides helpers
|
||||||
|
// for checking it Notify was called (or not).
|
||||||
|
type testNotifier struct {
|
||||||
|
notified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNotifier) Notify() {
|
||||||
|
n.notified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNotifier) AssertNotified(t *testing.T) {
|
||||||
|
assert.True(t, n.notified)
|
||||||
|
n.notified = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *testNotifier) AssertNotNotified(t *testing.T) {
|
||||||
|
assert.False(t, n.notified)
|
||||||
|
}
|
||||||
|
|
23
config.go
23
config.go
|
@ -22,12 +22,20 @@ func getActuallyProvidedFlags() map[string]bool {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maybeGetConfig(override *config.Config) *config.Config {
|
||||||
|
if override != nil {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return getConfig()
|
||||||
|
}
|
||||||
|
|
||||||
func getConfig() *config.Config {
|
func getConfig() *config.Config {
|
||||||
showVersion := false
|
showVersion := false
|
||||||
ignoreConfig := false
|
ignoreConfig := false
|
||||||
shell := ""
|
shell := ""
|
||||||
debugMode := false
|
debugMode := false
|
||||||
slomo := false
|
slomo := false
|
||||||
|
cpuProfile := ""
|
||||||
|
|
||||||
if flag.Parsed() == false {
|
if flag.Parsed() == false {
|
||||||
flag.BoolVar(&showVersion, "version", showVersion, "Output version information")
|
flag.BoolVar(&showVersion, "version", showVersion, "Output version information")
|
||||||
|
@ -35,6 +43,7 @@ func getConfig() *config.Config {
|
||||||
flag.StringVar(&shell, "shell", shell, "Specify the shell to use")
|
flag.StringVar(&shell, "shell", shell, "Specify the shell to use")
|
||||||
flag.BoolVar(&debugMode, "debug", debugMode, "Enable debug logging")
|
flag.BoolVar(&debugMode, "debug", debugMode, "Enable debug logging")
|
||||||
flag.BoolVar(&slomo, "slomo", slomo, "Render in slow motion (useful for debugging)")
|
flag.BoolVar(&slomo, "slomo", slomo, "Render in slow motion (useful for debugging)")
|
||||||
|
flag.StringVar(&cpuProfile, "cpuprofile", cpuProfile, "Write a CPU profile to this file")
|
||||||
|
|
||||||
flag.Parse() // actual parsing and fetching flags from the command line
|
flag.Parse() // actual parsing and fetching flags from the command line
|
||||||
}
|
}
|
||||||
|
@ -51,7 +60,7 @@ func getConfig() *config.Config {
|
||||||
|
|
||||||
var conf *config.Config
|
var conf *config.Config
|
||||||
if ignoreConfig {
|
if ignoreConfig {
|
||||||
conf = &config.DefaultConfig
|
conf = config.DefaultConfig()
|
||||||
} else {
|
} else {
|
||||||
conf = loadConfigFile()
|
conf = loadConfigFile()
|
||||||
}
|
}
|
||||||
|
@ -69,6 +78,10 @@ func getConfig() *config.Config {
|
||||||
conf.Slomo = slomo
|
conf.Slomo = slomo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if actuallyProvidedFlags["cpuprofile"] {
|
||||||
|
conf.CPUProfile = cpuProfile
|
||||||
|
}
|
||||||
|
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,12 +90,12 @@ func loadConfigFile() *config.Config {
|
||||||
usr, err := user.Current()
|
usr, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to get current user information: %s\n", err)
|
fmt.Printf("Failed to get current user information: %s\n", err)
|
||||||
return &config.DefaultConfig
|
return config.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
home := usr.HomeDir
|
home := usr.HomeDir
|
||||||
if home == "" {
|
if home == "" {
|
||||||
return &config.DefaultConfig
|
return config.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
places := []string{}
|
places := []string{}
|
||||||
|
@ -105,7 +118,7 @@ func loadConfigFile() *config.Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b, err := config.DefaultConfig.Encode(); err != nil {
|
if b, err := config.DefaultConfig().Encode(); err != nil {
|
||||||
fmt.Printf("Failed to encode config file: %s\n", err)
|
fmt.Printf("Failed to encode config file: %s\n", err)
|
||||||
} else {
|
} else {
|
||||||
err = os.MkdirAll(filepath.Dir(places[0]), 0744)
|
err = os.MkdirAll(filepath.Dir(places[0]), 0744)
|
||||||
|
@ -118,5 +131,5 @@ func loadConfigFile() *config.Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &config.DefaultConfig
|
return config.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ const (
|
||||||
ActionReportBug UserAction = "report"
|
ActionReportBug UserAction = "report"
|
||||||
ActionToggleDebug UserAction = "debug"
|
ActionToggleDebug UserAction = "debug"
|
||||||
ActionToggleSlomo UserAction = "slomo"
|
ActionToggleSlomo UserAction = "slomo"
|
||||||
|
ActionBufferClear UserAction = "buffer-clear"
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DebugMode bool `toml:"debug"`
|
|
||||||
Slomo bool `toml:"slomo"`
|
|
||||||
ColourScheme ColourScheme `toml:"colours"`
|
ColourScheme ColourScheme `toml:"colours"`
|
||||||
DPIScale float32 `toml:"dpi-scale"`
|
DPIScale float32 `toml:"dpi-scale"`
|
||||||
Shell string `toml:"shell"`
|
Shell string `toml:"shell"`
|
||||||
|
@ -17,17 +15,22 @@ type Config struct {
|
||||||
MaxLines uint64 `toml:"max_lines"`
|
MaxLines uint64 `toml:"max_lines"`
|
||||||
CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"`
|
CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"`
|
||||||
ShowVerticalScrollbar bool `toml:"show_vertical_scrollbar"`
|
ShowVerticalScrollbar bool `toml:"show_vertical_scrollbar"`
|
||||||
|
|
||||||
|
// Developer options.
|
||||||
|
DebugMode bool `toml:"debug"`
|
||||||
|
Slomo bool `toml:"slomo"`
|
||||||
|
CPUProfile string `toml:"cpu_profile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyMappingConfig map[string]string
|
type KeyMappingConfig map[string]string
|
||||||
|
|
||||||
func Parse(data []byte) (*Config, error) {
|
func Parse(data []byte) (*Config, error) {
|
||||||
c := DefaultConfig
|
c := DefaultConfig()
|
||||||
err := toml.Unmarshal(data, &c)
|
err := toml.Unmarshal(data, c)
|
||||||
if c.KeyMapping == nil {
|
if c.KeyMapping == nil {
|
||||||
c.KeyMapping = KeyMappingConfig(map[string]string{})
|
c.KeyMapping = KeyMappingConfig(map[string]string{})
|
||||||
}
|
}
|
||||||
return &c, err
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Encode() ([]byte, error) {
|
func (c *Config) Encode() ([]byte, error) {
|
||||||
|
|
|
@ -2,7 +2,8 @@ package config
|
||||||
|
|
||||||
import "runtime"
|
import "runtime"
|
||||||
|
|
||||||
var DefaultConfig = Config{
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
DebugMode: false,
|
DebugMode: false,
|
||||||
ColourScheme: ColourScheme{
|
ColourScheme: ColourScheme{
|
||||||
Cursor: strToColourNoErr("#e8dfd6"),
|
Cursor: strToColourNoErr("#e8dfd6"),
|
||||||
|
@ -26,28 +27,26 @@ var DefaultConfig = Config{
|
||||||
White: strToColourNoErr("#ffffff"),
|
White: strToColourNoErr("#ffffff"),
|
||||||
Selection: strToColourNoErr("#333366"),
|
Selection: strToColourNoErr("#333366"),
|
||||||
},
|
},
|
||||||
KeyMapping: KeyMappingConfig(map[string]string{}),
|
KeyMapping: KeyMappingConfig(map[string]string{
|
||||||
|
string(ActionCopy): addMod("c"),
|
||||||
|
string(ActionPaste): addMod("v"),
|
||||||
|
string(ActionSearch): addMod("g"),
|
||||||
|
string(ActionToggleDebug): addMod("d"),
|
||||||
|
string(ActionToggleSlomo): addMod(";"),
|
||||||
|
string(ActionReportBug): addMod("r"),
|
||||||
|
string(ActionBufferClear): addMod("k"),
|
||||||
|
}),
|
||||||
SearchURL: "https://www.google.com/search?q=$QUERY",
|
SearchURL: "https://www.google.com/search?q=$QUERY",
|
||||||
MaxLines: 1000,
|
MaxLines: 1000,
|
||||||
CopyAndPasteWithMouse: true,
|
CopyAndPasteWithMouse: true,
|
||||||
ShowVerticalScrollbar: true,
|
ShowVerticalScrollbar: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
DefaultConfig.KeyMapping[string(ActionCopy)] = addMod("c")
|
|
||||||
DefaultConfig.KeyMapping[string(ActionPaste)] = addMod("v")
|
|
||||||
DefaultConfig.KeyMapping[string(ActionSearch)] = addMod("g")
|
|
||||||
DefaultConfig.KeyMapping[string(ActionToggleDebug)] = addMod("d")
|
|
||||||
DefaultConfig.KeyMapping[string(ActionToggleSlomo)] = addMod(";")
|
|
||||||
DefaultConfig.KeyMapping[string(ActionReportBug)] = addMod("r")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addMod(keys string) string {
|
func addMod(keys string) string {
|
||||||
standardMod := "ctrl + shift + "
|
standardMod := "ctrl + shift + "
|
||||||
|
|
||||||
if runtime.GOOS == "darwin" {
|
if runtime.GOOS == "darwin" {
|
||||||
standardMod = "super + "
|
standardMod = "super + "
|
||||||
}
|
}
|
||||||
|
|
||||||
return standardMod + keys
|
return standardMod + keys
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,12 +53,10 @@ func LoadTrueTypeFont(program uint32, r io.Reader, scale float32) (*Font, error)
|
||||||
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
|
vertAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vert\x00")))
|
||||||
gl.EnableVertexAttribArray(vertAttrib)
|
gl.EnableVertexAttribArray(vertAttrib)
|
||||||
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
|
gl.VertexAttribPointer(vertAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(0))
|
||||||
defer gl.DisableVertexAttribArray(vertAttrib)
|
|
||||||
|
|
||||||
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
|
texCoordAttrib := uint32(gl.GetAttribLocation(f.program, gl.Str("vertTexCoord\x00")))
|
||||||
gl.EnableVertexAttribArray(texCoordAttrib)
|
gl.EnableVertexAttribArray(texCoordAttrib)
|
||||||
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
|
gl.VertexAttribPointer(texCoordAttrib, 2, gl.FLOAT, false, 4*4, gl.PtrOffset(2*4))
|
||||||
defer gl.DisableVertexAttribArray(texCoordAttrib)
|
|
||||||
|
|
||||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||||
gl.BindVertexArray(0)
|
gl.BindVertexArray(0)
|
||||||
|
|
|
@ -15,6 +15,7 @@ var actionMap = map[config.UserAction]func(gui *GUI){
|
||||||
config.ActionSearch: actionSearchSelection,
|
config.ActionSearch: actionSearchSelection,
|
||||||
config.ActionToggleSlomo: actionToggleSlomo,
|
config.ActionToggleSlomo: actionToggleSlomo,
|
||||||
config.ActionReportBug: actionReportBug,
|
config.ActionReportBug: actionReportBug,
|
||||||
|
config.ActionBufferClear: actionBufferClear,
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionCopy(gui *GUI) {
|
func actionCopy(gui *GUI) {
|
||||||
|
@ -33,7 +34,7 @@ func actionPaste(gui *GUI) {
|
||||||
|
|
||||||
func actionToggleDebug(gui *GUI) {
|
func actionToggleDebug(gui *GUI) {
|
||||||
gui.showDebugInfo = !gui.showDebugInfo
|
gui.showDebugInfo = !gui.showDebugInfo
|
||||||
gui.terminal.SetDirty()
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionSearchSelection(gui *GUI) {
|
func actionSearchSelection(gui *GUI) {
|
||||||
|
@ -50,3 +51,7 @@ func actionToggleSlomo(gui *GUI) {
|
||||||
func actionReportBug(gui *GUI) {
|
func actionReportBug(gui *GUI) {
|
||||||
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
|
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func actionBufferClear(gui *GUI) {
|
||||||
|
gui.terminal.ReallyClear()
|
||||||
|
}
|
||||||
|
|
159
gui/gui.go
159
gui/gui.go
|
@ -24,6 +24,12 @@ import (
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// wakePeriod controls how often the main loop is woken up. This has
|
||||||
|
// significant impact on how Aminal feels to use. Adjust with care and
|
||||||
|
// test changes on all supported platforms.
|
||||||
|
const wakePeriod = time.Second / 120
|
||||||
|
const halfWakePeriod = wakePeriod / 2
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultWindowWidth = 800
|
DefaultWindowWidth = 800
|
||||||
DefaultWindowHeight = 600
|
DefaultWindowHeight = 600
|
||||||
|
@ -78,6 +84,8 @@ type GUI struct {
|
||||||
selectionRegionMode buffer.SelectionRegionMode
|
selectionRegionMode buffer.SelectionRegionMode
|
||||||
|
|
||||||
vScrollbar *scrollbar
|
vScrollbar *scrollbar
|
||||||
|
|
||||||
|
mainThreadFunc chan func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Min(x, y int) int {
|
func Min(x, y int) int {
|
||||||
|
@ -182,6 +190,8 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
|
||||||
internalResize: false,
|
internalResize: false,
|
||||||
vScrollbar: nil,
|
vScrollbar: nil,
|
||||||
catchedMouseHandler: nil,
|
catchedMouseHandler: nil,
|
||||||
|
|
||||||
|
mainThreadFunc: make(chan func()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,15 +211,23 @@ func (gui *GUI) scale() float32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// can only be called on OS thread
|
// can only be called on OS thread
|
||||||
func (gui *GUI) resizeToTerminal(newCols uint, newRows uint) {
|
func (gui *GUI) resizeToTerminal() {
|
||||||
|
|
||||||
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order of locking:
|
||||||
|
// 1. resizeLock
|
||||||
|
// 2. terminal's lock
|
||||||
gui.resizeLock.Lock()
|
gui.resizeLock.Lock()
|
||||||
defer gui.resizeLock.Unlock()
|
defer gui.resizeLock.Unlock()
|
||||||
|
gui.terminal.Lock()
|
||||||
|
defer gui.terminal.Unlock()
|
||||||
|
|
||||||
|
termCols, termRows := gui.terminal.GetSize()
|
||||||
|
newCols := uint(termCols)
|
||||||
|
newRows := uint(termRows)
|
||||||
cols, rows := gui.renderer.GetTermSize()
|
cols, rows := gui.renderer.GetTermSize()
|
||||||
if cols == newCols && rows == newRows {
|
if cols == newCols && rows == newRows {
|
||||||
return
|
return
|
||||||
|
@ -277,9 +295,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order of locking:
|
||||||
|
// 1. resizeLock
|
||||||
|
// 2. terminal's lock
|
||||||
|
terminalAlreadyLocked := false
|
||||||
if gui.internalResize == false {
|
if gui.internalResize == false {
|
||||||
gui.resizeLock.Lock()
|
gui.resizeLock.Lock()
|
||||||
defer gui.resizeLock.Unlock()
|
defer gui.resizeLock.Unlock()
|
||||||
|
// No need to lock the terminal right away, we can lock it later
|
||||||
|
} else {
|
||||||
|
terminalAlreadyLocked = true
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
|
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
|
||||||
|
@ -307,6 +332,11 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
||||||
gui.logger.Debugf("Calculating size in cols/rows...")
|
gui.logger.Debugf("Calculating size in cols/rows...")
|
||||||
cols, rows := gui.renderer.GetTermSize()
|
cols, rows := gui.renderer.GetTermSize()
|
||||||
gui.logger.Debugf("Resizing internal terminal...")
|
gui.logger.Debugf("Resizing internal terminal...")
|
||||||
|
if !terminalAlreadyLocked {
|
||||||
|
gui.terminal.Lock()
|
||||||
|
defer gui.terminal.Unlock()
|
||||||
|
terminalAlreadyLocked = true
|
||||||
|
}
|
||||||
if err := gui.terminal.SetSize(cols, rows); err != nil {
|
if err := gui.terminal.SetSize(cols, rows); err != nil {
|
||||||
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
|
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
|
||||||
}
|
}
|
||||||
|
@ -317,11 +347,16 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
||||||
gui.logger.Debugf("Setting viewport size...")
|
gui.logger.Debugf("Setting viewport size...")
|
||||||
gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
|
gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
|
||||||
|
|
||||||
|
if !terminalAlreadyLocked {
|
||||||
|
gui.terminal.Lock()
|
||||||
|
defer gui.terminal.Unlock()
|
||||||
|
terminalAlreadyLocked = true
|
||||||
|
}
|
||||||
gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight)
|
gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight)
|
||||||
|
|
||||||
gui.logger.Debugf("Resize complete!")
|
gui.logger.Debugf("Resize complete!")
|
||||||
|
|
||||||
gui.redraw()
|
gui.redraw(!terminalAlreadyLocked)
|
||||||
gui.window.SwapBuffers()
|
gui.window.SwapBuffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,6 +369,7 @@ func (gui *GUI) getTermSize() (uint, uint) {
|
||||||
|
|
||||||
func (gui *GUI) Close() {
|
func (gui *GUI) Close() {
|
||||||
gui.window.SetShouldClose(true)
|
gui.window.SetShouldClose(true)
|
||||||
|
glfw.PostEmptyEvent() // wake up main loop so it notices close request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *GUI) Render() error {
|
func (gui *GUI) Render() error {
|
||||||
|
@ -391,11 +427,11 @@ func (gui *GUI) Render() error {
|
||||||
gui.window.SetCursorPosCallback(gui.globalMouseMoveCallback)
|
gui.window.SetCursorPosCallback(gui.globalMouseMoveCallback)
|
||||||
gui.window.SetCursorEnterCallback(gui.globalCursorEnterCallback)
|
gui.window.SetCursorEnterCallback(gui.globalCursorEnterCallback)
|
||||||
gui.window.SetRefreshCallback(func(w *glfw.Window) {
|
gui.window.SetRefreshCallback(func(w *glfw.Window) {
|
||||||
gui.terminal.SetDirty()
|
gui.terminal.NotifyDirty()
|
||||||
})
|
})
|
||||||
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
|
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
|
||||||
if focused {
|
if focused {
|
||||||
gui.terminal.SetDirty()
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
gui.window.SetPosCallback(gui.windowPosChangeCallback)
|
gui.window.SetPosCallback(gui.windowPosChangeCallback)
|
||||||
|
@ -408,6 +444,10 @@ func (gui *GUI) Render() error {
|
||||||
gui.resize(gui.window, w, h)
|
gui.resize(gui.window, w, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gui.terminal.AttachTitleChangeHandler(titleChan)
|
||||||
|
gui.terminal.AttachResizeHandler(resizeChan)
|
||||||
|
gui.terminal.AttachReverseHandler(reverseChan)
|
||||||
|
|
||||||
gui.logger.Debugf("Starting pty read handling...")
|
gui.logger.Debugf("Starting pty read handling...")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -430,10 +470,6 @@ func (gui *GUI) Render() error {
|
||||||
gl.Disable(gl.DEPTH_TEST)
|
gl.Disable(gl.DEPTH_TEST)
|
||||||
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||||
|
|
||||||
gui.terminal.AttachTitleChangeHandler(titleChan)
|
|
||||||
gui.terminal.AttachResizeHandler(resizeChan)
|
|
||||||
gui.terminal.AttachReverseHandler(reverseChan)
|
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
@ -452,34 +488,18 @@ func (gui *GUI) Render() error {
|
||||||
r, err := version.GetNewerRelease()
|
r, err := version.GetNewerRelease()
|
||||||
if err == nil && r != nil {
|
if err == nil && r != nil {
|
||||||
latestVersion = r.TagName
|
latestVersion = r.TagName
|
||||||
gui.terminal.SetDirty()
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
showMessage := true
|
showMessage := true
|
||||||
|
|
||||||
|
stop := make(chan struct{})
|
||||||
|
go gui.waker(stop)
|
||||||
|
|
||||||
for !gui.window.ShouldClose() {
|
for !gui.window.ShouldClose() {
|
||||||
|
gui.redraw(true)
|
||||||
forceRedraw := false
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-titleChan:
|
|
||||||
gui.window.SetTitle(gui.terminal.GetTitle())
|
|
||||||
case <-resizeChan:
|
|
||||||
cols, rows := gui.terminal.GetSize()
|
|
||||||
gui.resizeToTerminal(uint(cols), uint(rows))
|
|
||||||
case reverse := <-reverseChan:
|
|
||||||
gui.generateDefaultCell(reverse)
|
|
||||||
forceRedraw = true
|
|
||||||
default:
|
|
||||||
// this is more efficient than glfw.PollEvents()
|
|
||||||
glfw.WaitEventsTimeout(0.02) // up to 50fps on no input, otherwise higher
|
|
||||||
}
|
|
||||||
|
|
||||||
if gui.terminal.CheckDirty() || forceRedraw || (gui.vScrollbar != nil && gui.vScrollbar.isDirty) {
|
|
||||||
|
|
||||||
gui.redraw()
|
|
||||||
|
|
||||||
if gui.showDebugInfo {
|
if gui.showDebugInfo {
|
||||||
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
|
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
|
||||||
|
@ -499,7 +519,7 @@ Buffer Size: %d lines
|
||||||
|
|
||||||
if showMessage {
|
if showMessage {
|
||||||
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
|
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
|
||||||
time.AfterFunc(time.Second, gui.terminal.SetDirty)
|
time.AfterFunc(time.Second, gui.terminal.NotifyDirty)
|
||||||
_, h := gui.terminal.GetSize()
|
_, h := gui.terminal.GetSize()
|
||||||
var msg string
|
var msg string
|
||||||
if version.Version == "" {
|
if version.Version == "" {
|
||||||
|
@ -520,17 +540,74 @@ Buffer Size: %d lines
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.SwapBuffers()
|
gui.SwapBuffers()
|
||||||
|
glfw.WaitEvents() // Go to sleep until next event.
|
||||||
|
|
||||||
|
// Process any terminal events since the last wakeup.
|
||||||
|
terminalEvents:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-titleChan:
|
||||||
|
gui.window.SetTitle(gui.terminal.GetTitle())
|
||||||
|
case <-resizeChan:
|
||||||
|
gui.resizeToTerminal()
|
||||||
|
case reverse := <-reverseChan:
|
||||||
|
gui.generateDefaultCell(reverse)
|
||||||
|
case funcForMainThread := <-gui.mainThreadFunc:
|
||||||
|
funcForMainThread()
|
||||||
|
default:
|
||||||
|
break terminalEvents
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
close(stop) // Tell waker to end.
|
||||||
|
|
||||||
gui.logger.Debugf("Stopping render...")
|
gui.logger.Debugf("Stopping render...")
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *GUI) redraw() {
|
// waker is a goroutine which listens to the terminal's dirty channel,
|
||||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
// waking up the main thread when the GUI needs to be
|
||||||
|
// redrawn. Limiting is applied on wakeups to avoid excessive CPU
|
||||||
|
// usage when the terminal is being updated rapidly.
|
||||||
|
func (gui *GUI) waker(stop <-chan struct{}) {
|
||||||
|
dirty := gui.terminal.Dirty()
|
||||||
|
var nextWake <-chan time.Time
|
||||||
|
var last time.Time
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-dirty:
|
||||||
|
if nextWake == nil {
|
||||||
|
if time.Since(last) > wakePeriod {
|
||||||
|
// There hasn't been a wakeup recently so schedule
|
||||||
|
// the next one sooner.
|
||||||
|
nextWake = time.After(halfWakePeriod)
|
||||||
|
} else {
|
||||||
|
nextWake = time.After(wakePeriod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case last = <-nextWake:
|
||||||
|
// TODO(mjs) - This is somewhat of a voodoo sleep but it
|
||||||
|
// avoid various rendering issues on Windows in some
|
||||||
|
// situations. Suspect that this will become unnecessary
|
||||||
|
// once various goroutine synchronisation issues have been
|
||||||
|
// resolved.
|
||||||
|
time.Sleep(halfWakePeriod)
|
||||||
|
|
||||||
|
glfw.PostEmptyEvent()
|
||||||
|
nextWake = nil
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) renderTerminalData(shouldLock bool) {
|
||||||
|
if shouldLock {
|
||||||
|
gui.terminal.Lock()
|
||||||
|
defer gui.terminal.Unlock()
|
||||||
|
}
|
||||||
lines := gui.terminal.GetVisibleLines()
|
lines := gui.terminal.GetVisibleLines()
|
||||||
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
|
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
|
||||||
colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
|
colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
|
||||||
|
@ -661,6 +738,11 @@ func (gui *GUI) redraw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.renderScrollbar()
|
gui.renderScrollbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) redraw(shouldLock bool) {
|
||||||
|
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
||||||
|
gui.renderTerminalData(shouldLock)
|
||||||
gui.renderOverlay()
|
gui.renderOverlay()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,3 +900,14 @@ func (gui *GUI) renderScrollbar() {
|
||||||
gui.vScrollbar.render(gui)
|
gui.vScrollbar.render(gui)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Synchronously executes the argument function in the main thread.
|
||||||
|
// Does not return until f() executed!
|
||||||
|
func (gui *GUI) executeInMainThread(f func() error) error {
|
||||||
|
resultChan := make(chan error, 1)
|
||||||
|
gui.mainThreadFunc <- func() {
|
||||||
|
resultChan <- f()
|
||||||
|
}
|
||||||
|
gui.terminal.NotifyDirty() // wake up the main thread to allow processing
|
||||||
|
return <-resultChan
|
||||||
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (gui *GUI) updateSelectionMode(mods glfw.ModifierKey) {
|
||||||
}
|
}
|
||||||
if gui.selectionRegionMode != mode {
|
if gui.selectionRegionMode != mode {
|
||||||
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) {
|
func (gui *GUI) setOverlay(m overlay) {
|
||||||
defer gui.terminal.SetDirty()
|
|
||||||
gui.overlay = m
|
gui.overlay = m
|
||||||
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *GUI) renderOverlay() {
|
func (gui *GUI) renderOverlay() {
|
||||||
|
|
|
@ -26,8 +26,7 @@ const (
|
||||||
outColor = inColor;
|
outColor = inColor;
|
||||||
}` + "\x00"
|
}` + "\x00"
|
||||||
|
|
||||||
BorderVertexValuesCount = 16
|
NumberOfVertexValues = 100
|
||||||
ArrowsVertexValuesCount = 24
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -85,8 +84,6 @@ type scrollbar struct {
|
||||||
uniformLocationResolution int32
|
uniformLocationResolution int32
|
||||||
uniformLocationInColor int32
|
uniformLocationInColor int32
|
||||||
|
|
||||||
isDirty bool
|
|
||||||
|
|
||||||
position ScreenRectangle // relative to the window's top left corner, in pixels
|
position ScreenRectangle // relative to the window's top left corner, in pixels
|
||||||
positionUpperArrow ScreenRectangle // relative to the control's top left corner
|
positionUpperArrow ScreenRectangle // relative to the control's top left corner
|
||||||
positionBottomArrow ScreenRectangle
|
positionBottomArrow ScreenRectangle
|
||||||
|
@ -152,7 +149,7 @@ func newScrollbar() (*scrollbar, error) {
|
||||||
gl.BindVertexArray(vao)
|
gl.BindVertexArray(vao)
|
||||||
|
|
||||||
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
|
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
|
||||||
gl.BufferData(gl.ARRAY_BUFFER, (BorderVertexValuesCount+ArrowsVertexValuesCount)*4, nil, gl.DYNAMIC_DRAW) // only reserve data
|
gl.BufferData(gl.ARRAY_BUFFER, NumberOfVertexValues*4, nil, gl.DYNAMIC_DRAW) // only reserve the space
|
||||||
|
|
||||||
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
|
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
|
||||||
gl.EnableVertexAttribArray(0)
|
gl.EnableVertexAttribArray(0)
|
||||||
|
@ -167,8 +164,6 @@ func newScrollbar() (*scrollbar, error) {
|
||||||
uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")),
|
uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")),
|
||||||
uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")),
|
uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")),
|
||||||
|
|
||||||
isDirty: false,
|
|
||||||
|
|
||||||
position: ScreenRectangle{
|
position: ScreenRectangle{
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
@ -245,7 +240,7 @@ func (sb *scrollbar) resize(gui *GUI) {
|
||||||
sb.position.bottom = float32(gui.height - 1)
|
sb.position.bottom = float32(gui.height - 1)
|
||||||
|
|
||||||
sb.recalcElementPositions()
|
sb.recalcElementPositions()
|
||||||
sb.isDirty = true
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *scrollbar) render(gui *GUI) {
|
func (sb *scrollbar) render(gui *GUI) {
|
||||||
|
@ -256,7 +251,11 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
gl.UseProgram(sb.program)
|
gl.UseProgram(sb.program)
|
||||||
gl.Uniform2f(sb.uniformLocationResolution, float32(gui.width), float32(gui.height))
|
gl.Uniform2f(sb.uniformLocationResolution, float32(gui.width), float32(gui.height))
|
||||||
gl.BindVertexArray(sb.vao)
|
gl.BindVertexArray(sb.vao)
|
||||||
defer gl.BindVertexArray(0)
|
gl.BindBuffer(gl.ARRAY_BUFFER, sb.vbo)
|
||||||
|
defer func() {
|
||||||
|
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||||
|
gl.BindVertexArray(0)
|
||||||
|
}()
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
gl.Uniform4f(sb.uniformLocationInColor, scrollbarColor_Bg[0], scrollbarColor_Bg[1], scrollbarColor_Bg[2], 1.0)
|
gl.Uniform4f(sb.uniformLocationInColor, scrollbarColor_Bg[0], scrollbarColor_Bg[1], scrollbarColor_Bg[2], 1.0)
|
||||||
|
@ -269,7 +268,7 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left, sb.position.bottom,
|
sb.position.left, sb.position.bottom,
|
||||||
sb.position.left, sb.position.top,
|
sb.position.left, sb.position.top,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(borderVertices)*4, gl.Ptr(&borderVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(borderVertices)*4, gl.Ptr(&borderVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(borderVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(borderVertices)/2))
|
||||||
|
|
||||||
// Draw upper arrow
|
// Draw upper arrow
|
||||||
|
@ -284,7 +283,7 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.bottom,
|
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.bottom,
|
||||||
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top,
|
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(upperArrowBgVertices)*4, gl.Ptr(&upperArrowBgVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowBgVertices)*4, gl.Ptr(&upperArrowBgVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowBgVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowBgVertices)/2))
|
||||||
|
|
||||||
// Upper arrow foreground
|
// Upper arrow foreground
|
||||||
|
@ -294,7 +293,7 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()*2.0/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
|
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()*2.0/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
|
||||||
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
|
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(upperArrowFgVertices)*4, gl.Ptr(&upperArrowFgVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowFgVertices)*4, gl.Ptr(&upperArrowFgVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowFgVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowFgVertices)/2))
|
||||||
|
|
||||||
// Draw bottom arrow
|
// Draw bottom arrow
|
||||||
|
@ -309,7 +308,7 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.bottom,
|
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.bottom,
|
||||||
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top,
|
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(bottomArrowBgVertices)*4, gl.Ptr(&bottomArrowBgVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowBgVertices)*4, gl.Ptr(&bottomArrowBgVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowBgVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowBgVertices)/2))
|
||||||
|
|
||||||
// Bottom arrow foreground
|
// Bottom arrow foreground
|
||||||
|
@ -319,7 +318,7 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()*2.0/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0,
|
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()*2.0/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0,
|
||||||
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/2.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()*2.0/3.0,
|
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/2.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()*2.0/3.0,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(bottomArrowFgVertices)*4, gl.Ptr(&bottomArrowFgVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowFgVertices)*4, gl.Ptr(&bottomArrowFgVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowFgVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowFgVertices)/2))
|
||||||
|
|
||||||
// Draw thumb
|
// Draw thumb
|
||||||
|
@ -333,10 +332,10 @@ func (sb *scrollbar) render(gui *GUI) {
|
||||||
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.bottom,
|
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.bottom,
|
||||||
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top,
|
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top,
|
||||||
}
|
}
|
||||||
gl.NamedBufferSubData(sb.vbo, 0, len(thumbVertices)*4, gl.Ptr(&thumbVertices[0]))
|
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(thumbVertices)*4, gl.Ptr(&thumbVertices[0]))
|
||||||
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(thumbVertices)/2))
|
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(thumbVertices)/2))
|
||||||
|
|
||||||
sb.isDirty = false
|
gui.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *scrollbar) setPosition(max int, position int) {
|
func (sb *scrollbar) setPosition(max int, position int) {
|
||||||
|
@ -352,7 +351,6 @@ func (sb *scrollbar) setPosition(max int, position int) {
|
||||||
sb.scrollPosition = position
|
sb.scrollPosition = position
|
||||||
|
|
||||||
sb.recalcElementPositions()
|
sb.recalcElementPositions()
|
||||||
sb.isDirty = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *scrollbar) mouseHitTest(px float64, py float64) scrollbarPart {
|
func (sb *scrollbar) mouseHitTest(px float64, py float64) scrollbarPart {
|
||||||
|
@ -435,7 +433,7 @@ func (sb *scrollbar) mouseButtonCallback(g *GUI, button glfw.MouseButton, action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.isDirty = true
|
g.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.resetElementColors(mouseX, mouseY)
|
sb.resetElementColors(mouseX, mouseY)
|
||||||
|
@ -470,7 +468,7 @@ func (sb *scrollbar) mouseMoveCallback(g *GUI, px float64, py float64) {
|
||||||
g.logger.Debugf("new thumbTop: %f, fact thumbTop: %f, position: %d", newThumbTop, sb.positionThumb.top, sb.scrollPosition)
|
g.logger.Debugf("new thumbTop: %f, fact thumbTop: %f, position: %d", newThumbTop, sb.positionThumb.top, sb.scrollPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.isDirty = true
|
g.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) {
|
func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) {
|
||||||
|
@ -516,6 +514,6 @@ func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) {
|
||||||
func (sb *scrollbar) cursorEnterCallback(g *GUI, entered bool) {
|
func (sb *scrollbar) cursorEnterCallback(g *GUI, entered bool) {
|
||||||
if !entered {
|
if !entered {
|
||||||
sb.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse
|
sb.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse
|
||||||
sb.isDirty = true
|
g.terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-gl/glfw/v3.2/glfw"
|
||||||
|
"github.com/liamg/aminal/terminal"
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Implementation of the terminal.WindowManipulationInterface
|
||||||
|
//
|
||||||
|
|
||||||
|
func (gui *GUI) RestoreWindow(term *terminal.Terminal) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
gui.window.Restore()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) IconifyWindow(term *terminal.Terminal) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
return gui.window.Iconify()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) MoveWindow(term *terminal.Terminal, pixelX int, pixelY int) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
gui.window.SetPos(pixelX, pixelY)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ResizeWindowByPixels(term *terminal.Terminal, pixelsHeight int, pixelsWidth int) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
term.Unlock()
|
||||||
|
gui.window.SetSize(pixelsWidth, pixelsHeight)
|
||||||
|
term.Lock()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) BringWindowToFront(term *terminal.Terminal) error {
|
||||||
|
var err error
|
||||||
|
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
||||||
|
err = gui.executeInMainThread(func() error {
|
||||||
|
return gui.window.Restore()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = gui.window.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ResizeWindowByChars(term *terminal.Terminal, charsHeight int, charsWidth int) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
return term.SetSize(uint(charsWidth), uint(charsHeight))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) MaximizeWindow(term *terminal.Terminal) error {
|
||||||
|
return gui.executeInMainThread(func() error {
|
||||||
|
term.Lock()
|
||||||
|
err := gui.window.Maximize()
|
||||||
|
term.Unlock()
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ReportWindowState(term *terminal.Terminal) error {
|
||||||
|
// Report xterm window state. If the xterm window is open (non-iconified), it returns CSI 1 t .
|
||||||
|
// If the xterm window is iconified, it returns CSI 2 t .
|
||||||
|
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
||||||
|
_ = term.Write([]byte("\x1b[2t"))
|
||||||
|
} else {
|
||||||
|
_ = term.Write([]byte("\x1b[1t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ReportWindowPosition(term *terminal.Terminal) error {
|
||||||
|
// Report xterm window position as CSI 3 ; x; yt
|
||||||
|
x, y := gui.window.GetPos()
|
||||||
|
|
||||||
|
_ = term.Write([]byte(fmt.Sprintf("\x1b[3;%d;%dt", x, y)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ReportWindowSizeInPixels(term *terminal.Terminal) error {
|
||||||
|
// Report xterm window in pixels as CSI 4 ; height ; width t
|
||||||
|
_ = term.Write([]byte(fmt.Sprintf("\x1b[4;%d;%dt", gui.height, gui.width)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *GUI) ReportWindowSizeInChars(term *terminal.Terminal) error {
|
||||||
|
// Report the size of the text area in characters as CSI 8 ; height ; width t
|
||||||
|
charsWidth, charsHeight := gui.renderer.GetTermSize()
|
||||||
|
|
||||||
|
_ = term.Write([]byte(fmt.Sprintf("\x1b[8;%d;%dt", charsHeight, charsWidth)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
34
main.go
34
main.go
|
@ -2,12 +2,16 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
|
||||||
|
"github.com/liamg/aminal/config"
|
||||||
"github.com/liamg/aminal/gui"
|
"github.com/liamg/aminal/gui"
|
||||||
"github.com/liamg/aminal/platform"
|
"github.com/liamg/aminal/platform"
|
||||||
"github.com/liamg/aminal/terminal"
|
"github.com/liamg/aminal/terminal"
|
||||||
"github.com/riywo/loginshell"
|
"github.com/riywo/loginshell"
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type callback func(terminal *terminal.Terminal, g *gui.GUI)
|
type callback func(terminal *terminal.Terminal, g *gui.GUI)
|
||||||
|
@ -17,11 +21,12 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
initialize(nil)
|
initialize(nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialize(unitTestfunc callback) {
|
func initialize(unitTestfunc callback, configOverride *config.Config) {
|
||||||
conf := getConfig()
|
conf := maybeGetConfig(configOverride)
|
||||||
|
|
||||||
logger, err := getLogger(conf)
|
logger, err := getLogger(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to create logger: %s\n", err)
|
fmt.Printf("Failed to create logger: %s\n", err)
|
||||||
|
@ -29,13 +34,13 @@ func initialize(unitTestfunc callback) {
|
||||||
}
|
}
|
||||||
defer logger.Sync()
|
defer logger.Sync()
|
||||||
|
|
||||||
if unitTestfunc != nil {
|
if conf.CPUProfile != "" {
|
||||||
// Force the scrollbar not showing when running unit tests
|
logger.Infof("Starting CPU profiling...")
|
||||||
conf.ShowVerticalScrollbar = false
|
stop := startCPUProf(conf.CPUProfile)
|
||||||
|
defer stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Allocating pty...")
|
logger.Infof("Allocating pty...")
|
||||||
|
|
||||||
pty, err := platform.NewPty(80, 25)
|
pty, err := platform.NewPty(80, 25)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Failed to allocate pty: %s", err)
|
logger.Fatalf("Failed to allocate pty: %s", err)
|
||||||
|
@ -69,6 +74,8 @@ func initialize(unitTestfunc callback) {
|
||||||
}
|
}
|
||||||
defer g.Free()
|
defer g.Free()
|
||||||
|
|
||||||
|
terminal.WindowManipulation = g
|
||||||
|
|
||||||
if unitTestfunc != nil {
|
if unitTestfunc != nil {
|
||||||
go unitTestfunc(terminal, g)
|
go unitTestfunc(terminal, g)
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,3 +91,12 @@ func initialize(unitTestfunc callback) {
|
||||||
logger.Fatalf("Render error: %s", err)
|
logger.Fatalf("Render error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startCPUProf(filename string) func() {
|
||||||
|
profileFile, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
pprof.StartCPUProfile(profileFile)
|
||||||
|
return pprof.StopCPUProfile
|
||||||
|
}
|
||||||
|
|
28
main_test.go
28
main_test.go
|
@ -6,10 +6,12 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/liamg/aminal/config"
|
||||||
"github.com/liamg/aminal/gui"
|
"github.com/liamg/aminal/gui"
|
||||||
"github.com/liamg/aminal/terminal"
|
"github.com/liamg/aminal/terminal"
|
||||||
|
|
||||||
|
@ -105,7 +107,11 @@ func TestCursorMovement(t *testing.T) {
|
||||||
send(term, "1\n")
|
send(term, "1\n")
|
||||||
sleep()
|
sleep()
|
||||||
|
|
||||||
if term.ActiveBuffer().CompareViewLines("vttest/test-cursor-movement-1") == false {
|
term.Lock()
|
||||||
|
compareResult := term.ActiveBuffer().CompareViewLines("vttest/test-cursor-movement-1")
|
||||||
|
term.Unlock()
|
||||||
|
|
||||||
|
if compareResult == false {
|
||||||
os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1")))
|
os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +125,7 @@ func TestCursorMovement(t *testing.T) {
|
||||||
g.Close()
|
g.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(testFunc)
|
initialize(testFunc, testConfig())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +161,7 @@ func TestScreenFeatures(t *testing.T) {
|
||||||
g.Close()
|
g.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(testFunc)
|
initialize(testFunc, testConfig())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +186,7 @@ func TestSixel(t *testing.T) {
|
||||||
g.Close()
|
g.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(testFunc)
|
initialize(testFunc, testConfig())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,3 +194,17 @@ func TestSixel(t *testing.T) {
|
||||||
func TestExit(t *testing.T) {
|
func TestExit(t *testing.T) {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testConfig() *config.Config {
|
||||||
|
c := config.DefaultConfig()
|
||||||
|
|
||||||
|
// Force the scrollbar not showing when running unit tests
|
||||||
|
c.ShowVerticalScrollbar = false
|
||||||
|
|
||||||
|
// Use a vanilla shell on POSIX to help ensure consistency.
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
c.Shell = "/bin/sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
|
@ -19,4 +19,5 @@ type Pty interface {
|
||||||
Resize(x int, y int) error
|
Resize(x int, y int) error
|
||||||
CreateGuestProcess(imagePath string) (Process, error)
|
CreateGuestProcess(imagePath string) (Process, error)
|
||||||
GetPlatformDependentSettings() PlatformDependentSettings
|
GetPlatformDependentSettings() PlatformDependentSettings
|
||||||
|
Clear()
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,6 +86,10 @@ func (pty *unixPty) GetPlatformDependentSettings() PlatformDependentSettings {
|
||||||
return pty.platformDependentSettings
|
return pty.platformDependentSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pty *unixPty) Clear() {
|
||||||
|
// do nothing for unix
|
||||||
|
}
|
||||||
|
|
||||||
func NewPty(x, y int) (Pty, error) {
|
func NewPty(x, y int) (Pty, error) {
|
||||||
innerPty, innerTty, err := pty.Open()
|
innerPty, innerTty, err := pty.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/MaxRis/w32"
|
"github.com/MaxRis/w32"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,6 +99,7 @@ type winConPty struct {
|
||||||
innerInPipe syscall.Handle
|
innerInPipe syscall.Handle
|
||||||
innerOutPipe syscall.Handle
|
innerOutPipe syscall.Handle
|
||||||
hcon uintptr
|
hcon uintptr
|
||||||
|
processID uint32 // required to obtain old-style console handle (for standard console functions)
|
||||||
platformDependentSettings PlatformDependentSettings
|
platformDependentSettings PlatformDependentSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,6 +142,8 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pty.processID = process.processID
|
||||||
|
|
||||||
err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT)
|
err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
process.Close()
|
process.Close()
|
||||||
|
@ -192,6 +196,50 @@ func (pty *winConPty) GetPlatformDependentSettings() PlatformDependentSettings {
|
||||||
return pty.platformDependentSettings
|
return pty.platformDependentSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pty *winConPty) Clear() {
|
||||||
|
C.FreeConsole()
|
||||||
|
defer C.AttachConsole(^C.DWORD(0)) // attach to parent process console
|
||||||
|
|
||||||
|
if C.AttachConsole(C.DWORD(pty.processID)) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer C.FreeConsole()
|
||||||
|
hConsole := C.GetStdHandle(C.STD_OUTPUT_HANDLE)
|
||||||
|
|
||||||
|
var coordScreen C.COORD
|
||||||
|
coordScreen.X = 0
|
||||||
|
coordScreen.Y = 0
|
||||||
|
var cCharsWritten C.DWORD
|
||||||
|
var csbi C.CONSOLE_SCREEN_BUFFER_INFO
|
||||||
|
|
||||||
|
// Get the number of character cells in the current buffer.
|
||||||
|
if C.GetConsoleScreenBufferInfo(hConsole, &csbi) != C.BOOL(0) {
|
||||||
|
|
||||||
|
dwConSize := C.DWORD(csbi.dwSize.X * csbi.dwSize.Y)
|
||||||
|
|
||||||
|
// Fill the entire screen with blanks.
|
||||||
|
C.FillConsoleOutputCharacterA(hConsole, // Handle to console screen buffer
|
||||||
|
' ', // Character to write to the buffer
|
||||||
|
dwConSize, // Number of cells to write
|
||||||
|
coordScreen, // Coordinates of first cell
|
||||||
|
&cCharsWritten) // Receive number of characters written
|
||||||
|
|
||||||
|
// Get the current text attribute.
|
||||||
|
if C.GetConsoleScreenBufferInfo(hConsole, &csbi) != C.BOOL(0) {
|
||||||
|
// Set the buffer's attributes accordingly.
|
||||||
|
|
||||||
|
C.FillConsoleOutputAttribute(hConsole, // Handle to console screen buffer
|
||||||
|
csbi.wAttributes, // Character attributes to use
|
||||||
|
dwConSize, // Number of cells to set attribute
|
||||||
|
coordScreen, // Coordinates of first cell
|
||||||
|
&cCharsWritten) // Receive number of characters written
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the cursor at its home coordinates.
|
||||||
|
C.SetConsoleCursorPosition(hConsole, coordScreen)
|
||||||
|
}
|
||||||
|
|
||||||
// NewPty creates a new instance of a Pty implementation for Windows on a newly allocated ConPTY
|
// NewPty creates a new instance of a Pty implementation for Windows on a newly allocated ConPTY
|
||||||
func NewPty(x, y int) (pty Pty, err error) {
|
func NewPty(x, y int) (pty Pty, err error) {
|
||||||
if !ptyInitSucceeded {
|
if !ptyInitSucceeded {
|
||||||
|
|
|
@ -35,26 +35,42 @@ func swallowHandler(n int) func(pty chan rune, terminal *Terminal) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func risHandler(pty chan rune, terminal *Terminal) error {
|
func risHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().Clear()
|
terminal.ActiveBuffer().Clear()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexHandler(pty chan rune, terminal *Terminal) error {
|
func indexHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().Index()
|
terminal.ActiveBuffer().Index()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reverseIndexHandler(pty chan rune, terminal *Terminal) error {
|
func reverseIndexHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().ReverseIndex()
|
terminal.ActiveBuffer().ReverseIndex()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCursorHandler(pty chan rune, terminal *Terminal) error {
|
func saveCursorHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
// Handler should lock the terminal if there will be write operations to any data read by the renderer
|
||||||
|
// terminal.Lock()
|
||||||
|
// defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().SaveCursor()
|
terminal.ActiveBuffer().SaveCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreCursorHandler(pty chan rune, terminal *Terminal) error {
|
func restoreCursorHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().RestoreCursor()
|
terminal.ActiveBuffer().RestoreCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -73,11 +89,18 @@ func ansiHandler(pty chan rune, terminal *Terminal) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextLineHandler(pty chan rune, terminal *Terminal) error {
|
func nextLineHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.ActiveBuffer().NewLineEx(true)
|
terminal.ActiveBuffer().NewLineEx(true)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabSetHandler(pty chan rune, terminal *Terminal) error {
|
func tabSetHandler(pty chan rune, terminal *Terminal) error {
|
||||||
|
// Handler should lock the terminal if there will be write operations to any data read by the renderer
|
||||||
|
// terminal.Lock()
|
||||||
|
// defer terminal.Unlock()
|
||||||
|
|
||||||
terminal.terminalState.TabSetAtCursor()
|
terminal.terminalState.TabSetAtCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,10 @@ func scs1Handler(pty chan rune, terminal *Terminal) error {
|
||||||
func scsHandler(pty chan rune, terminal *Terminal, which int) error {
|
func scsHandler(pty chan rune, terminal *Terminal, which int) error {
|
||||||
b := <-pty
|
b := <-pty
|
||||||
|
|
||||||
|
// Handler should lock the terminal if there will be write operations to any data read by the renderer
|
||||||
|
// terminal.Lock()
|
||||||
|
// defer terminal.Unlock()
|
||||||
|
|
||||||
cs, ok := charSets[b]
|
cs, ok := charSets[b]
|
||||||
if ok {
|
if ok {
|
||||||
terminal.logger.Debugf("Selected charset %v into G%v", string(b), which)
|
terminal.logger.Debugf("Selected charset %v into G%v", string(b), which)
|
||||||
|
|
|
@ -89,6 +89,9 @@ func splitParams(paramString string) []string {
|
||||||
func csiHandler(pty chan rune, terminal *Terminal) error {
|
func csiHandler(pty chan rune, terminal *Terminal) error {
|
||||||
final, param, intermediate := loadCSI(pty)
|
final, param, intermediate := loadCSI(pty)
|
||||||
|
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
// process intermediate control codes before the CSI
|
// process intermediate control codes before the CSI
|
||||||
for _, b := range intermediate {
|
for _, b := range intermediate {
|
||||||
terminal.processRune(b)
|
terminal.processRune(b)
|
||||||
|
@ -422,10 +425,6 @@ func csiSetModeHandler(params []string, terminal *Terminal) error {
|
||||||
return csiSetModes(params, true, terminal)
|
return csiSetModes(params, true, terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csiWindowManipulation(params []string, terminal *Terminal) error {
|
|
||||||
return fmt.Errorf("Window manipulation is not yet supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func csiLinePositionAbsolute(params []string, terminal *Terminal) error {
|
func csiLinePositionAbsolute(params []string, terminal *Terminal) error {
|
||||||
row := 1
|
row := 1
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
|
|
|
@ -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 {
|
func newLineHandler(terminal *Terminal) error {
|
||||||
terminal.ActiveBuffer().NewLine()
|
terminal.ActiveBuffer().NewLine()
|
||||||
terminal.isDirty = true
|
terminal.NotifyDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tabHandler(terminal *Terminal) error {
|
func tabHandler(terminal *Terminal) error {
|
||||||
terminal.ActiveBuffer().Tab()
|
terminal.ActiveBuffer().Tab()
|
||||||
terminal.isDirty = true
|
terminal.NotifyDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func carriageReturnHandler(terminal *Terminal) error {
|
func carriageReturnHandler(terminal *Terminal) error {
|
||||||
terminal.ActiveBuffer().CarriageReturn()
|
terminal.ActiveBuffer().CarriageReturn()
|
||||||
terminal.isDirty = true
|
terminal.NotifyDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func backspaceHandler(terminal *Terminal) error {
|
func backspaceHandler(terminal *Terminal) error {
|
||||||
terminal.ActiveBuffer().Backspace()
|
terminal.ActiveBuffer().Backspace()
|
||||||
terminal.isDirty = true
|
terminal.NotifyDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,17 +72,24 @@ func shiftInHandler(terminal *Terminal) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) processRuneLocked(b rune) {
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
|
terminal.processRune(b)
|
||||||
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) processRune(b rune) {
|
func (terminal *Terminal) processRune(b rune) {
|
||||||
|
defer terminal.NotifyDirty()
|
||||||
|
|
||||||
if handler, ok := runeMap[b]; ok {
|
if handler, ok := runeMap[b]; ok {
|
||||||
if err := handler(terminal); err != nil {
|
if err := handler(terminal); err != nil {
|
||||||
terminal.logger.Errorf("Error handling control code: %s", err)
|
terminal.logger.Errorf("Error handling control code: %s", err)
|
||||||
}
|
}
|
||||||
terminal.isDirty = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
|
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
|
||||||
terminal.ActiveBuffer().Write(terminal.translateRune(b))
|
terminal.ActiveBuffer().Write(terminal.translateRune(b))
|
||||||
terminal.isDirty = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) translateRune(b rune) rune {
|
func (terminal *Terminal) translateRune(b rune) rune {
|
||||||
|
@ -103,6 +110,8 @@ func (terminal *Terminal) processInput(pty chan rune) {
|
||||||
|
|
||||||
var b rune
|
var b rune
|
||||||
|
|
||||||
|
// debug := ""
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|
||||||
if terminal.config.Slomo {
|
if terminal.config.Slomo {
|
||||||
|
@ -111,15 +120,19 @@ func (terminal *Terminal) processInput(pty chan rune) {
|
||||||
|
|
||||||
b = <-pty
|
b = <-pty
|
||||||
|
|
||||||
|
// debug += fmt.Sprintf("0x%x ", b)
|
||||||
|
|
||||||
if b == 0x1b {
|
if b == 0x1b {
|
||||||
|
// terminal.logger.Debug(debug)
|
||||||
|
// debug = ""
|
||||||
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
|
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
|
||||||
if err := ansiHandler(pty, terminal); err != nil {
|
if err := ansiHandler(pty, terminal); err != nil {
|
||||||
terminal.logger.Errorf("Error handling escape sequence: %s", err)
|
terminal.logger.Errorf("Error handling escape sequence: %s", err)
|
||||||
}
|
}
|
||||||
terminal.isDirty = true
|
terminal.NotifyDirty()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.processRune(b)
|
terminal.processRuneLocked(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ func screenStateHandler(pty chan rune, terminal *Terminal) error {
|
||||||
b := <-pty
|
b := <-pty
|
||||||
switch b {
|
switch b {
|
||||||
case '8': // DECALN -- Screen Alignment Pattern
|
case '8': // DECALN -- Screen Alignment Pattern
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
// hide cursor?
|
// hide cursor?
|
||||||
buffer := terminal.ActiveBuffer()
|
buffer := terminal.ActiveBuffer()
|
||||||
terminal.ResetVerticalMargins()
|
terminal.ResetVerticalMargins()
|
||||||
|
|
|
@ -131,6 +131,9 @@ func sixelHandler(pty chan rune, terminal *Terminal) error {
|
||||||
defer terminal.SetLineFeedMode()
|
defer terminal.SetLineFeedMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
terminal.Lock()
|
||||||
|
defer terminal.Unlock()
|
||||||
|
|
||||||
drawSixel(six, terminal)
|
drawSixel(six, terminal)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -33,6 +33,20 @@ const (
|
||||||
MouseExtSGR
|
MouseExtSGR
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WindowManipulationInterface interface {
|
||||||
|
RestoreWindow(term *Terminal) error
|
||||||
|
IconifyWindow(term *Terminal) error
|
||||||
|
MoveWindow(term *Terminal, pixelX int, pixelY int) error
|
||||||
|
ResizeWindowByPixels(term *Terminal, pixelsHeight int, pixelsWidth int) error
|
||||||
|
BringWindowToFront(term *Terminal) error
|
||||||
|
ResizeWindowByChars(term *Terminal, charsHeight int, charsWidth int) error
|
||||||
|
MaximizeWindow(term *Terminal) error
|
||||||
|
ReportWindowState(term *Terminal) error
|
||||||
|
ReportWindowPosition(term *Terminal) error
|
||||||
|
ReportWindowSizeInPixels(term *Terminal) error
|
||||||
|
ReportWindowSizeInChars(term *Terminal) error
|
||||||
|
}
|
||||||
|
|
||||||
type Terminal struct {
|
type Terminal struct {
|
||||||
program uint32
|
program uint32
|
||||||
buffers []*buffer.Buffer
|
buffers []*buffer.Buffer
|
||||||
|
@ -50,12 +64,14 @@ type Terminal struct {
|
||||||
mouseMode MouseMode
|
mouseMode MouseMode
|
||||||
mouseExtMode MouseExtMode
|
mouseExtMode MouseExtMode
|
||||||
bracketedPasteMode bool
|
bracketedPasteMode bool
|
||||||
isDirty bool
|
|
||||||
charWidth float32
|
charWidth float32
|
||||||
charHeight float32
|
charHeight float32
|
||||||
lastBuffer uint8
|
lastBuffer uint8
|
||||||
terminalState *buffer.TerminalState
|
terminalState *buffer.TerminalState
|
||||||
platformDependentSettings platform.PlatformDependentSettings
|
platformDependentSettings platform.PlatformDependentSettings
|
||||||
|
dirty *notifier
|
||||||
|
|
||||||
|
WindowManipulation WindowManipulationInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
type Modes struct {
|
type Modes struct {
|
||||||
|
@ -85,15 +101,28 @@ func New(pty platform.Pty, logger *zap.SugaredLogger, config *config.Config) *Te
|
||||||
ShowCursor: true,
|
ShowCursor: true,
|
||||||
},
|
},
|
||||||
platformDependentSettings: pty.GetPlatformDependentSettings(),
|
platformDependentSettings: pty.GetPlatformDependentSettings(),
|
||||||
|
dirty: newNotifier(),
|
||||||
}
|
}
|
||||||
t.buffers = []*buffer.Buffer{
|
t.buffers = []*buffer.Buffer{
|
||||||
buffer.NewBuffer(t.terminalState),
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||||
buffer.NewBuffer(t.terminalState),
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||||
buffer.NewBuffer(t.terminalState),
|
buffer.NewBuffer(t.terminalState, t.dirty),
|
||||||
}
|
}
|
||||||
t.activeBuffer = t.buffers[0]
|
t.activeBuffer = t.buffers[0]
|
||||||
return t
|
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dirty returns a channel that receives an empty struct whenever the
|
||||||
|
// terminal becomes dirty.
|
||||||
|
func (terminal *Terminal) Dirty() <-chan struct{} {
|
||||||
|
return terminal.dirty.C
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyDirty is used to signal that the terminal is dirty and the
|
||||||
|
// screen must be redrawn.
|
||||||
|
func (terminal *Terminal) NotifyDirty() {
|
||||||
|
terminal.dirty.Notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) SetProgram(program uint32) {
|
func (terminal *Terminal) SetProgram(program uint32) {
|
||||||
|
@ -104,16 +133,6 @@ func (terminal *Terminal) SetBracketedPasteMode(enabled bool) {
|
||||||
terminal.bracketedPasteMode = enabled
|
terminal.bracketedPasteMode = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) CheckDirty() bool {
|
|
||||||
d := terminal.isDirty
|
|
||||||
terminal.isDirty = false
|
|
||||||
return d || terminal.ActiveBuffer().IsDirty()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (terminal *Terminal) SetDirty() {
|
|
||||||
terminal.isDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
|
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
|
||||||
return terminal.modes.ApplicationCursorKeys
|
return terminal.modes.ApplicationCursorKeys
|
||||||
}
|
}
|
||||||
|
@ -171,7 +190,7 @@ func (terminal *Terminal) GetScrollOffset() uint {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) ScreenScrollDown(lines uint16) {
|
func (terminal *Terminal) ScreenScrollDown(lines uint16) {
|
||||||
defer terminal.SetDirty()
|
defer terminal.NotifyDirty()
|
||||||
buffer := terminal.ActiveBuffer()
|
buffer := terminal.ActiveBuffer()
|
||||||
|
|
||||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||||
|
@ -199,7 +218,7 @@ func (terminal *Terminal) AreaScrollDown(lines uint16) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) ScreenScrollUp(lines uint16) {
|
func (terminal *Terminal) ScreenScrollUp(lines uint16) {
|
||||||
defer terminal.SetDirty()
|
defer terminal.NotifyDirty()
|
||||||
buffer := terminal.ActiveBuffer()
|
buffer := terminal.ActiveBuffer()
|
||||||
|
|
||||||
if buffer.Height() < int(buffer.ViewHeight()) {
|
if buffer.Height() < int(buffer.ViewHeight()) {
|
||||||
|
@ -223,8 +242,8 @@ func (terminal *Terminal) ScrollPageUp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) ScrollToEnd() {
|
func (terminal *Terminal) ScrollToEnd() {
|
||||||
defer terminal.SetDirty()
|
|
||||||
terminal.terminalState.SetScrollOffset(0)
|
terminal.terminalState.SetScrollOffset(0)
|
||||||
|
terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) GetVisibleLines() []buffer.Line {
|
func (terminal *Terminal) GetVisibleLines() []buffer.Line {
|
||||||
|
@ -298,6 +317,7 @@ func (terminal *Terminal) GetTitle() string {
|
||||||
func (terminal *Terminal) SetTitle(title string) {
|
func (terminal *Terminal) SetTitle(title string) {
|
||||||
terminal.title = title
|
terminal.title = title
|
||||||
terminal.emitTitleChange()
|
terminal.emitTitleChange()
|
||||||
|
terminal.NotifyDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write sends data, i.e. locally typed keystrokes to the pty
|
// Write sends data, i.e. locally typed keystrokes to the pty
|
||||||
|
@ -350,14 +370,16 @@ func (terminal *Terminal) Clear() {
|
||||||
terminal.ActiveBuffer().Clear()
|
terminal.ActiveBuffer().Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) ReallyClear() {
|
||||||
|
terminal.pty.Clear()
|
||||||
|
terminal.ActiveBuffer().ReallyClear()
|
||||||
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) GetSize() (int, int) {
|
func (terminal *Terminal) GetSize() (int, int) {
|
||||||
return int(terminal.size.Width), int(terminal.size.Height)
|
return int(terminal.size.Width), int(terminal.size.Height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
|
func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
|
||||||
terminal.lock.Lock()
|
|
||||||
defer terminal.lock.Unlock()
|
|
||||||
|
|
||||||
if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) {
|
if terminal.size.Width == uint16(newCols) && terminal.size.Height == uint16(newLines) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -373,6 +395,7 @@ func (terminal *Terminal) SetSize(newCols uint, newLines uint) error {
|
||||||
terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
|
terminal.ActiveBuffer().ResizeView(terminal.size.Width, terminal.size.Height)
|
||||||
|
|
||||||
terminal.emitResize()
|
terminal.emitResize()
|
||||||
|
terminal.NotifyDirty()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,4 +442,13 @@ func (terminal *Terminal) SetScreenMode(enabled bool) {
|
||||||
buffer.ReverseVideo()
|
buffer.ReverseVideo()
|
||||||
}
|
}
|
||||||
terminal.emitReverse(enabled)
|
terminal.emitReverse(enabled)
|
||||||
|
terminal.NotifyDirty()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) Lock() {
|
||||||
|
terminal.lock.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (terminal *Terminal) Unlock() {
|
||||||
|
terminal.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
95
windows.md
95
windows.md
|
@ -12,8 +12,7 @@
|
||||||
cd %YOUR_PROJECT_WORKING_DIR%
|
cd %YOUR_PROJECT_WORKING_DIR%
|
||||||
mkdir go\src\github.com\liamg
|
mkdir go\src\github.com\liamg
|
||||||
cd go\src\github.com\liamg
|
cd go\src\github.com\liamg
|
||||||
git clone git@github.com:jumptrading/aminal-mirror.git
|
git clone https://github.com/liamg/aminal.git
|
||||||
move aminal-mirror aminal
|
|
||||||
|
|
||||||
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
|
set GOPATH=%YOUR_PROJECT_WORKING_DIR%\go
|
||||||
set GOBIN=%GOPATH%/bin
|
set GOBIN=%GOPATH%/bin
|
||||||
|
@ -28,3 +27,95 @@ go install
|
||||||
|
|
||||||
Look for the aminal.exe built binary under your %GOBIN% path
|
Look for the aminal.exe built binary under your %GOBIN% path
|
||||||
|
|
||||||
|
### Building an installer for automatic updates:
|
||||||
|
|
||||||
|
In addition to the above commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
|
||||||
|
go get golang.org/x/sys/windows
|
||||||
|
go get github.com/jteeuwen/go-bindata/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Install NSIS and place it on the `PATH`:
|
||||||
|
```
|
||||||
|
choco install nsis
|
||||||
|
set PATH=%PATH%;%ProgramFiles(x86)%\NSIS\Bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure `signtool.exe` is on the `PATH`. For instance, on Windows 10:
|
||||||
|
|
||||||
|
```
|
||||||
|
set PATH=%PATH%;%ProgramFiles(x86)%\Windows Kits\10\bin\10.0.17763.0\x64
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy your code signing certificate to `go\src\github.com\liamg\windows\codesigning_certificate.pfx`.
|
||||||
|
|
||||||
|
Set the `WINDOWS_CODESIGNING_CERT_PW` to the password of your code signing certificate:
|
||||||
|
|
||||||
|
```
|
||||||
|
set WINDOWS_CODESIGNING_CERT_PW=PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile Aminal and build the installer:
|
||||||
|
|
||||||
|
```
|
||||||
|
mingw32-make installer-windows
|
||||||
|
```
|
||||||
|
|
||||||
|
This produces several files in `bin/windows`. Their purpose is explained below.
|
||||||
|
|
||||||
|
### How Aminal's automatic update mechanism works (on Windows)
|
||||||
|
|
||||||
|
Aminal uses a technology called [Google Omaha](https://github.com/google/omaha) for automatic updates on Windows. It's the same technology which Google use to auto-update Chrome. For a quick introduction, see [this Google Omaha tutorial](https://fman.io/blog/google-omaha-tutorial/).
|
||||||
|
|
||||||
|
Aminal has an online installer. This is a 1 MB executable, which was created using Google Omaha. Suppose it's called `OnlineInstaller.exe`. When a user runs it, the following things happen:
|
||||||
|
|
||||||
|
* `OnlineInstaller.exe` installs Aminal's version of Google Omaha on the user's system. In particular, this creates two `AminalUpdateTask...` tasks in the Windows Task Scheduler. They're set up to run once per day in the background and check for updates.
|
||||||
|
* `OnlineInstaller.exe` contacts Aminal's update server and asks "what is the latest version?". The server responds with the URL to an _offline_ installer and some command line parameters. Say this offline installer is called `install-aminal-0.9.0.exe`.
|
||||||
|
* `OnlineInstaller.exe` downloads `install-aminal-0.9.0.exe` and invokes it with the given command line arguments, typically `-install`.
|
||||||
|
* `install-aminal-0.9.0.exe` performs the following steps:
|
||||||
|
* It installs Aminal 0.9.0 into `%LOCALAPPDATA%\Aminal`.
|
||||||
|
* It sets some registry keys in `HKCU\Software\Aminal`. This lets the `AminalUpdateTask...` tasks know which version of Aminal is installed.
|
||||||
|
* It creates a Start menu shortcut for starting Aminal.
|
||||||
|
|
||||||
|
When the update tasks run, you will see `AminalUpdate.exe` in the Windows Task Manager. They use the registry to send the current Aminal version to the update server and ask "is there a new version?". If yes, the server again responds with the URL to an `.exe` and some command line parameters. In Aminal's current setup, this too is `install-aminal-0.9.0.exe` (if the user had, say, Aminal 0.8.9 installed). But this time the command line flag is `-update`. The `.exe` again installs the current version of Aminal and updates the registry.
|
||||||
|
|
||||||
|
The offline installer `install-aminal-0.9.0.exe` is actually what's produced by the `mingw32-make installer-windows` command in the previous section. It is placed at `bin/windows/AminalSetup.exe` and supports the command line flags `-install`, `-update` or none. In the last case (i.e. when invoked without arguments), it acts as a normal offline installer to be invoked by the user, and does not set Omaha's registry keys. The source code for this installer lies in `windows/installer/installer.go`.
|
||||||
|
|
||||||
|
Due to the asynchronous nature of the update tasks, it can happen that Aminal is running while a new version is being downloaded / installed. To prevent this from breaking Aminal's running instance, Aminal's install dir `%LOCALAPPDATA%\Aminal` contains the following hierarchy:
|
||||||
|
|
||||||
|
* `Aminal.exe`
|
||||||
|
* `Versions/`
|
||||||
|
* `0.9.0/`
|
||||||
|
* `Aminal.exe`
|
||||||
|
|
||||||
|
The top-level `Aminal.exe` is a launcher that always invokes the latest version. When an update is downloaded / installed, it is placed in a new subfolder of `Versions/`. For instance:
|
||||||
|
|
||||||
|
* `Aminal.exe`
|
||||||
|
* `Versions/`
|
||||||
|
* `0.9.0/`
|
||||||
|
* `0.9.1/`
|
||||||
|
|
||||||
|
The next time the top-level `Aminal.exe` is invoked, it runs `Versions/0.9.1/Aminal.exe`.
|
||||||
|
|
||||||
|
The code for this top-level launcher is in `windows/launcher/launcher.go`. Its binary (and the current version subdirectory) is produced in `bin/windows` when you do `mingw32-make installer-windows`.
|
||||||
|
|
||||||
|
#### Forcing updates
|
||||||
|
|
||||||
|
By default, Aminal's (/Omaha's) update tasks only run once per day. To force an immediate update, perform the following steps:
|
||||||
|
|
||||||
|
* Delete the registry key `HKCU\Software\Aminal\Updated\LastChecked`.
|
||||||
|
* Run the task `AminalUpdateTask...UA` in the Windows Task Scheduler. Press `F5`. You'll see its result change to `0x41301`. This means it's currently running. You'll also see `AminalUpdate.exe` in the Task _Scheduler_. Keep refreshing with `F5` until both disappear.
|
||||||
|
|
||||||
|
#### Uninstalling Aminal
|
||||||
|
|
||||||
|
The installer above adds an uninstaller to the user's _Add or remove programs_ panel in Windows. When the user goes to the Control Panel and uninstalls Aminal this way, the install directory and start menu entries are removed. Further, the registry key `HKCU\Software\Aminal\Clients\{35B0CF1E-FBB0-486F-A1DA-BE3A41DDC780}` is removed. What's not removed immediately is Omaha. (So you'll still see the update tasks in the Task Scheduler.) But! The next time the update tasks run, they realize by looking at the registry that Aminal is no longer installed and uninstall themselves (and Omaha).
|
||||||
|
|
||||||
|
To work around some potential permission issues, the uninstaller is not implemented in Go (like the installer and launcher above). But via NSIS. The source code is in `windows/Uninstaller.nsi`. It's an "installer" whose sole purpose is to generate `bin/windows/Aminal/uninstall.exe`.
|
||||||
|
|
||||||
|
#### Releasing a new version via automatic updates
|
||||||
|
|
||||||
|
To release a new version, update the `VERSION` fields in `Makefile`. Then, invoke `mingw32-make installer-windows`. Log into the Omaha update server, add a new Version for Aminal that mirrors the one you set in `Makefile`. You'll need to add a trailing `.0` to the version number on the server, because Omaha uses four-tuples for versions (`0.9.1.0` instead of `0.9.1`). As the "File" for the version, upload `bin/windows/AminalSetup.exe`. Add two "Action"s: One for Event _install_, one for event _update_. In both cases, instruct the server to _Run_ `AminalSetup.exe`. For the install Event, supply Arguments `-install`. For the update Event, supply Arguments `-update`.
|
||||||
|
|
||||||
|
Once you have done this, the background update tasks on your users' systems will (over the next 24 hours) download and install the new version of Aminal from the server.
|
|
@ -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