Merge branch 'master' into vttest-changes

This commit is contained in:
Liam Galvin 2019-01-04 13:13:50 +00:00 committed by GitHub
commit 6931354579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 625 additions and 531 deletions

View File

@ -102,4 +102,5 @@ workflows:
tags:
only: /^v.*/
branches:
ignore: /.*/
ignore: /.*/

View File

@ -29,4 +29,4 @@ If applicable, add screenshots to help explain your problem.
Add any other context about the problem here.
**Logs**
Run aminal with the `--debug` flag, then paste the relevant deug logs here.
Run aminal with the `--debug` flag, then paste the relevant debug logs here.

View File

@ -3,9 +3,8 @@ BINARY := aminal
FONTPATH := ./gui/packed-fonts
.PHONY: build
build: test install-tools
packr -v
go build -ldflags "-X github.com/liamg/aminal/version.Version=`git describe --tags`"
build:
./build.sh `git describe --tags`
.PHONY: test
test:
@ -13,8 +12,7 @@ test:
go vet -v
.PHONY: install
install: build install-tools
packr -v
install: build
go install -ldflags "-X github.com/liamg/aminal/version.Version=`git describe --tags`"
.PHONY: install-tools
@ -22,12 +20,6 @@ install-tools:
which dep || curl -L https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
which packr || go get -u github.com/gobuffalo/packr/packr
.PHONY: update-fonts
update-fonts: install-tools
curl -L https://github.com/ryanoasis/nerd-fonts/raw/master/patched-fonts/Hack/Regular/complete/Hack%20Regular%20Nerd%20Font%20Complete.ttf -o "${FONTPATH}/Hack Regular Nerd Font Complete.ttf"
curl -L https://github.com/ryanoasis/nerd-fonts/raw/master/patched-fonts/Hack/Bold/complete/Hack%20Bold%20Nerd%20Font%20Complete.ttf -o "${FONTPATH}/Hack Bold Nerd Font Complete.ttf"
packr -v
.PHONY: build-linux
build-linux:
mkdir -p bin/linux
@ -40,4 +32,4 @@ build-darwin:
.PHONY: package-debian
package-debian: build-linux
./scripts/package-debian.sh "${CIRCLE_TAG}" bin/linux/${BINARY}-linux-amd64
./scripts/package-debian.sh "${CIRCLE_TAG}" bin/linux/${BINARY}-linux-amd64

View File

@ -30,31 +30,40 @@ Ensure you have your latest graphics card drivers installed before use.
- Built-in patched fonts for powerline
- Retina display support
## Quick Start
## Installation
### Installation
### MacOS
#### Prebuilt Binaries
```
brew tap liamg/aminal
brew install aminal
```
### Windows
A Windows version of Aminal is expected in the next 1-2 months.
### Prebuilt Binaries
Prebuilt binaries are available for Linux and OSX on the [releases](https://github.com/liamg/aminal/releases) page.
Download the binary and `sudo cp aminal-* /usr/local/bin/aminal`.
Download the binary and `sudo cp aminal-* /usr/local/bin/aminal && chmod +x /usr/local/bin/aminal`.
#### Install with Go
### Install with Go
```
go get -u github.com/liamg/aminal
```
### Build
## Build
#### Dependencies
### Dependencies
- On macOS, you need Xcode or Command Line Tools for Xcode (`xcode-select --install`) for required headers and libraries.
- On Ubuntu/Debian-like Linux distributions, you need `libgl1-mesa-dev xorg-dev`.
- On CentOS/Fedora-like Linux distributions, you need `libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel`.
#### Building Locally
### Building Locally
There are various make targets available, the most obvious being:
@ -82,7 +91,13 @@ As long as you have your `GOBIN` environment variable set up properly (and in `P
## Configuration
Aminal looks for a config file in `~/.aminal.toml`, and will write one there the first time it runs, if it doesn't already exist.
Aminal looks for a config file in the following places, and stops when it finds one:
* `$XDG_CONFIG_HOME/aminal/config.toml`
* `$HOME/.config/aminal/config.toml`
* `$HOME/.aminal.toml`
It will write a config file to whichever of those directories exists (preferring the top of the list) the first time it runs, if one doesn't already exist.
You can ignore the config and use defaults by specifying `--ignore-config` as a CLI flag.
@ -92,7 +107,7 @@ You can ignore the config and use defaults by specifying `--ignore-config` as a
debug = false # Enable debug logging to stdout. Defaults to false.
slomo = false # Enable slow motion output mode, useful for debugging shells/terminal GUI apps etc. Defaults to false.
shell = "/bin/bash" # The shell to run for the terminal session. Defaults to the users shell.
search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replcae when searching.
search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replace when searching.
[colours]
cursor = "#e8dfd6"
@ -133,3 +148,7 @@ search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use
| `--slomo` | Enable slomo mode, delay the handling of each incoming byte (or escape sequence) from the pty by 100ms. Useful for debugging.
| `--shell [shell]` | Use the specified shell program instead of the user's usual one.
| `--version` | Show the version of aminal and exit.
# Contributors
[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/0)](https://sourcerer.io/fame/liamg/liamg/aminal/links/0)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/1)](https://sourcerer.io/fame/liamg/liamg/aminal/links/1)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/2)](https://sourcerer.io/fame/liamg/liamg/aminal/links/2)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/3)](https://sourcerer.io/fame/liamg/liamg/aminal/links/3)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/4)](https://sourcerer.io/fame/liamg/liamg/aminal/links/4)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/5)](https://sourcerer.io/fame/liamg/liamg/aminal/links/5)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/6)](https://sourcerer.io/fame/liamg/liamg/aminal/links/6)[![](https://sourcerer.io/fame/liamg/liamg/aminal/images/7)](https://sourcerer.io/fame/liamg/liamg/aminal/links/7)

View File

@ -27,6 +27,7 @@ type Buffer struct {
selectionComplete bool // whether the selected text can update or whether it is final
selectionExpanded bool // whether the selection to word expansion has already run on this point
selectionClickTime time.Time
defaultCell Cell
}
type Position struct {
@ -37,11 +38,12 @@ type Position struct {
// NewBuffer creates a new terminal buffer
func NewBuffer(viewCols uint16, viewLines uint16, attr CellAttributes) *Buffer {
b := &Buffer{
cursorX: 0,
cursorY: 0,
lines: []Line{},
cursorAttr: attr,
autoWrap: true,
cursorX: 0,
cursorY: 0,
lines: []Line{},
cursorAttr: attr,
autoWrap: true,
defaultCell: Cell{attr: attr},
}
b.SetVerticalMargins(0, uint(viewLines-1))
b.ResizeView(viewCols, viewLines)
@ -511,8 +513,7 @@ func (buffer *Buffer) InsertBlankCharacters(count int) {
index := int(buffer.RawLine())
for i := 0; i < count; i++ {
cells := buffer.lines[index].cells
c := Cell{}
buffer.lines[index].cells = append(cells[:buffer.cursorX], append([]Cell{c}, cells[buffer.cursorX:]...)...)
buffer.lines[index].cells = append(cells[:buffer.cursorX], append([]Cell{buffer.defaultCell}, cells[buffer.cursorX:]...)...)
}
}
@ -610,20 +611,10 @@ func (buffer *Buffer) ReverseIndex() {
func (buffer *Buffer) Write(runes ...rune) {
// scroll to bottom on input
inc := true
buffer.scrollLinesFromBottom = 0
for _, r := range runes {
if r == 0x0a {
buffer.NewLine()
continue
} else if r == 0x0d {
buffer.CarriageReturn()
continue
} else if r == 0x9 {
buffer.Tab()
continue
}
line := buffer.getCurrentLine()
if buffer.replaceMode {
@ -634,7 +625,7 @@ func (buffer *Buffer) Write(runes ...rune) {
}
for int(buffer.CursorColumn()) >= len(line.cells) {
line.cells = append(line.cells, NewBackgroundCell(buffer.cursorAttr.BgColour))
line.cells = append(line.cells, buffer.defaultCell)
}
line.cells[buffer.cursorX].attr = buffer.cursorAttr
line.cells[buffer.cursorX].setRune(r)
@ -665,31 +656,23 @@ func (buffer *Buffer) Write(runes ...rune) {
} else {
for int(buffer.CursorColumn()) >= len(line.cells) {
line.cells = append(line.cells, NewBackgroundCell(buffer.cursorAttr.BgColour))
line.cells = append(line.cells, buffer.defaultCell)
}
cell := &line.cells[buffer.CursorColumn()]
cell.setRune(r)
cell.attr = buffer.cursorAttr
}
if inc {
buffer.incrementCursorPosition()
}
buffer.incrementCursorPosition()
}
}
func (buffer *Buffer) incrementCursorPosition() {
defer buffer.emitDisplayChange()
// we can increment one column past the end of the line.
// this is effectively the beginning of the next line, except when we \r etc.
if buffer.CursorColumn() < buffer.Width() { // if not at end of line
if buffer.CursorColumn() < buffer.Width() {
buffer.cursorX++
}
}
@ -708,7 +691,6 @@ func (buffer *Buffer) Backspace() {
}
func (buffer *Buffer) CarriageReturn() {
defer buffer.emitDisplayChange()
for {
line := buffer.getCurrentLine()
@ -726,19 +708,14 @@ func (buffer *Buffer) CarriageReturn() {
}
func (buffer *Buffer) Tab() {
defer buffer.emitDisplayChange()
tabSize := 4
shift := int(buffer.cursorX-1) % tabSize
if shift == 0 {
shift = tabSize
}
shift := tabSize - (int(buffer.cursorX+1) % tabSize)
for i := 0; i < shift; i++ {
buffer.Write(' ')
}
}
func (buffer *Buffer) NewLine() {
defer buffer.emitDisplayChange()
buffer.cursorX = 0
buffer.Index()

View File

@ -9,12 +9,49 @@ import (
"github.com/stretchr/testify/assert"
)
func TestTabbing(t *testing.T) {
b := NewBuffer(30, 3, CellAttributes{})
b.Write([]rune("hello")...)
b.Tab()
b.Write([]rune("x")...)
b.Tab()
b.Write([]rune("goodbye")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hell")...)
b.Tab()
b.Write([]rune("xxx")...)
b.Tab()
b.Write([]rune("good")...)
b.CarriageReturn()
b.NewLine()
expected := `
hello x goodbye
hell xxx good
`
lines := b.GetVisibleLines()
strs := []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, strings.TrimSpace(expected), strings.Join(strs, "\n"))
}
func TestOffsets(t *testing.T) {
b := NewBuffer(10, 3, CellAttributes{})
b.Write([]rune("hello\r\n")...)
b.Write([]rune("hello\r\n")...)
b.Write([]rune("hello\r\n")...)
b.Write([]rune("hello\r\n")...)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello")...)
assert.Equal(t, uint16(10), b.ViewWidth())
assert.Equal(t, uint16(10), b.Width())
@ -84,14 +121,14 @@ func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
b.Write('a', 'b', 'c')
assert.Equal(t, uint16(3), b.cursorX)
assert.Equal(t, uint16(0), b.cursorY)
b.Write(0x0a)
b.NewLine()
assert.Equal(t, uint16(0), b.cursorX)
assert.Equal(t, uint16(1), b.cursorY)
b.Write('d', 'e', 'f')
assert.Equal(t, uint16(3), b.cursorX)
assert.Equal(t, uint16(1), b.cursorY)
b.Write(0x0a)
b.NewLine()
assert.Equal(t, uint16(0), b.cursorX)
assert.Equal(t, uint16(2), b.cursorY)
@ -114,11 +151,11 @@ func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
*/
b.Write('a', 'b', 'c', 'd')
b.Write(0x0a)
b.NewLine()
b.Write('e', 'f')
b.Write(0x0a)
b.Write(0x0a)
b.Write(0x0a)
b.NewLine()
b.NewLine()
b.NewLine()
b.Write('z')
assert.Equal(t, "abc", b.lines[0].String())
@ -170,19 +207,45 @@ func TestMovePosition(t *testing.T) {
func TestVisibleLines(t *testing.T) {
b := NewBuffer(80, 10, CellAttributes{})
b.Write([]rune("hello 1\r\n")...)
b.Write([]rune("hello 2\r\n")...)
b.Write([]rune("hello 3\r\n")...)
b.Write([]rune("hello 4\r\n")...)
b.Write([]rune("hello 5\r\n")...)
b.Write([]rune("hello 6\r\n")...)
b.Write([]rune("hello 7\r\n")...)
b.Write([]rune("hello 8\r\n")...)
b.Write([]rune("hello 9\r\n")...)
b.Write([]rune("hello 10\r\n")...)
b.Write([]rune("hello 11\r\n")...)
b.Write([]rune("hello 12\r\n")...)
b.Write([]rune("hello 13\r\n")...)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 2")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 3")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 4")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 5")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 6")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 7")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 8")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 9")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 10")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 11")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 12")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 13")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 14")...)
lines := b.GetVisibleLines()
@ -194,9 +257,13 @@ func TestVisibleLines(t *testing.T) {
func TestClearWithoutFullView(t *testing.T) {
b := NewBuffer(80, 10, CellAttributes{})
b.Write([]rune("hello 1\r\n")...)
b.Write([]rune("hello 2\r\n")...)
b.Write([]rune("hello 3")...)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.Clear()
lines := b.GetVisibleLines()
for _, line := range lines {
@ -206,14 +273,28 @@ func TestClearWithoutFullView(t *testing.T) {
func TestClearWithFullView(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello 1\r\n")...)
b.Write([]rune("hello 2\r\n")...)
b.Write([]rune("hello 3\r\n")...)
b.Write([]rune("hello 4\r\n")...)
b.Write([]rune("hello 5\r\n")...)
b.Write([]rune("hello 6\r\n")...)
b.Write([]rune("hello 7\r\n")...)
b.Write([]rune("hello 8\r\n")...)
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("hello 1")...)
b.Clear()
lines := b.GetVisibleLines()
for _, line := range lines {
@ -241,7 +322,10 @@ func TestCarriageReturnOnFullLine(t *testing.T) {
func TestCarriageReturnOnFullLastLine(t *testing.T) {
b := NewBuffer(20, 2, CellAttributes{})
b.Write([]rune("\nabcdeabcdeabcdeabcde\rxxxxxxxxxxxxxxxxxxxx")...)
b.NewLine()
b.Write([]rune("abcdeabcdeabcdeabcde")...)
b.CarriageReturn()
b.Write([]rune("xxxxxxxxxxxxxxxxxxxx")...)
lines := b.GetVisibleLines()
assert.Equal(t, "", lines[0].String())
assert.Equal(t, "xxxxxxxxxxxxxxxxxxxx", lines[1].String())
@ -249,7 +333,10 @@ func TestCarriageReturnOnFullLastLine(t *testing.T) {
func TestCarriageReturnOnWrappedLine(t *testing.T) {
b := NewBuffer(80, 6, CellAttributes{})
b.Write([]rune("hello!\rsecret")...)
b.Write([]rune("hello!")...)
b.CarriageReturn()
b.Write([]rune("secret")...)
lines := b.GetVisibleLines()
assert.Equal(t, "secret", lines[0].String())
}
@ -257,14 +344,22 @@ func TestCarriageReturnOnWrappedLine(t *testing.T) {
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
b := NewBuffer(6, 10, CellAttributes{})
b.cursorY = 3
b.Write('\r')
b.CarriageReturn()
assert.Equal(t, uint16(0), b.cursorX)
assert.Equal(t, uint16(3), b.cursorY)
}
func TestGetCell(t *testing.T) {
b := NewBuffer(80, 20, CellAttributes{})
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
b.Write([]rune("Hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("there")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("something...")...)
cell := b.GetCell(8, 2)
require.NotNil(t, cell)
assert.Equal(t, 'g', cell.Rune())
@ -272,7 +367,17 @@ func TestGetCell(t *testing.T) {
func TestGetCellWithHistory(t *testing.T) {
b := NewBuffer(80, 2, CellAttributes{})
b.Write([]rune("Hello\r\nthere\r\nsomething...")...)
b.Write([]rune("Hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("there")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("something...")...)
cell := b.GetCell(8, 1)
require.NotNil(t, cell)
assert.Equal(t, 'g', cell.Rune())
@ -301,7 +406,35 @@ func TestCursorPositionQuerying(t *testing.T) {
func TestRawPositionQuerying(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("a\r\na\r\na\r\na\r\na\r\na\r\na\r\na\r\na\r\na")...)
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("a")...)
b.cursorX = 3
b.cursorY = 4
assert.Equal(t, uint64(9), b.RawLine())
@ -310,7 +443,10 @@ func TestRawPositionQuerying(t *testing.T) {
// CSI 2 K
func TestEraseLine(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello, this is a test\r\nthis line should be deleted")...)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("this line should be deleted")...)
b.EraseLine()
assert.Equal(t, "hello, this is a test", b.lines[0].String())
assert.Equal(t, "", b.lines[1].String())
@ -319,7 +455,11 @@ func TestEraseLine(t *testing.T) {
// CSI 1 K
func TestEraseLineToCursor(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello, this is a test\r\ndeleted")...)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("deleted")...)
b.MovePosition(-3, 0)
b.EraseLineToCursor()
assert.Equal(t, "hello, this is a test", b.lines[0].String())
@ -329,7 +469,10 @@ func TestEraseLineToCursor(t *testing.T) {
// CSI 0 K
func TestEraseLineAfterCursor(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello, this is a test\r\ndeleted")...)
b.Write([]rune("hello, this is a test")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("deleted")...)
b.MovePosition(-3, 0)
b.EraseLineFromCursor()
assert.Equal(t, "hello, this is a test", b.lines[0].String())
@ -337,7 +480,13 @@ func TestEraseLineAfterCursor(t *testing.T) {
}
func TestEraseDisplay(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello\r\nasdasd\r\nthing")...)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("asdasd")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("thing")...)
b.MovePosition(2, 1)
b.EraseDisplay()
lines := b.GetVisibleLines()
@ -347,7 +496,13 @@ func TestEraseDisplay(t *testing.T) {
}
func TestEraseDisplayToCursor(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello\r\nasdasd\r\nthing")...)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("asdasd")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("thing")...)
b.MovePosition(-2, 0)
b.EraseDisplayToCursor()
lines := b.GetVisibleLines()
@ -359,7 +514,13 @@ func TestEraseDisplayToCursor(t *testing.T) {
func TestEraseDisplayFromCursor(t *testing.T) {
b := NewBuffer(80, 5, CellAttributes{})
b.Write([]rune("hello\r\nasdasd\r\nthings")...)
b.Write([]rune("hello")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("asdasd")...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune("things")...)
b.MovePosition(-3, -1)
b.EraseDisplayFromCursor()
lines := b.GetVisibleLines()
@ -381,9 +542,12 @@ func TestHorizontalResizeView(t *testing.T) {
b := NewBuffer(80, 10, CellAttributes{})
// 60 characters
b.Write([]rune(
`hellohellohellohellohellohellohellohellohellohellohellohello
goodbyegoodbye`)...)
b.Write([]rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
b.CarriageReturn()
b.NewLine()
b.Write([]rune(`goodbyegoodbye`)...)
require.Equal(t, uint16(14), b.cursorX)
require.Equal(t, uint16(1), b.cursorY)

View File

@ -40,10 +40,16 @@ func (cell *Cell) Rune() rune {
}
func (cell *Cell) Fg() [3]float32 {
if cell.Attr().Reverse {
return cell.attr.BgColour
}
return cell.attr.FgColour
}
func (cell *Cell) Bg() [3]float32 {
if cell.Attr().Reverse {
return cell.attr.FgColour
}
return cell.attr.BgColour
}

30
build.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
set -eux
version="$1"
if [[ "$version" == "" ]]; then
version=`git describe --tags`
fi
if [[ "$version" == "" ]]; then
echo "Error: Cannot determine version"
exit 1
fi
export GOPATH="/tmp/.gobuild"
SRCDIR="${GOPATH}/src/github.com/liamg/aminal"
[ -d ${GOPATH} ] && rm -rf ${GOPATH}
mkdir -p ${GOPATH}/{src,pkg,bin}
mkdir -p ${SRCDIR}
cp -r . ${SRCDIR}
(
echo ${GOPATH}
cd ${SRCDIR}
go build -ldflags "-X github.com/liamg/aminal/version.Version=$version"
)
cp ${SRCDIR}/aminal ./aminal
rm -rf /tmp/.gobuild

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/version"
@ -48,10 +49,16 @@ func loadConfigFile() *config.Config {
return &config.DefaultConfig
}
places := []string{
fmt.Sprintf("%s/.aminal.toml", home),
places := []string{}
xdgHome := os.Getenv("XDG_CONFIG_HOME")
if xdgHome != "" {
places = append(places, fmt.Sprintf("%s/aminal/config.toml", xdgHome))
}
places = append(places, fmt.Sprintf("%s/.config/aminal/config.toml", home))
places = append(places, fmt.Sprintf("%s/.aminal.toml", home))
for _, place := range places {
if b, err := ioutil.ReadFile(place); err == nil {
if c, err := config.Parse(b); err == nil {
@ -62,10 +69,18 @@ func loadConfigFile() *config.Config {
}
}
parts := strings.Split(places[0], string(os.PathSeparator))
path := strings.Join(parts[0:len(parts)-1], string(os.PathSeparator))
err := os.MkdirAll(path, 0744)
if err != nil {
panic(err)
}
if b, err := config.DefaultConfig.Encode(); err != nil {
fmt.Printf("Failed to encode config file: %s\n", err)
} else {
if err := ioutil.WriteFile(fmt.Sprintf("%s/.aminal.toml", home), b, 0644); err != nil {
if err := ioutil.WriteFile(fmt.Sprintf("%s/config.toml", path), b, 0644); err != nil {
fmt.Printf("Failed to encode config file: %s\n", err)
}
}

View File

@ -9,7 +9,7 @@ import (
type KeyCombination struct {
mods glfw.ModifierKey
key glfw.Key
char rune
}
type KeyMod string
@ -28,41 +28,11 @@ var modMap = map[KeyMod]glfw.ModifierKey{
super: glfw.ModSuper,
}
var keyMap = map[string]glfw.Key{
"a": glfw.KeyA,
"b": glfw.KeyB,
"c": glfw.KeyC,
"d": glfw.KeyD,
"e": glfw.KeyE,
"f": glfw.KeyF,
"g": glfw.KeyG,
"h": glfw.KeyH,
"i": glfw.KeyI,
"j": glfw.KeyJ,
"k": glfw.KeyK,
"l": glfw.KeyL,
"m": glfw.KeyM,
"n": glfw.KeyN,
"o": glfw.KeyO,
"p": glfw.KeyP,
"q": glfw.KeyQ,
"r": glfw.KeyR,
"s": glfw.KeyS,
"t": glfw.KeyT,
"u": glfw.KeyU,
"v": glfw.KeyV,
"w": glfw.KeyW,
"x": glfw.KeyX,
"y": glfw.KeyY,
"z": glfw.KeyZ,
";": glfw.KeySemicolon,
}
// keyStr e.g. "ctrl + alt + a"
func parseKeyCombination(keyStr string) (*KeyCombination, error) {
var mods glfw.ModifierKey
var key *glfw.Key
var key rune
keys := strings.Split(keyStr, "+")
for _, k := range keys {
@ -72,19 +42,15 @@ func parseKeyCombination(keyStr string) (*KeyCombination, error) {
mods = mods + mod
continue
}
mappedKey, ok := keyMap[k]
if ok {
if key != nil {
return nil, fmt.Errorf("Multiple non-modifier keys specified in keyboard shortcut")
}
key = &mappedKey
continue
if key > 0 {
return nil, fmt.Errorf("Multiple non-modifier keys specified in keyboard shortcut")
}
return nil, fmt.Errorf("Unknown key '%s' in configured keyboard shortcut", k)
key = rune(k[0])
}
if key == nil {
if key == 0 {
return nil, fmt.Errorf("No non-modifier key specified in keyboard shortcut")
}
@ -94,12 +60,12 @@ func parseKeyCombination(keyStr string) (*KeyCombination, error) {
return &KeyCombination{
mods: mods,
key: *key,
char: key,
}, nil
}
func (combi KeyCombination) Match(pressedMods glfw.ModifierKey, pressedKey glfw.Key) bool {
return pressedKey == combi.key && pressedMods == combi.mods
func (combi KeyCombination) Match(pressedMods glfw.ModifierKey, pressedChar rune) bool {
return pressedChar == combi.char && pressedMods == combi.mods
}
func (keyMapConfig KeyMappingConfig) GenerateActionMap() (map[UserAction]*KeyCombination, error) {

View File

@ -14,14 +14,14 @@ func TestKeyCombinations(t *testing.T) {
require.Nil(t, err)
require.NotNil(t, combi)
assert.Equal(t, glfw.KeyA, combi.key)
assert.Equal(t, 'a', combi.char)
assert.Equal(t, glfw.ModControl+glfw.ModAlt, combi.mods)
assert.True(t, combi.Match(glfw.ModControl^glfw.ModAlt, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt, glfw.KeyB))
assert.False(t, combi.Match(glfw.ModControl, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModAlt, glfw.KeyA))
assert.False(t, combi.Match(0, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt^glfw.ModShift, glfw.KeyA))
assert.True(t, combi.Match(glfw.ModControl^glfw.ModAlt, 'a'))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt, 'b'))
assert.False(t, combi.Match(glfw.ModControl, 'b'))
assert.False(t, combi.Match(glfw.ModAlt, 'd'))
assert.False(t, combi.Match(0, 'e'))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt^glfw.ModShift, 'f'))
}

View File

@ -2,14 +2,15 @@ package glfont
import (
"fmt"
"image"
"image/draw"
"io"
"github.com/go-gl/gl/all-core/gl"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"image"
"image/draw"
"io"
)
const DPI = 72
@ -198,18 +199,18 @@ func (f *Font) Size(text string) (float32, float32) {
return width, height
}
func(f *Font) MaxSize() (float32, float32){
b:= f.ttf.Bounds(fixed.Int26_6(f.scale))
return float32(b.Max.X - b.Min.X),float32(b.Max.Y - b.Min.Y)
func (f *Font) MaxSize() (float32, float32) {
b := f.ttf.Bounds(fixed.Int26_6(f.scale))
return float32(b.Max.X - b.Min.X), float32(b.Max.Y - b.Min.Y)
}
func(f *Font) MinY() float32 {
b:= f.ttf.Bounds(fixed.Int26_6(f.scale))
func (f *Font) MinY() float32 {
b := f.ttf.Bounds(fixed.Int26_6(f.scale))
return float32(b.Min.Y)
}
func(f *Font) MaxY() float32 {
b:= f.ttf.Bounds(fixed.Int26_6(f.scale))
func (f *Font) MaxY() float32 {
b := f.ttf.Bounds(fixed.Int26_6(f.scale))
return float32(b.Max.Y)
}

View File

@ -82,7 +82,7 @@ void main()
var vertexFontShader = `#version 150 core
//vertex position
//vertex position
in vec2 vert;
//pass through to fragTexCoord

28
gui/darwin_opengl.go Normal file
View File

@ -0,0 +1,28 @@
//+build darwin
package gui
/*
#cgo darwin CFLAGS: -x objective-c -Wno-deprecated-declarations
#cgo darwin LDFLAGS: -framework Foundation
#include <Cocoa/Cocoa.h>
void cocoa_update_nsgl_context(void* id) {
NSOpenGLContext *ctx = id;
[ctx update];
}
*/
import "C"
import (
"github.com/go-gl/glfw/v3.2/glfw"
"unsafe"
)
var nsglContextUpdateCounter int
func UpdateNSGLContext(window *glfw.Window) {
if nsglContextUpdateCounter < 2 {
ctx := window.GetNSGLContext()
C.cocoa_update_nsgl_context(unsafe.Pointer(ctx))
nsglContextUpdateCounter++
}
}

View File

@ -28,16 +28,16 @@ func (a *annotation) render(gui *GUI) {
}
cell := cells[x]
var colour *[3]float32
var colour [3]float32 = cell.Fg()
var alpha float32 = 0.6
if y == int(a.hint.StartY) {
if x >= int(a.hint.StartX) && x <= int(a.hint.StartX+uint16(len(a.hint.Word))) {
colour = &[3]float32{0.2, 1.0, 0.2}
colour = [3]float32{0.2, 1.0, 0.2}
alpha = 1.0
}
}
gui.renderer.DrawCellText(cell, uint(x), uint(y), alpha, colour)
gui.renderer.DrawCellText(string(cell.Rune()), uint(x), uint(y), alpha, colour, cell.Attr().Bold)
}
}

View File

@ -5,73 +5,25 @@ import "github.com/liamg/aminal/glfont"
type FontMap struct {
defaultFont *glfont.Font
defaultBoldFont *glfont.Font
runeMap map[rune]*glfont.Font
ranges map[runeRange]*glfont.Font
}
type runeRange struct {
start rune
end rune // inclusive
}
func NewFontMap(defaultFont *glfont.Font, defaultBoldFont *glfont.Font) *FontMap {
return &FontMap{
defaultFont: defaultFont,
defaultBoldFont: defaultBoldFont,
runeMap: map[rune]*glfont.Font{},
ranges: map[runeRange]*glfont.Font{},
}
}
func (fm *FontMap) UpdateResolution(w int, h int) {
fm.defaultFont.UpdateResolution(w, h)
fm.defaultBoldFont.UpdateResolution(w, h)
for _, f := range fm.ranges {
f.UpdateResolution(w, h)
}
}
func (fm *FontMap) findOverride(r rune) *glfont.Font {
override, ok := fm.runeMap[r]
if ok {
return override
}
for rr, f := range fm.ranges {
if r >= rr.start && r <= rr.end {
fm.runeMap[r] = f
return f
}
}
return nil
}
func (fm *FontMap) setOverrideRange(start rune, end rune, font *glfont.Font) {
fm.ranges[runeRange{start: start, end: end}] = font
}
func (fm *FontMap) GetFont(r rune) *glfont.Font {
if r <= 0xff {
return fm.defaultFont
}
if f := fm.findOverride(r); f != nil {
return f
}
func (fm *FontMap) DefaultFont() *glfont.Font {
return fm.defaultFont
}
func (fm *FontMap) GetBoldFont(r rune) *glfont.Font {
if r <= 0xff {
return fm.defaultBoldFont
}
if f := fm.findOverride(r); f != nil {
return f
}
func (fm *FontMap) BoldFont() *glfont.Font {
return fm.defaultBoldFont
}

View File

@ -39,12 +39,11 @@ func (gui *GUI) loadFonts() error {
if gui.fontMap == nil {
gui.fontMap = NewFontMap(defaultFont, boldFont)
}else{
} else {
gui.fontMap.defaultFont = defaultFont
gui.fontMap.defaultBoldFont = boldFont
}
// add special non-ascii fonts here
return nil

View File

@ -6,6 +6,7 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/go-gl/gl/all-core/gl"
@ -33,6 +34,7 @@ type GUI struct {
terminalAlpha float32
showDebugInfo bool
keyboardShortcuts map[config.UserAction]*config.KeyCombination
resizeLock *sync.Mutex
}
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) (*GUI, error) {
@ -51,6 +53,7 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
fontScale: 14.0,
terminalAlpha: 1,
keyboardShortcuts: shortcuts,
resizeLock: &sync.Mutex{},
}, nil
}
@ -65,6 +68,9 @@ func (gui *GUI) scale() float32 {
// can only be called on OS thread
func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.resizeLock.Lock()
defer gui.resizeLock.Unlock()
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
gui.width = width
@ -91,6 +97,8 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.logger.Debugf("Resize complete!")
gui.redraw(buffer.NewBackgroundCell(gui.config.ColourScheme.Background))
gui.window.SwapBuffers()
}
func (gui *GUI) getTermSize() (uint, uint) {
@ -205,6 +213,7 @@ func (gui *GUI) Render() error {
}()
startTime := time.Now()
showMessage := true
for !gui.window.ShouldClose() {
@ -218,66 +227,7 @@ func (gui *GUI) Render() error {
if gui.terminal.CheckDirty() {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
lines := gui.terminal.GetVisibleLines()
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
for y := 0; y < lineCount; y++ {
for x := 0; x < colCount; x++ {
cell := defaultCell
if y < len(lines) {
cells := lines[y].Cells()
if x < len(cells) {
cell = cells[x]
}
}
cursor := false
if gui.terminal.Modes().ShowCursor {
cx := uint(gui.terminal.GetLogicalCursorX())
cy := uint(gui.terminal.GetLogicalCursorY())
cy = cy + uint(gui.terminal.GetScrollOffset())
cursor = cx == uint(x) && cy == uint(y)
}
var colour *config.Colour
if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y)) {
colour = &gui.config.ColourScheme.Selection
}
if cell.Image() != nil {
gui.renderer.DrawCellImage(cell, uint(x), uint(y))
} else {
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor, colour, false)
}
}
}
for y := 0; y < lineCount; y++ {
for x := 0; x < colCount; x++ {
cell := defaultCell
hasText := false
if y < len(lines) {
cells := lines[y].Cells()
if x < len(cells) {
cell = cells[x]
if cell.Rune() != 0 && cell.Rune() != 32 {
hasText = true
}
}
}
if hasText {
gui.renderer.DrawCellText(cell, uint(x), uint(y), 1.0, nil)
}
}
}
gui.renderOverlay()
gui.redraw(defaultCell)
if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
@ -295,26 +245,29 @@ Buffer Size: %d lines
)
}
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.SetDirty)
_, h := gui.terminal.GetSize()
var msg string
if version.Version == "" {
msg = "You are using a development build of Aminal."
if showMessage {
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
time.AfterFunc(time.Second, gui.terminal.SetDirty)
_, h := gui.terminal.GetSize()
var msg string
if version.Version == "" {
msg = "You are using a development build of Aminal."
} else {
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
}
gui.textbox(
2,
uint16(h-3),
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
[3]float32{1, 1, 1},
[3]float32{0, 0.5, 0},
)
} else {
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
showMessage = false
}
gui.textbox(
2,
uint16(h-3),
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
[3]float32{1, 1, 1},
[3]float32{0, 0.5, 0},
)
}
gui.window.SwapBuffers()
gui.SwapBuffers()
}
}
@ -324,6 +277,93 @@ Buffer Size: %d lines
}
func (gui *GUI) redraw(defaultCell buffer.Cell) {
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
lines := gui.terminal.GetVisibleLines()
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
cx := uint(gui.terminal.GetLogicalCursorX())
cy := uint(gui.terminal.GetLogicalCursorY()) + uint(gui.terminal.GetScrollOffset())
var colour *config.Colour
for y := 0; y < lineCount; y++ {
if y < len(lines) {
cells := lines[y].Cells()
for x := 0; x < colCount; x++ {
cursor := false
if gui.terminal.Modes().ShowCursor {
cursor = cx == uint(x) && cy == uint(y)
}
if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y)) {
colour = &gui.config.ColourScheme.Selection
} else {
colour = nil
}
cell := defaultCell
if colour != nil || cursor || x < len(cells) {
if x < len(cells) {
cell = cells[x]
if cell.Image() != nil {
gui.renderer.DrawCellImage(cell, uint(x), uint(y))
continue
}
}
gui.renderer.DrawCellBg(cell, uint(x), uint(y), cursor, colour, false)
}
}
}
}
for y := 0; y < lineCount; y++ {
if y < len(lines) {
bufStr := ""
bold := false
dim := false
col := 0
colour := [3]float32{0, 0, 0}
cells := lines[y].Cells()
for x := 0; x < colCount; x++ {
if x < len(cells) {
cell := cells[x]
if bufStr != "" && (cell.Attr().Dim != dim || cell.Attr().Bold != bold || colour != cell.Fg()) {
var alpha float32 = 1.0
if dim {
alpha = 0.5
}
gui.renderer.DrawCellText(bufStr, uint(col), uint(y), alpha, colour, bold)
col = x
bufStr = ""
}
dim = cell.Attr().Dim
colour = cell.Fg()
bold = cell.Attr().Bold
r := cell.Rune()
if r == 0 {
r = ' '
}
bufStr += string(r)
}
}
if bufStr != "" {
var alpha float32 = 1.0
if dim {
alpha = 0.5
}
gui.renderer.DrawCellText(bufStr, uint(col), uint(y), alpha, colour, bold)
}
}
}
gui.renderOverlay()
}
func (gui *GUI) createWindow() (*glfw.Window, error) {
if err := glfw.Init(); err != nil {
return nil, fmt.Errorf("Failed to initialise GLFW: %s", err)
@ -352,7 +392,7 @@ func (gui *GUI) createWindow() (*glfw.Window, error) {
window, err = gui.createWindowWithOpenGLVersion(v[0], v[1])
if err != nil {
gui.logger.Warnf("Failed to create window: %s. Will attempt older version...", err)
}else{
} else {
break
}
}
@ -369,7 +409,7 @@ func (gui *GUI) createWindow() (*glfw.Window, error) {
return window, nil
}
func(gui *GUI) createWindowWithOpenGLVersion(major int, minor int) (*glfw.Window, error) {
func (gui *GUI) createWindowWithOpenGLVersion(major int, minor int) (*glfw.Window, error) {
glfw.WindowHint(glfw.ContextVersionMajor, major)
glfw.WindowHint(glfw.ContextVersionMinor, minor)
@ -377,13 +417,13 @@ func(gui *GUI) createWindowWithOpenGLVersion(major int, minor int) (*glfw.Window
window, err := glfw.CreateWindow(gui.width, gui.height, "Terminal", nil, nil)
if err != nil {
e := err.Error()
if i := strings.Index(e, ", got version "); i > - 1 {
if i := strings.Index(e, ", got version "); i > -1 {
v := strings.Split(strings.TrimSpace(e[i+14:]), ".")
if len(v) == 2 {
major, err := strconv.Atoi(v[0])
if err == nil {
if minor, err := strconv.Atoi(v[1]); err == nil {
return gui.createWindowWithOpenGLVersion(major, minor)
maj, mjErr := strconv.Atoi(v[0])
if mjErr == nil {
if min, miErr := strconv.Atoi(v[1]); miErr == nil {
return gui.createWindowWithOpenGLVersion(maj, min)
}
}
@ -395,6 +435,7 @@ func(gui *GUI) createWindowWithOpenGLVersion(major int, minor int) (*glfw.Window
return window, nil
}
// initOpenGL initializes OpenGL and returns an intiialized program.
func (gui *GUI) createProgram() (uint32, error) {
if err := gl.Init(); err != nil {
@ -437,3 +478,8 @@ func (gui *GUI) launchTarget(target string) {
gui.logger.Errorf("Failed to launch external command %s: %s", cmd, err)
}
}
func (gui *GUI) SwapBuffers() {
UpdateNSGLContext(gui.window)
gui.window.SwapBuffers()
}

View File

@ -21,6 +21,28 @@ func modsPressed(pressed glfw.ModifierKey, mods ...glfw.ModifierKey) bool {
return pressed == 0
}
func getModStr(mods glfw.ModifierKey) string {
switch true {
case modsPressed(mods, glfw.ModControl, glfw.ModShift, glfw.ModAlt):
return "8"
case modsPressed(mods, glfw.ModControl, glfw.ModAlt):
return "7"
case modsPressed(mods, glfw.ModControl, glfw.ModShift):
return "6"
case modsPressed(mods, glfw.ModControl):
return "5"
case modsPressed(mods, glfw.ModAlt, glfw.ModShift):
return "4"
case modsPressed(mods, glfw.ModAlt):
return "3"
case modsPressed(mods, glfw.ModShift):
return "2"
}
return ""
}
func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
if action == glfw.Repeat || action == glfw.Press {
@ -31,128 +53,33 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
}
}
for userAction, shortcut := range gui.keyboardShortcuts {
if shortcut.Match(mods, key) {
f, ok := actionMap[userAction]
if ok {
f(gui)
break
// get key name to handle alternative keyboard layouts
name := glfw.GetKeyName(key, scancode)
if len(name) == 1 {
r := rune(name[0])
for userAction, shortcut := range gui.keyboardShortcuts {
if shortcut.Match(mods, r) {
f, ok := actionMap[userAction]
if ok {
f(gui)
break
}
}
}
switch key {
case glfw.KeyD:
case glfw.KeyG:
case glfw.KeyR:
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
case glfw.KeySemicolon:
gui.config.Slomo = !gui.config.Slomo
// standard ctrl codes e.g. ^C
if modsPressed(mods, glfw.ModControl) {
if r >= 97 && r < 123 {
gui.terminal.Write([]byte{byte(r) - 96})
return
} else if r >= 65 && r < 91 {
gui.terminal.Write([]byte{byte(r) - 64})
return
}
}
}
modStr := ""
switch true {
case modsPressed(mods, glfw.ModControl, glfw.ModShift, glfw.ModAlt):
modStr = "8"
case modsPressed(mods, glfw.ModControl, glfw.ModAlt):
modStr = "7"
case modsPressed(mods, glfw.ModControl, glfw.ModShift):
modStr = "6"
case modsPressed(mods, glfw.ModControl):
modStr = "5"
switch key {
case glfw.KeyA:
gui.terminal.Write([]byte{0x1})
return
case glfw.KeyB:
gui.terminal.Write([]byte{0x2})
return
case glfw.KeyC: // ctrl^c
gui.terminal.Write([]byte{0x3}) // send EOT
return
case glfw.KeyD:
gui.terminal.Write([]byte{0x4}) // send EOT
return
case glfw.KeyE:
gui.terminal.Write([]byte{0x5})
return
case glfw.KeyF:
gui.terminal.Write([]byte{0x6})
return
case glfw.KeyG:
gui.terminal.Write([]byte{0x7})
return
case glfw.KeyH:
gui.terminal.Write([]byte{0x08})
return
case glfw.KeyI:
gui.terminal.Write([]byte{0x9})
return
case glfw.KeyJ:
gui.terminal.Write([]byte{0x0a})
return
case glfw.KeyK:
gui.terminal.Write([]byte{0x0b})
return
case glfw.KeyL:
gui.terminal.Write([]byte{0x0c})
return
case glfw.KeyM:
gui.terminal.Write([]byte{0x0d})
return
case glfw.KeyN:
gui.terminal.Write([]byte{0x0e})
return
case glfw.KeyO:
gui.terminal.Write([]byte{0x0f})
return
case glfw.KeyP:
gui.terminal.Write([]byte{0x10})
return
case glfw.KeyQ:
gui.terminal.Write([]byte{0x11})
return
case glfw.KeyR:
gui.terminal.Write([]byte{0x12})
return
case glfw.KeyS:
gui.terminal.Write([]byte{0x13})
return
case glfw.KeyT:
gui.terminal.Write([]byte{0x14})
return
case glfw.KeyU:
gui.terminal.Write([]byte{0x15})
return
case glfw.KeyV:
gui.terminal.Write([]byte{0x16})
return
case glfw.KeyW:
gui.terminal.Write([]byte{0x17})
return
case glfw.KeyX:
gui.terminal.Write([]byte{0x18})
return
case glfw.KeyY:
gui.terminal.Write([]byte{0x19})
return
case glfw.KeyZ:
gui.terminal.Write([]byte{0x1a})
return
}
case modsPressed(mods, glfw.ModAlt, glfw.ModShift):
modStr = "4"
case modsPressed(mods, glfw.ModAlt):
modStr = "3"
case modsPressed(mods, glfw.ModShift):
modStr = "2"
}
modStr := getModStr(mods)
switch key {
case glfw.KeyF1:
@ -240,10 +167,14 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
'3', '~',
})
case glfw.KeyHome:
if modStr == "" {
gui.terminal.Write([]byte("\x1b[1~"))
if gui.terminal.IsApplicationCursorKeysModeEnabled() {
if modStr == "" {
gui.terminal.Write([]byte("\x1b[1~"))
} else {
gui.terminal.Write([]byte(fmt.Sprintf("\x1b[1;%s~", modStr)))
}
} else {
gui.terminal.Write([]byte(fmt.Sprintf("\x1b[1;%s~", modStr)))
gui.terminal.Write([]byte("\x1b[H"))
}
case glfw.KeyEnd:
if modStr == "" {
@ -276,17 +207,9 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
})
}
case glfw.KeyTab:
if gui.terminal.IsApplicationCursorKeysModeEnabled() {
gui.terminal.Write([]byte{
0x1b,
'O',
'I',
})
} else {
gui.terminal.Write([]byte{
0x09,
})
}
gui.terminal.Write([]byte{
0x09,
})
case glfw.KeyEnter:
gui.terminal.Write([]byte{
0x0d,
@ -304,7 +227,11 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
})
}
case glfw.KeyBackspace:
gui.terminal.Write([]byte{0x08})
if modsPressed(mods, glfw.ModAlt) {
gui.terminal.Write([]byte{0x17}) // ctrl-w/delete word
} else {
gui.terminal.Write([]byte{0x8})
}
case glfw.KeyUp:
if modStr != "" {
gui.terminal.Write([]byte(fmt.Sprintf("\x1b[1;%sA", modStr)))
@ -379,9 +306,6 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
})
}
}
//gui.logger.Debugf("Key pressed: 0x%X %q", key, string([]byte{byte(key)}))
//gui.terminal.Write([]byte{byte(scancode)})
}
}

9
gui/opengl.go Normal file
View File

@ -0,0 +1,9 @@
// +build !darwin
package gui
import "github.com/go-gl/glfw/v3.2/glfw"
func UpdateNSGLContext(window *glfw.Window) {
}

View File

@ -57,11 +57,10 @@ func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *
halfAreaWidth := float32(r.areaWidth / 2)
halfAreaHeight := float32(r.areaHeight / 2)
x = (x - halfAreaWidth) / halfAreaWidth
y = -(y - ( halfAreaHeight)) / halfAreaHeight
y = -(y - (halfAreaHeight)) / halfAreaHeight
w := r.cellWidth / halfAreaWidth
h := (r.cellHeight ) / halfAreaHeight
h := (r.cellHeight) / halfAreaHeight
rect := &rectangle{
points: []float32{
@ -162,10 +161,10 @@ func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight
r.areaHeight = areaHeight
r.areaX = areaX
r.areaY = areaY
f := r.fontMap.GetFont('X')
f := r.fontMap.DefaultFont()
_, r.cellHeight = f.MaxSize()
r.cellWidth, _ = f.Size("X")
//= f.LineHeight() // includes vertical padding
//= f.LineHeight() // includes vertical padding
r.termCols = uint(math.Floor(float64(float32(r.areaWidth) / r.cellWidth)))
r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight)))
r.rectangles = map[[2]uint]*rectangle{}
@ -179,7 +178,7 @@ func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle {
}
x := float32(float32(col) * r.cellWidth)
y := float32(float32(row) * r.cellHeight) + r.cellHeight
y := float32(float32(row)*r.cellHeight) + r.cellHeight
r.rectangles[[2]uint{col, row}] = r.newRectangle(x, y, r.colourAttr)
return r.rectangles[[2]uint{col, row}]
@ -201,8 +200,6 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
if cursor {
bg = r.config.ColourScheme.Cursor
} else if cell.Attr().Reverse {
bg = cell.Fg()
} else {
bg = cell.Bg()
}
@ -216,32 +213,21 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor
}
func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, alpha float32, colour *[3]float32) {
func (r *OpenGLRenderer) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) {
var fg [3]float32
if colour != nil {
fg = *colour
} else if cell.Attr().Reverse {
fg = cell.Bg()
var f *glfont.Font
if bold {
f = r.fontMap.BoldFont()
} else {
fg = cell.Fg()
f = r.fontMap.DefaultFont()
}
f := r.fontMap.GetFont(cell.Rune())
if cell.Attr().Bold {
f = r.fontMap.GetBoldFont(cell.Rune())
}
if cell.Attr().Dim {
alpha = 0.5 * alpha
}
f.SetColor(fg[0], fg[1], fg[2], alpha)
f.SetColor(colour[0], colour[1], colour[2], alpha)
x := float32(r.areaX) + float32(col)*r.cellWidth
y := float32(r.areaY) + (float32(row+1) * r.cellHeight) + f.MinY()
f.Print(x, y, string(cell.Rune()))
f.Print(x, y, text)
}
func (r *OpenGLRenderer) DrawCellImage(cell buffer.Cell, col uint, row uint) {

View File

@ -24,7 +24,7 @@ const (
smooth in vec3 theColour;
out vec4 outColour;
void main() {
outColour = vec4(theColour, 1.0);
outColour = vec4(theColour, 1.0);
}
` + "\x00"
)

View File

@ -89,7 +89,7 @@ DONE:
x := float32(col) * gui.renderer.cellWidth
f := gui.fontMap.GetFont('X')
f := gui.fontMap.DefaultFont()
f.SetColor(fg[0], fg[1], fg[2], 1)
for i, line := range lines {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
hint.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

14
main.go
View File

@ -31,13 +31,13 @@ func main() {
logger.Fatalf("Failed to allocate pty: %s", err)
}
shellStr, err := loginshell.Shell()
if err != nil {
logger.Fatalf("Failed to ascertain your shell: %s", err)
}
if conf.Shell != "" {
shellStr = conf.Shell
shellStr := conf.Shell
if shellStr == "" {
loginShell, err := loginshell.Shell()
if err != nil {
logger.Fatalf("Failed to ascertain your shell: %s", err)
}
shellStr = loginShell
}
os.Setenv("TERM", "xterm-256color") // controversial! easier than installing terminfo everywhere, but obviously going to be slightly different to xterm functionality, so we'll see...

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

BIN
sixel.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,7 +1,6 @@
package terminal
import (
"context"
"time"
)
@ -27,21 +26,25 @@ var escapeSequenceMap = map[rune]escapeSequenceHandler{
func newLineSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().NewLine()
terminal.isDirty = true
return nil
}
func tabSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().Tab()
terminal.isDirty = true
return nil
}
func carriageReturnSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().CarriageReturn()
terminal.isDirty = true
return nil
}
func backspaceSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().Backspace()
terminal.isDirty = true
return nil
}
@ -65,47 +68,33 @@ func shiftInSequenceHandler(pty chan rune, terminal *Terminal) error {
return nil
}
func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) {
func (terminal *Terminal) processInput(pty chan rune) {
// https://en.wikipedia.org/wiki/ANSI_escape_code
for {
var b rune
select {
case <-terminal.pauseChan:
// @todo alert user when terminal is suspended
terminal.logger.Debugf("Terminal suspended")
<-terminal.resumeChan
case <-ctx.Done():
break
default:
}
for {
if terminal.config.Slomo {
time.Sleep(time.Millisecond * 100)
}
b := <-pty
b = <-pty
terminal.logger.Debugf("0x%q", string(b))
handler, ok := escapeSequenceMap[b]
if ok {
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
if err := handler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err)
}
} else {
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
if b >= 0x20 {
//terminal.logger.Debugf("%c", b)
terminal.ActiveBuffer().Write(b)
} else {
terminal.logger.Error("Non-readable rune received: 0x%X", b)
if b < 0x20 {
if handler, ok := escapeSequenceMap[b]; ok {
//terminal.logger.Debugf("Handling escape sequence: 0x%x", b)
if err := handler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err)
}
terminal.isDirty = true
continue
}
}
//terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
terminal.ActiveBuffer().Write(b)
terminal.isDirty = true
}
}

View File

@ -2,7 +2,6 @@ package terminal
import (
"bufio"
"context"
"fmt"
"io"
"os"
@ -35,7 +34,7 @@ const (
type Terminal struct {
program uint32
buffers []*buffer.Buffer
activeBufferIndex uint8
activeBuffer *buffer.Buffer
lock sync.Mutex
pty *os.File
logger *zap.SugaredLogger
@ -43,8 +42,6 @@ type Terminal struct {
size Winsize
config *config.Config
titleHandlers []chan bool
pauseChan chan bool
resumeChan chan bool
modes Modes
mouseMode MouseMode
bracketedPasteMode bool
@ -87,13 +84,11 @@ func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Termin
logger: logger,
config: config,
titleHandlers: []chan bool{},
pauseChan: make(chan bool, 1),
resumeChan: make(chan bool, 1),
modes: Modes{
ShowCursor: true,
},
}
t.activeBuffer = t.buffers[0]
return t
}
@ -129,32 +124,30 @@ func (terminal *Terminal) GetMouseMode() MouseMode {
}
func (terminal *Terminal) UseMainBuffer() {
terminal.activeBufferIndex = MainBuffer
terminal.activeBuffer = terminal.buffers[MainBuffer]
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
}
func (terminal *Terminal) UseAltBuffer() {
terminal.activeBufferIndex = AltBuffer
terminal.activeBuffer = terminal.buffers[AltBuffer]
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
}
func (terminal *Terminal) UseInternalBuffer() {
terminal.pauseChan <- true
terminal.activeBufferIndex = InternalBuffer
terminal.activeBuffer = terminal.buffers[InternalBuffer]
terminal.SetSize(uint(terminal.size.Width), uint(terminal.size.Height))
}
func (terminal *Terminal) ExitInternalBuffer() {
terminal.activeBufferIndex = terminal.lastBuffer
terminal.resumeChan <- true
terminal.activeBuffer = terminal.buffers[terminal.lastBuffer]
}
func (terminal *Terminal) ActiveBuffer() *buffer.Buffer {
return terminal.buffers[terminal.activeBufferIndex]
return terminal.activeBuffer
}
func (terminal *Terminal) UsingMainBuffer() bool {
return terminal.activeBufferIndex == MainBuffer
return terminal.activeBuffer == terminal.buffers[MainBuffer]
}
func (terminal *Terminal) GetScrollOffset() uint {
@ -255,20 +248,17 @@ func (terminal *Terminal) Read() error {
buffer := make(chan rune, 0xffff)
reader := bufio.NewReader(terminal.pty)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go terminal.processInput(ctx, buffer)
go terminal.processInput(buffer)
for {
r, size, err := reader.ReadRune()
r, _, err := reader.ReadRune()
if err != nil {
if err == io.EOF {
break
}
return err
} else if size > 0 {
buffer <- r
}
buffer <- r
}
//clean exit