diff --git a/.circleci/config.yml b/.circleci/config.yml index 3bd9efe..33a1a90 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,4 +102,5 @@ workflows: tags: only: /^v.*/ branches: - ignore: /.*/ \ No newline at end of file + ignore: /.*/ + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b1b7abb..d27a344 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/Makefile b/Makefile index 0fd0907..ab6de91 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file + ./scripts/package-debian.sh "${CIRCLE_TAG}" bin/linux/${BINARY}-linux-amd64 diff --git a/README.md b/README.md index 5e426d4..52cadab 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/buffer/buffer.go b/buffer/buffer.go index 6324ee4..9b8e23c 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -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() diff --git a/buffer/buffer_test.go b/buffer/buffer_test.go index d62510f..7ee3bfb 100644 --- a/buffer/buffer_test.go +++ b/buffer/buffer_test.go @@ -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) diff --git a/buffer/cell.go b/buffer/cell.go index 4821406..d41cd62 100644 --- a/buffer/cell.go +++ b/buffer/cell.go @@ -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 } diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..6e2e579 --- /dev/null +++ b/build.sh @@ -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 + diff --git a/config.go b/config.go index 21d35c8..f74a935 100644 --- a/config.go +++ b/config.go @@ -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) } } diff --git a/config/keys.go b/config/keys.go index 52ea135..bbd4ce4 100644 --- a/config/keys.go +++ b/config/keys.go @@ -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) { diff --git a/config/keys_test.go b/config/keys_test.go index 1508ce3..6887469 100644 --- a/config/keys_test.go +++ b/config/keys_test.go @@ -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')) } diff --git a/glfont/font.go b/glfont/font.go index fe3c871..f91d749 100644 --- a/glfont/font.go +++ b/glfont/font.go @@ -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) } diff --git a/glfont/shader.go b/glfont/shader.go index 9f23476..175eb0f 100644 --- a/glfont/shader.go +++ b/glfont/shader.go @@ -82,7 +82,7 @@ void main() var vertexFontShader = `#version 150 core -//vertex position +//vertex position in vec2 vert; //pass through to fragTexCoord diff --git a/gui/darwin_opengl.go b/gui/darwin_opengl.go new file mode 100644 index 0000000..7c024e9 --- /dev/null +++ b/gui/darwin_opengl.go @@ -0,0 +1,28 @@ +//+build darwin + +package gui + +/* +#cgo darwin CFLAGS: -x objective-c -Wno-deprecated-declarations +#cgo darwin LDFLAGS: -framework Foundation +#include +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++ + } +} diff --git a/gui/explain.go b/gui/explain.go index a8f7705..8e6756e 100644 --- a/gui/explain.go +++ b/gui/explain.go @@ -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) } } diff --git a/gui/fontmap.go b/gui/fontmap.go index 6b3b77f..7af5a81 100644 --- a/gui/fontmap.go +++ b/gui/fontmap.go @@ -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 } diff --git a/gui/fonts.go b/gui/fonts.go index 5582e6d..a060fd3 100644 --- a/gui/fonts.go +++ b/gui/fonts.go @@ -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 diff --git a/gui/gui.go b/gui/gui.go index f859b9b..6d33a8d 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -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() +} diff --git a/gui/input.go b/gui/input.go index d7d390b..d249fcf 100644 --- a/gui/input.go +++ b/gui/input.go @@ -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)}) } } diff --git a/gui/opengl.go b/gui/opengl.go new file mode 100644 index 0000000..5406861 --- /dev/null +++ b/gui/opengl.go @@ -0,0 +1,9 @@ +// +build !darwin + +package gui + +import "github.com/go-gl/glfw/v3.2/glfw" + +func UpdateNSGLContext(window *glfw.Window) { + +} diff --git a/gui/renderer.go b/gui/renderer.go index b4d5e77..c998a0d 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -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) { diff --git a/gui/shaders.go b/gui/shaders.go index 63e618a..7c737b7 100644 --- a/gui/shaders.go +++ b/gui/shaders.go @@ -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" ) diff --git a/gui/textbox.go b/gui/textbox.go index f8ea56f..223e18e 100644 --- a/gui/textbox.go +++ b/gui/textbox.go @@ -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 { diff --git a/hint.colour.png b/hint.colour.png deleted file mode 100644 index c0ec105..0000000 Binary files a/hint.colour.png and /dev/null differ diff --git a/hint.png b/hint.png deleted file mode 100644 index 69eadbe..0000000 Binary files a/hint.png and /dev/null differ diff --git a/main.go b/main.go index ab850fe..4d793e8 100644 --- a/main.go +++ b/main.go @@ -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... diff --git a/powerline.png b/powerline.png deleted file mode 100644 index 7d164ef..0000000 Binary files a/powerline.png and /dev/null differ diff --git a/sixel.png b/sixel.png deleted file mode 100644 index 771a218..0000000 Binary files a/sixel.png and /dev/null differ diff --git a/terminal/output.go b/terminal/output.go index a37440e..9dd19a3 100644 --- a/terminal/output.go +++ b/terminal/output.go @@ -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 } } diff --git a/terminal/terminal.go b/terminal/terminal.go index 2dc1647..72d4d7a 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -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