Initial commit

This commit is contained in:
Liam Galvin 2021-07-30 23:29:20 +01:00
commit e60a0b0427
1302 changed files with 801418 additions and 0 deletions

28
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Release
on:
push:
tags:
- v*
jobs:
build:
name: Releasing Darktile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-go@v2
with:
go-version: '^1.16.6'
- run: go version
- name: Release
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

20
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,20 @@
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.16.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Test
run: |
sudo apt install xorg-dev libgl1-mesa-dev
DISPLAY=:0 go test -mod=vendor ./...

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
/darktile

39
.goreleaser.yml Normal file
View File

@ -0,0 +1,39 @@
builds:
-
id: darktile
main: ./cmd/darktile
binary: darktile
ldflags:
- "-X github.com/liamg/darktile/internal/app/darktile/version.Version={{.Version}} -s -w -extldflags '-fno-PIC -static'"
env:
- CGO_ENABLED=0
- GOFLAGS=-mod=vendor
goos:
- linux
goarch:
- amd64
- arm64
checksum:
name_template: '{{ .ProjectName }}_checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
archives:
-
format: binary
name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}"
release:
prerelease: auto
github:
owner: liamg
name: darktile

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
default: build
build:
./scripts/build.sh

105
README.md Normal file
View File

@ -0,0 +1,105 @@
# Darktile
[![GoReportCard](https://goreportcard.com/badge/github.com/liamg/darktile)](https://goreportcard.com/report/github.com/liamg/darktile)
[![Downloads](https://img.shields.io/github/downloads/liamg/darktile/total)](https://github.com/liamg/darktile/releases)
Darktile is a GPU rendered terminal emulator designed for tiling window managers.
![Demo](demo.gif)
## Features
- GPU rendering
- Unicode support
- Compiled-in powerline font
- Configurable/customisable, supports custom themes, fonts etc.
- Hints: Context-aware overlays e.g. hex colour viewer
- Take screenshots with a single key-binding
- Sixel support
- Transparency
## Installation
Install dependencies:
- `xorg-dev`
- `libgl1-mesa-dev`
Grab a binary from the [latest release](https://github.com/liamg/darktile/releases/latest).
Alternatively, you can install with Go:
```bash
go get github.com/liamg/darktile/cmd/darktile
```
## Configuration
Configuration files should be created in `$XDG_CONFIG_HOME/darktile/` if the variable is defined, otherwise in `$HOME/.config/darktile/`.
If you wish, you can create an example config file as a starting point using `darktile --rewrite-config`.
Darktile will use sensible defaults if no config/theme files are available. The same applies when you omit settings from config/theme files, meaning it is perfectly valid to start with empty config/theme files and add to them as required to override the default behaviour.
### Config File
Found in the config directory (see above) inside `config.yaml`.
```yaml
opacity: 1.0 # window opacity: 0.0 is fully transparent, 1.0 is fully opaque
font:
family: "" # Find possible values for this by running 'darktile list-fonts'
size: 16
dpi: 72
```
### Example Theme
Found in the config directory (see above) inside `theme.yaml`. You can replace this file with a symlink or any theme file from [darktile-themes](https://github.com/liamg/darktile-themes).
```yaml
black: '#1d1f21'
red: '#cc6666'
green: '#b5bd68'
yellow: '#f0c674'
blue: '#81a2be'
magenta: '#b294bb'
cyan: '#8abeb7'
white: '#c5c8c6'
brightblack: '#666666'
brightred: '#d54e53'
brightgreen: '#b9ca4a'
brightyellow: '#e7c547'
brightblue: '#7aa6da'
brightmagenta: '#c397d8'
brightcyan: '#70c0b1'
brightwhite: '#eaeaea'
background: '#1d1f21'
foreground: '#c5c8c6'
selectionbackground: '#aa8800'
selectionforeground: '#ffffff'
cursorforeground: '#1d1f21'
cursorbackground: '#c5c8c6'
```
## Key Bindings
| Action | Binding |
|-----------------------------|---------|
| Copy | `ctrl + shift + C`
| Paste | `ctrl + shift + V`
| Decrease font size | `ctrl + -`
| Increase font size | `ctrl + =`
| Take screenshot | `ctrl + shift + [`
| Open URL | `ctrl + click`
## FAQ
### What happened to Aminal?
The name changed as a result of a near-complete rewrite of Aminal. Also, Google's "did you mean animal?" was getting pretty annoying.
### Did darktile drop Windows/OSX support?
While the project likely won't need much work to build on Windows/OSX, the focus is to develop Darktile for tiling window managers under Linux. If you'd like to get Darktile working for other environments, pull requests are always very welcome, especially when preceded by issues/discussion.

78
cmd/darktile/main.go Normal file
View File

@ -0,0 +1,78 @@
package main
import (
"os"
"github.com/liamg/darktile/internal/app/darktile/cmd"
)
/**
*/
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

52
cmd/packfont/main.go Normal file
View File

@ -0,0 +1,52 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Please specify font path and name")
os.Exit(1)
}
path := os.Args[1]
name := os.Args[2]
fontBytes, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
packed, err := os.OpenFile(fmt.Sprintf("./internal/app/darktile/packed/%s.go", strings.ToLower(name)), os.O_WRONLY|os.O_CREATE, 0744)
if err != nil {
panic(err)
}
defer packed.Close()
if _, err := packed.WriteString(fmt.Sprintf(`package packed
var %sTTF = []byte{
`, name)); err != nil {
panic(err)
}
for i, b := range fontBytes {
if _, err := packed.WriteString(fmt.Sprintf(" 0x%x,", b)); err != nil {
panic(err)
}
if i > 0 && i%16 == 0 {
if _, err := packed.WriteString("\n"); err != nil {
panic(err)
}
}
}
if _, err := packed.WriteString("}\n"); err != nil {
panic(err)
}
}

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

21
go.mod Normal file
View File

@ -0,0 +1,21 @@
module github.com/liamg/darktile
go 1.16
require (
github.com/creack/pty v1.1.12
github.com/d-tsuji/clipboard v0.0.3
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be // indirect
github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.11.0.20210724070913-1706d9436a78
github.com/liamg/fontinfo v0.1.1-0.20210518075346-15a4f7cd9383
github.com/mvdan/xurls v1.1.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
golang.org/x/exp v0.0.0-20210729172720-737cce5152fc // indirect
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
gopkg.in/yaml.v2 v2.4.0
mvdan.cc/xurls v1.1.0
)

577
go.sum Normal file
View File

@ -0,0 +1,577 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.wow.st/gmp/clip v0.0.0-20191001134149-1458ba6a7cf5 h1:OKeTjZST+/TKvtdA258NXJH+/gIx/xwyZxKrAezNFvk=
git.wow.st/gmp/clip v0.0.0-20191001134149-1458ba6a7cf5/go.mod h1:NLdpaBoMQNFqncwP8OVRNWUDw1Kt9XWm3snfT7cXu24=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 h1:3iF31c7rp7nGZVDv7YQ+VxOgpipVfPKotLXykjZmwM8=
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.12 h1:2QLiUCEbsI13vUyWH6026g0o4u7vxgg2hLUc+D7FPFU=
github.com/creack/pty v1.1.12/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d-tsuji/clipboard v0.0.3 h1:ceGmYEF+uiuSZXnAfMu56DDZaSN+SgM2+3OHrNy0ABY=
github.com/d-tsuji/clipboard v0.0.3/go.mod h1:hF88aLYx9LHNUFRrT6KPRkXEUm34nqP97IFgORGBRFs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210715014612-ab6297867137/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be h1:vEIVIuBApEBQTEJt19GfhoU+zFSV+sNTa9E9FdnRYfk=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210727001814-0db043d8d5be/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hajimehoshi/bitmapfont/v2 v2.1.3 h1:JefUkL0M4nrdVwVq7MMZxSTh6mSxOylm+C4Anoucbb0=
github.com/hajimehoshi/bitmapfont/v2 v2.1.3/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs=
github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.11.0.20210724070913-1706d9436a78 h1:eb0XQeiwKUPLLISmH3ssGzq/HXdydrMnYz6SyunNwKU=
github.com/hajimehoshi/ebiten/v2 v2.2.0-alpha.11.0.20210724070913-1706d9436a78/go.mod h1:4AG16fE4/E9OfftCnkhL1KXUEAkA/my+AQ0eY/vi8jw=
github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/liamg/fontinfo v0.1.1-0.20210518075346-15a4f7cd9383 h1:UimfTA954V0lnPnh7X6baeyEG0daATQgQ7wWshI0S6s=
github.com/liamg/fontinfo v0.1.1-0.20210518075346-15a4f7cd9383/go.mod h1:6REdGXLC8yXmxpX31DDwjjzT06g1c7UcvY75AGf9sH4=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1 h1:/QwQcwWVOQXcoNuV9tHx30gQ3q7jCE/rKcGjwzsa5tg=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 h1:5BmtGkQbch91lglMHQ9JIDGiYCL3kBRBA0ItZTvOcEI=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20210729172720-737cce5152fc h1:PKeT2DzGeVcdpsh9a/oZ8gJUG/+S7sADhVilAHjY0dA=
golang.org/x/exp v0.0.0-20210729172720-737cce5152fc/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554 h1:3In5TnfvnuXTF/uflgpYxSCEGP2NdYT37KsPh3VjZYU=
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554/go.mod h1:jFTmtFYCV0MFtXBU+J5V/+5AUeVS0ON/0WkE/KSrl6E=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

View File

@ -0,0 +1,30 @@
package cmd
import (
"fmt"
"github.com/liamg/fontinfo"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(listFontsCmd)
}
var listFontsCmd = &cobra.Command{
Use: "list-fonts",
Short: "List fonts on your system which are compatible with darktile",
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
fonts, err := fontinfo.Match(fontinfo.MatchStyle("Regular"))
if err != nil {
return err
}
for _, font := range fonts {
fmt.Println(font.Family)
}
return nil
},
}

View File

@ -0,0 +1,130 @@
package cmd
import (
"errors"
"fmt"
"os"
"time"
"github.com/liamg/darktile/internal/app/darktile/config"
"github.com/liamg/darktile/internal/app/darktile/gui"
"github.com/liamg/darktile/internal/app/darktile/hinters"
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/liamg/darktile/internal/app/darktile/version"
"github.com/spf13/cobra"
)
var rewriteConfig bool
var debugFile string
var initialCommand string
var shell string
var screenshotAfterMS int
var screenshotFilename string
var themePath string
var showVersion bool
var rootCmd = &cobra.Command{
Use: os.Args[0],
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
if showVersion {
fmt.Println(version.Version)
os.Exit(0)
}
var startupErrors []error
var fileNotFound *config.ErrorFileNotFound
conf, err := config.LoadConfig()
if err != nil {
if !errors.As(err, &fileNotFound) {
startupErrors = append(startupErrors, err)
}
conf = config.DefaultConfig()
}
if rewriteConfig {
if _, err := conf.Save(); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
}
var theme *termutil.Theme
if themePath != "" {
theme, err = config.LoadThemeFromPath(conf, themePath)
if err != nil {
return fmt.Errorf("failed to load theme: %s", err)
}
} else {
theme, err = config.LoadTheme(conf)
if err != nil {
if !errors.As(err, &fileNotFound) {
startupErrors = append(startupErrors, err)
}
theme, err = config.DefaultTheme(conf)
if err != nil {
return fmt.Errorf("failed to load default theme: %w", err)
}
}
}
termOpts := []termutil.Option{
termutil.WithTheme(theme),
}
if debugFile != "" {
termOpts = append(termOpts, termutil.WithLogFile(debugFile))
}
if shell != "" {
termOpts = append(termOpts, termutil.WithShell(shell))
}
if initialCommand != "" {
termOpts = append(termOpts, termutil.WithInitialCommand(initialCommand))
}
terminal := termutil.New(termOpts...)
options := []gui.Option{
gui.WithFontDPI(conf.Font.DPI),
gui.WithFontSize(conf.Font.Size),
gui.WithFontFamily(conf.Font.Family),
}
if screenshotAfterMS > 0 {
options = append(options, gui.WithStartupFunc(func(g *gui.GUI) {
<-time.After(time.Duration(screenshotAfterMS) * time.Millisecond)
g.RequestScreenshot(screenshotFilename)
}))
}
// load all hinters
for _, hinter := range hinters.All() {
options = append(options, gui.WithHinter(hinter))
}
g, err := gui.New(terminal, options...)
if err != nil {
return err
}
for _, err := range startupErrors {
g.ShowError(err.Error())
}
return g.Run()
},
}
func Execute() error {
rootCmd.Flags().BoolVar(&showVersion, "version", showVersion, "Show darktile version information and exit")
rootCmd.Flags().BoolVar(&rewriteConfig, "rewrite-config", rewriteConfig, "Write the resultant config after parsing config files and merging with defauls back to the config file")
rootCmd.Flags().StringVar(&debugFile, "log-file", debugFile, "Debug log file")
rootCmd.Flags().StringVarP(&shell, "shell", "s", shell, "Shell to launch terminal with - defaults to configured user shell")
rootCmd.Flags().StringVarP(&initialCommand, "command", "c", initialCommand, "Command to run when shell starts - use this with caution")
rootCmd.Flags().IntVar(&screenshotAfterMS, "screenshot-after-ms", screenshotAfterMS, "Take a screenshot after this many milliseconds")
rootCmd.Flags().StringVar(&screenshotFilename, "screenshot-filename", screenshotFilename, "Filename to store screenshot taken by --screenshot-after-ms")
rootCmd.Flags().StringVar(&themePath, "theme-path", themePath, "Path to a theme file to use instead of the default")
return rootCmd.Execute()
}

View File

@ -0,0 +1,83 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path"
"gopkg.in/yaml.v2"
)
type Config struct {
Opacity float64
Font Font
}
type Font struct {
Family string
Size float64
DPI float64
}
type ErrorFileNotFound struct {
Path string
}
func (e *ErrorFileNotFound) Error() string {
return fmt.Sprintf("file was not found at '%s'", e.Path)
}
func getConfigPath() (string, error) {
return getPath("config.yaml")
}
func getPath(filename string) (string, error) {
baseDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("config directory missing: %w", err)
}
return path.Join(baseDir, "darktile", filename), nil
}
func LoadConfig() (*Config, error) {
configPath, err := getConfigPath()
if err != nil {
return nil, fmt.Errorf("failed to locate config path: %w", err)
}
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return nil, &ErrorFileNotFound{Path: configPath}
}
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file at '%s': %w", configPath, err)
}
config := defaultConfig
if err := yaml.Unmarshal(configData, &config); err != nil {
return nil, fmt.Errorf("invalid config file at '%s': %w", configPath, err)
}
return &config, nil
}
func (c *Config) Save() (string, error) {
configPath, err := getConfigPath()
if err != nil {
return "", fmt.Errorf("failed to locate config path: %w", err)
}
if err := os.MkdirAll(path.Dir(configPath), 0700); err != nil {
return "", err
}
data, err := yaml.Marshal(c)
if err != nil {
return "", err
}
return configPath, ioutil.WriteFile(configPath, data, 0600)
}

View File

@ -0,0 +1,137 @@
package config
import (
"encoding/hex"
"fmt"
"image/color"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
var defaultConfig = Config{
Opacity: 1.0,
Font: Font{
Family: "", // internally packed font will be loaded by default
Size: 18.0,
DPI: 72.0,
},
}
var defaultTheme = Theme{
Black: "#1d1f21",
Red: "#cc6666",
Green: "#b5bd68",
Yellow: "#f0c674",
Blue: "#81a2be",
Magenta: "#b294bb",
Cyan: "#8abeb7",
White: "#c5c8c6",
BrightBlack: "#666666",
BrightRed: "#d54e53",
BrightGreen: "#b9ca4a",
BrightYellow: "#e7c547",
BrightBlue: "#7aa6da",
BrightMagenta: "#c397d8",
BrightCyan: "#70c0b1",
BrightWhite: "#eaeaea",
Background: "#1d1f21",
Foreground: "#c5c8c6",
SelectionBackground: "#33aa33",
SelectionForeground: "#ffffff",
CursorForeground: "#1d1f21",
CursorBackground: "#c5c8c6",
}
func DefaultConfig() *Config {
copiedConf := defaultConfig
return &copiedConf
}
func DefaultTheme(conf *Config) (*termutil.Theme, error) {
return loadThemeFromConf(conf, &defaultTheme)
}
func LoadTheme(conf *Config) (*termutil.Theme, error) {
themeConf, err := loadTheme("")
if err != nil {
return nil, err
}
return loadThemeFromConf(conf, themeConf)
}
func LoadThemeFromPath(conf *Config, path string) (*termutil.Theme, error) {
themeConf, err := loadTheme(path)
if err != nil {
return nil, err
}
return loadThemeFromConf(conf, themeConf)
}
func loadThemeFromConf(conf *Config, themeConf *Theme) (*termutil.Theme, error) {
factory := termutil.NewThemeFactory().WithOpacity(conf.Opacity)
colours := map[termutil.Colour]string{
termutil.ColourBlack: themeConf.Black,
termutil.ColourRed: themeConf.Red,
termutil.ColourGreen: themeConf.Green,
termutil.ColourYellow: themeConf.Yellow,
termutil.ColourBlue: themeConf.Blue,
termutil.ColourMagenta: themeConf.Magenta,
termutil.ColourCyan: themeConf.Cyan,
termutil.ColourWhite: themeConf.White,
termutil.ColourBrightBlack: themeConf.BrightBlack,
termutil.ColourBrightRed: themeConf.BrightRed,
termutil.ColourBrightGreen: themeConf.BrightGreen,
termutil.ColourBrightYellow: themeConf.BrightYellow,
termutil.ColourBrightBlue: themeConf.BrightBlue,
termutil.ColourBrightMagenta: themeConf.BrightMagenta,
termutil.ColourBrightCyan: themeConf.BrightCyan,
termutil.ColourBrightWhite: themeConf.BrightWhite,
termutil.ColourBackground: themeConf.Background,
termutil.ColourForeground: themeConf.Foreground,
termutil.ColourSelectionBackground: themeConf.SelectionBackground,
termutil.ColourSelectionForeground: themeConf.SelectionForeground,
termutil.ColourCursorForeground: themeConf.CursorForeground,
termutil.ColourCursorBackground: themeConf.CursorBackground,
}
for key, colHex := range colours {
col, err := colourFromHex(colHex, conf.Opacity)
if err != nil {
return nil, fmt.Errorf("invalid hex value '%s' in theme", colHex)
}
factory.WithColour(
key,
col,
)
}
return factory.Build(), nil
}
func colourFromHex(hexadecimal string, opacity float64) (color.Color, error) {
if len(hexadecimal) == 0 {
return nil, fmt.Errorf("colour value cannot be empty")
}
if hexadecimal[0] != '#' || len(hexadecimal) != 7 {
return nil, fmt.Errorf("colour values should start with '#' and contain an RGB value encoded in hex, for example #ffffff")
}
decoded, err := hex.DecodeString(hexadecimal[1:])
if err != nil {
return nil, err
}
return color.RGBA{
R: decoded[0],
G: decoded[1],
B: decoded[2],
A: uint8(opacity * 0xff),
}, nil
}

View File

@ -0,0 +1,31 @@
package config
import (
"errors"
"fmt"
)
type RecoverableError struct {
msg string
inner error
}
func NewRecoverableError(msg string, cause error) *RecoverableError {
return &RecoverableError{
inner: cause,
msg: msg,
}
}
func IsErrRecoverable(err error) bool {
var rec *RecoverableError
return errors.As(err, &rec)
}
func (e *RecoverableError) Error() string {
if e.inner == nil {
return e.msg
}
return fmt.Sprintf("%s: %s", e.msg, e.inner)
}

View File

@ -0,0 +1,84 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path"
"gopkg.in/yaml.v2"
)
type Theme struct {
Black string
Red string
Green string
Yellow string
Blue string
Magenta string
Cyan string
White string
BrightBlack string
BrightRed string
BrightGreen string
BrightYellow string
BrightBlue string
BrightMagenta string
BrightCyan string
BrightWhite string
Background string
Foreground string
SelectionBackground string
SelectionForeground string
CursorForeground string
CursorBackground string
}
func getThemePath() (string, error) {
return getPath("theme.yaml")
}
func loadTheme(themePath string) (*Theme, error) {
if themePath == "" {
var err error
themePath, err = getThemePath()
if err != nil {
return nil, fmt.Errorf("failed to locate theme path: %w", err)
}
}
if _, err := os.Stat(themePath); os.IsNotExist(err) {
return nil, &ErrorFileNotFound{Path: themePath}
}
themeData, err := ioutil.ReadFile(themePath)
if err != nil {
return nil, fmt.Errorf("failed to read theme file at '%s': %w", themePath, err)
}
theme := defaultTheme
if err := yaml.Unmarshal(themeData, &theme); err != nil {
return nil, fmt.Errorf("invalid theme file at '%s': %w", themePath, err)
}
return &theme, nil
}
func (t *Theme) Save() (string, error) {
themePath, err := getThemePath()
if err != nil {
return "", fmt.Errorf("failed to locate theme path: %w", err)
}
if err := os.MkdirAll(path.Dir(themePath), 0700); err != nil {
return "", err
}
data, err := yaml.Marshal(t)
if err != nil {
return "", err
}
return themePath, ioutil.WriteFile(themePath, data, 0600)
}

View File

@ -0,0 +1,257 @@
package font
import (
"fmt"
"image"
"math"
"os"
"github.com/liamg/darktile/internal/app/darktile/packed"
"github.com/liamg/fontinfo"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
type Style uint8
const (
Regular Style = iota
Bold
Italic
BoldItalic
)
type StyleName string
const (
StyleRegular StyleName = "Regular"
StyleBold StyleName = "Bold"
StyleItalic StyleName = "Italic"
StyleBoldItalic StyleName = "Bold Italic"
)
type Manager struct {
family string
regularFace font.Face
boldFace font.Face
italicFace font.Face
boldItalicFace font.Face
size float64
dpi float64
charSize image.Point
fontDotDepth int
}
func NewManager() *Manager {
return &Manager{
size: 16,
dpi: 72,
}
}
func (m *Manager) CharSize() image.Point {
return m.charSize
}
func (m *Manager) IncreaseSize() {
m.SetSize(m.size + 1)
}
func (m *Manager) DecreaseSize() {
if m.size < 2 {
return
}
m.SetSize(m.size - 1)
}
func (m *Manager) DotDepth() int {
return m.fontDotDepth
}
func (m *Manager) DPI() float64 {
return m.dpi
}
func (m *Manager) SetDPI(dpi float64) error {
if dpi <= 0 {
return fmt.Errorf("DPI must be >0")
}
m.dpi = dpi
return nil
}
func (m *Manager) SetSize(size float64) error {
m.size = size
if m.regularFace != nil {
// effectively reload fonts at new size
m.SetFontByFamilyName(m.family)
}
return nil
}
func (m *Manager) loadFontFace(path string) (font.Face, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
fnt, err := opentype.ParseReaderAt(f)
if err != nil {
return nil, err
}
return m.createFace(fnt)
}
func (m *Manager) createFace(f *opentype.Font) (font.Face, error) {
return opentype.NewFace(f, &opentype.FaceOptions{
Size: m.size,
DPI: m.dpi,
Hinting: font.HintingFull,
})
}
func (m *Manager) SetFontByFamilyName(name string) error {
m.family = name
if name == "" {
return m.loadDefaultFonts()
}
fonts, err := fontinfo.Match(fontinfo.MatchFamily(name))
if err != nil {
return err
}
if len(fonts) == 0 {
return fmt.Errorf("could not find font with family '%s'", name)
}
for _, fontMeta := range fonts {
switch StyleName(fontMeta.Style) {
case StyleRegular:
m.regularFace, err = m.loadFontFace(fontMeta.Path)
if err != nil {
return err
}
case StyleBold:
m.boldFace, err = m.loadFontFace(fontMeta.Path)
if err != nil {
return err
}
case StyleItalic:
m.italicFace, err = m.loadFontFace(fontMeta.Path)
if err != nil {
return err
}
case StyleBoldItalic:
m.boldItalicFace, err = m.loadFontFace(fontMeta.Path)
if err != nil {
return err
}
}
}
if m.regularFace == nil {
return fmt.Errorf("could not find regular style for font family '%s'", name)
}
return m.calcMetrics()
}
func (m *Manager) calcMetrics() error {
face := m.regularFace
var prevAdvance int
for ch := rune(32); ch <= 126; ch++ {
adv26, ok := face.GlyphAdvance(ch)
if ok && adv26 > 0 {
advance := int(adv26)
if prevAdvance > 0 && prevAdvance != advance {
return fmt.Errorf("the specified font is not monospaced: %d 0x%X=%d", prevAdvance, ch, advance)
}
prevAdvance = advance
}
}
if prevAdvance == 0 {
return fmt.Errorf("failed to calculate advance width for font face")
}
metrics := face.Metrics()
m.charSize.X = int(math.Round(float64(prevAdvance) / m.dpi))
m.charSize.Y = int(math.Round(float64(metrics.Height) / m.dpi))
m.fontDotDepth = int(math.Round(float64(metrics.Ascent) / m.dpi))
return nil
}
func (m *Manager) loadDefaultFonts() error {
regular, err := opentype.Parse(packed.MesloLGSNFRegularTTF)
if err != nil {
return err
}
m.regularFace, err = m.createFace(regular)
if err != nil {
return err
}
bold, err := opentype.Parse(packed.MesloLGSNFBoldTTF)
if err != nil {
return err
}
m.boldFace, err = m.createFace(bold)
if err != nil {
return err
}
italic, err := opentype.Parse(packed.MesloLGSNFItalicTTF)
if err != nil {
return err
}
m.italicFace, err = m.createFace(italic)
if err != nil {
return err
}
boldItalic, err := opentype.Parse(packed.MesloLGSNFBoldItalicTTF)
if err != nil {
return err
}
m.boldItalicFace, err = m.createFace(boldItalic)
if err != nil {
return err
}
return m.calcMetrics()
}
func (m *Manager) RegularFontFace() font.Face {
return m.regularFace
}
func (m *Manager) BoldFontFace() font.Face {
if m.boldFace == nil {
return m.RegularFontFace()
}
return m.boldFace
}
func (m *Manager) ItalicFontFace() font.Face {
if m.italicFace == nil {
return m.RegularFontFace()
}
return m.italicFace
}
func (m *Manager) BoldItalicFontFace() font.Face {
if m.boldItalicFace == nil {
if m.boldFace == nil {
return m.ItalicFontFace()
}
return m.BoldFontFace()
}
return m.boldItalicFace
}

View File

@ -0,0 +1,316 @@
package gui
import (
"image/color"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/liamg/darktile/internal/app/darktile/termutil"
imagefont "golang.org/x/image/font"
)
// Draw renders the terminal GUI to the ebtien window. Required to implement the ebiten interface.
func (g *GUI) Draw(screen *ebiten.Image) {
cellSize := g.fontManager.CharSize()
dotDepth := g.fontManager.DotDepth()
buffer := g.terminal.GetActiveBuffer()
regularFace := g.fontManager.RegularFontFace()
boldFace := g.fontManager.BoldFontFace()
italicFace := g.fontManager.ItalicFontFace()
boldItalicFace := g.fontManager.BoldItalicFontFace()
var useFace imagefont.Face
defBg := g.terminal.Theme().DefaultBackground()
defFg := g.terminal.Theme().DefaultForeground()
var colour color.Color
endX := float64(cellSize.X * int(buffer.ViewWidth()))
endY := float64(cellSize.Y * int(buffer.ViewHeight()))
extraW := float64(g.size.X) - endX
extraH := float64(g.size.Y) - endY
if extraW > 0 {
ebitenutil.DrawRect(screen, endX, 0, extraW, endY, defBg)
}
if extraH > 0 {
ebitenutil.DrawRect(screen, 0, endY, float64(g.size.X), extraH, defBg)
}
var inHighlight bool
var highlightRendered bool
var highlightMin termutil.Position
highlightMin.Col = uint16(g.size.X)
highlightMin.Line = uint64(g.size.Y)
var highlightMax termutil.Position
for y := int(buffer.ViewHeight() - 1); y >= 0; y-- {
py := cellSize.Y * y
ebitenutil.DrawRect(screen, 0, float64(py), float64(g.size.X), float64(cellSize.Y), defBg)
inHighlight = false
for x := uint16(0); x < buffer.ViewWidth(); x++ {
cell := buffer.GetCell(x, uint16(y))
px := cellSize.X * int(x)
if cell != nil {
colour = cell.Bg()
} else {
colour = defBg
}
isCursor := g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x
if isCursor {
colour = g.terminal.Theme().CursorBackground()
} else if buffer.InSelection(termutil.Position{
Line: uint64(y),
Col: x,
}) {
colour = g.terminal.Theme().SelectionBackground()
} else if colour == nil {
colour = defBg
}
ebitenutil.DrawRect(screen, float64(px), float64(py), float64(cellSize.X), float64(cellSize.Y), colour)
if buffer.IsHighlighted(termutil.Position{
Line: uint64(y),
Col: x,
}) {
if !inHighlight {
highlightRendered = true
}
if uint64(y) < highlightMin.Line {
highlightMin.Col = uint16(g.size.X)
highlightMin.Line = uint64(y)
}
if uint64(y) > highlightMax.Line {
highlightMax.Line = uint64(y)
}
if uint64(y) == highlightMax.Line && x > highlightMax.Col {
highlightMax.Col = x
}
if uint64(y) == highlightMin.Line && x < highlightMin.Col {
highlightMin.Col = x
}
inHighlight = true
} else if inHighlight {
inHighlight = false
}
if isCursor && !ebiten.IsFocused() {
ebitenutil.DrawRect(screen, float64(px)+1, float64(py)+1, float64(cellSize.X)-2, float64(cellSize.Y)-2, g.terminal.Theme().DefaultBackground())
}
}
for x := uint16(0); x < buffer.ViewWidth(); x++ {
cell := buffer.GetCell(x, uint16(y))
if cell == nil || cell.Rune().Rune == 0 {
continue
}
px := cellSize.X * int(x)
colour = cell.Fg()
if g.terminal.GetActiveBuffer().IsCursorVisible() && int(buffer.CursorLine()) == y && buffer.CursorColumn() == x {
colour = g.terminal.Theme().CursorForeground()
} else if buffer.InSelection(termutil.Position{
Line: uint64(y),
Col: x,
}) {
colour = g.terminal.Theme().SelectionForeground()
} else if colour == nil {
colour = defFg
}
useFace = regularFace
if cell.Bold() && cell.Italic() {
useFace = boldItalicFace
} else if cell.Bold() {
useFace = boldFace
} else if cell.Italic() {
useFace = italicFace
}
if cell.Underline() {
uly := float64(py + (dotDepth+cellSize.Y)/2)
ebitenutil.DrawLine(screen, float64(px), uly, float64(px+cellSize.X), uly, colour)
}
text.Draw(screen, string(cell.Rune().Rune), useFace, px, py+dotDepth, colour)
if cell.Strikethrough() {
ebitenutil.DrawLine(screen, float64(px), float64(py+(cellSize.Y/2)), float64(px+cellSize.X), float64(py+(cellSize.Y/2)), colour)
}
}
}
for _, sixel := range buffer.GetVisibleSixels() {
sx := float64(int(sixel.Sixel.X) * cellSize.X)
sy := float64(sixel.ViewLineOffset * cellSize.Y)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(sx, sy)
screen.DrawImage(
ebiten.NewImageFromImage(sixel.Sixel.Image),
op,
)
}
// draw annotations and overlays
if highlightRendered {
if annotation := buffer.GetHighlightAnnotation(); annotation != nil {
if highlightMin.Col == uint16(g.size.X) {
highlightMin.Col = 0
}
if highlightMin.Line == uint64(g.size.Y) {
highlightMin.Line = 0
}
mx, _ := ebiten.CursorPosition()
padding := float64(cellSize.X) / 2
lineX := float64(mx)
var lineY float64
var lineHeight float64
annotationX := mx - cellSize.X*2
var annotationY float64
annotationWidth := float64(cellSize.X) * annotation.Width
var annotationHeight float64
if annotationX+int(annotationWidth)+int(padding*2) > g.size.X {
annotationX = g.size.X - (int(annotationWidth) + int(padding*2))
}
if annotationX < int(padding) {
annotationX = int(padding)
}
if (highlightMin.Line + (highlightMax.Line-highlightMin.Line)/2) < uint64(buffer.ViewHeight()/2) {
// annotate underneath max
pixelsUnderHighlight := float64(g.size.Y) - float64((highlightMax.Line+1)*uint64(cellSize.Y))
// we need to reserve at least one cell height for the label line
pixelsAvailableY := pixelsUnderHighlight - float64(cellSize.Y)
annotationHeight = annotation.Height * float64(cellSize.Y)
if annotationHeight > pixelsAvailableY {
annotationHeight = pixelsAvailableY
}
lineHeight = pixelsUnderHighlight - padding - annotationHeight
if lineHeight > annotationHeight {
if annotationHeight > float64(cellSize.Y)*3 {
lineHeight = annotationHeight
} else {
lineHeight = float64(cellSize.Y) * 3
}
}
annotationY = float64((highlightMax.Line+1)*uint64(cellSize.Y)) + lineHeight + float64(padding)
lineY = float64((highlightMax.Line + 1) * uint64(cellSize.Y))
} else {
//annotate above min
pixelsAboveHighlight := float64((highlightMin.Line) * uint64(cellSize.Y))
// we need to reserve at least one cell height for the label line
pixelsAvailableY := pixelsAboveHighlight - float64(cellSize.Y)
annotationHeight = annotation.Height * float64(cellSize.Y)
if annotationHeight > pixelsAvailableY {
annotationHeight = pixelsAvailableY
}
lineHeight = pixelsAboveHighlight - annotationHeight
if lineHeight > annotationHeight {
if annotationHeight > float64(cellSize.Y)*3 {
lineHeight = annotationHeight
} else {
lineHeight = float64(cellSize.Y) * 3
}
}
annotationY = float64((highlightMin.Line)*uint64(cellSize.Y)) - lineHeight - float64(padding*2) - annotationHeight
lineY = annotationY + annotationHeight + +padding
}
// draw opaque box below and above highlighted line(s)
ebitenutil.DrawRect(screen, 0, float64(highlightMin.Line*uint64(cellSize.Y)), float64(cellSize.X*int(highlightMin.Col)), float64(cellSize.Y), color.RGBA{A: 0x80})
ebitenutil.DrawRect(screen, float64((cellSize.X)*int(highlightMax.Col+1)), float64(highlightMax.Line*uint64(cellSize.Y)), float64(g.size.X), float64(cellSize.Y), color.RGBA{A: 0x80})
ebitenutil.DrawRect(screen, 0, 0, float64(g.size.X), float64(highlightMin.Line*uint64(cellSize.Y)), color.RGBA{A: 0x80})
afterLineY := float64((1 + highlightMax.Line) * uint64(cellSize.Y))
ebitenutil.DrawRect(screen, 0, afterLineY, float64(g.size.X), float64(g.size.Y)-afterLineY, color.RGBA{A: 0x80})
// annotation border
ebitenutil.DrawRect(screen, float64(annotationX)-padding, annotationY-padding, float64(annotationWidth)+(padding*2), annotationHeight+(padding*2), g.terminal.Theme().SelectionBackground())
// annotation background
ebitenutil.DrawRect(screen, 1+float64(annotationX)-padding, 1+annotationY-padding, float64(annotationWidth)+(padding*2)-2, annotationHeight+(padding*2)-2, g.terminal.Theme().DefaultBackground())
// vertical line
ebitenutil.DrawLine(screen, lineX, float64(lineY), lineX, lineY+lineHeight, g.terminal.Theme().SelectionBackground())
var tY int
var tX int
if annotation.Image != nil {
tY += annotation.Image.Bounds().Dy() + cellSize.Y/2
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(annotationX), annotationY)
screen.DrawImage(
ebiten.NewImageFromImage(annotation.Image),
op,
)
}
for _, r := range annotation.Text {
if r == '\n' {
tY += cellSize.Y
tX = 0
continue
}
text.Draw(screen, string(r), regularFace, annotationX+tX, int(annotationY)+dotDepth+tY, g.terminal.Theme().DefaultForeground())
tX += cellSize.X
}
}
}
if len(g.popupMessages) > 0 {
pad := cellSize.Y / 2 // horizontal and vertical padding
msgEndY := endY
for _, msg := range g.popupMessages {
lines := strings.Split(msg.Text, "\n")
msgX := pad
msgY := msgEndY - float64(pad*3) - float64(cellSize.Y*len(lines))
msgText := msg.Text
boxWidth := float64(pad*2) + float64(cellSize.X*len(msgText))
boxHeight := float64(pad*2) + float64(cellSize.Y*len(lines))
if boxWidth < endX/8 {
boxWidth = endX / 8
}
ebitenutil.DrawRect(screen, float64(msgX-1), msgY-1, boxWidth+2, boxHeight+2, msg.Foreground)
ebitenutil.DrawRect(screen, float64(msgX), msgY, boxWidth, boxHeight, msg.Background)
for y, line := range lines {
for x, r := range line {
text.Draw(screen, string(r), regularFace, msgX+pad+(x*cellSize.X), pad+(y*cellSize.Y)+int(msgY)+dotDepth, msg.Foreground)
}
}
msgEndY = msgEndY - float64(pad*4) - float64(len(lines)*g.CellSize().Y)
}
}
if g.screenshotRequested {
g.takeScreenshot(screen)
}
}

View File

@ -0,0 +1,158 @@
package gui
import (
"fmt"
"image"
"image/color"
"math/rand"
"os"
"strings"
"time"
"github.com/liamg/darktile/internal/app/darktile/font"
"github.com/liamg/darktile/internal/app/darktile/hinters"
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/hajimehoshi/ebiten/v2"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type GUI struct {
mouseStateLeft MouseState
mouseStateRight MouseState
mouseStateMiddle MouseState
mouseDrag bool
size image.Point // pixels
terminal *termutil.Terminal
updateChan chan struct{}
lastClick time.Time
clickCount int
fontManager *font.Manager
mousePos termutil.Position
hinters []hinters.Hinter
activeHinter int
popupMessages []PopupMessage
screenshotRequested bool
screenshotFilename string
startupFuncs []func(g *GUI)
keyState *keyState
}
type PopupMessage struct {
Text string
Expiry time.Time
Foreground color.Color
Background color.Color
}
type MouseState uint8
const (
MouseStateNone MouseState = iota
MouseStatePressed
)
func New(terminal *termutil.Terminal, options ...Option) (*GUI, error) {
g := &GUI{
terminal: terminal,
size: image.Point{80, 30},
updateChan: make(chan struct{}),
fontManager: font.NewManager(),
activeHinter: -1,
keyState: newKeyState(),
}
for _, option := range options {
if err := option(g); err != nil {
return nil, err
}
}
terminal.SetWindowManipulator(NewManipulator(g))
return g, nil
}
func (g *GUI) Run() error {
go func() {
if err := g.terminal.Run(g.updateChan, uint16(g.size.X), uint16(g.size.Y)); err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err)
os.Exit(1)
}
os.Exit(0)
}()
ebiten.SetScreenTransparent(true)
ebiten.SetWindowResizable(true)
ebiten.SetRunnableOnUnfocused(true)
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMinimum)
for _, f := range g.startupFuncs {
go f(g)
}
go g.watchForUpdate()
return ebiten.RunGame(g)
}
func (g *GUI) watchForUpdate() {
for range g.updateChan {
ebiten.ScheduleFrame()
go func() {
for g.keyState.AnythingPressed() {
time.Sleep(time.Millisecond * 10)
ebiten.ScheduleFrame()
}
}()
}
}
func (g *GUI) CellSize() image.Point {
return g.fontManager.CharSize()
}
func (g *GUI) Highlight(start termutil.Position, end termutil.Position, label string, img image.Image) {
if label == "" && img == nil {
g.terminal.GetActiveBuffer().Highlight(start, end, nil)
return
}
annotation := &termutil.Annotation{
Text: label,
Image: img,
}
if label != "" {
lines := strings.Split(label, "\n")
annotation.Height = float64(len(lines))
for _, line := range lines {
if float64(len(line)) > annotation.Width {
annotation.Width = float64(len(line))
}
}
}
if img != nil {
annotation.Height += float64(img.Bounds().Dy() / g.fontManager.CharSize().Y)
if label != "" {
annotation.Height += 0.5 // half line spacing between image + text
}
imgCellWidth := img.Bounds().Dx() / g.fontManager.CharSize().X
if float64(imgCellWidth) > annotation.Width {
annotation.Width = float64(imgCellWidth)
}
}
g.terminal.GetActiveBuffer().Highlight(start, end, annotation)
}
func (g *GUI) ClearHighlight() {
g.terminal.GetActiveBuffer().ClearHighlight()
}

View File

@ -0,0 +1,228 @@
package gui
import (
"fmt"
"github.com/d-tsuji/clipboard"
"github.com/hajimehoshi/ebiten/v2"
)
var modifiableKeys = map[ebiten.Key]uint8{
ebiten.KeyA: 'A',
ebiten.KeyB: 'B',
ebiten.KeyC: 'C',
ebiten.KeyD: 'D',
ebiten.KeyE: 'E',
ebiten.KeyF: 'F',
ebiten.KeyG: 'G',
ebiten.KeyH: 'H',
ebiten.KeyI: 'I',
ebiten.KeyJ: 'J',
ebiten.KeyK: 'K',
ebiten.KeyL: 'L',
ebiten.KeyM: 'M',
ebiten.KeyN: 'N',
ebiten.KeyO: 'O',
ebiten.KeyP: 'P',
ebiten.KeyQ: 'Q',
ebiten.KeyR: 'R',
ebiten.KeyS: 'S',
ebiten.KeyT: 'T',
ebiten.KeyU: 'U',
ebiten.KeyV: 'V',
ebiten.KeyW: 'W',
ebiten.KeyX: 'X',
ebiten.KeyY: 'Y',
ebiten.KeyZ: 'Z',
}
func (g *GUI) handleInput() error {
if err := g.handleMouse(); err != nil {
return err
}
switch true {
case ebiten.IsKeyPressed(ebiten.KeyControl) && ebiten.IsKeyPressed(ebiten.KeyShift):
switch true {
case g.keyState.RepeatPressed(ebiten.KeyC):
content, selection := g.terminal.GetActiveBuffer().GetSelection()
if selection == nil {
return nil
}
return clipboard.Set(content)
case g.keyState.RepeatPressed(ebiten.KeyV):
paste, err := clipboard.Get()
if err != nil {
return err
}
return g.terminal.WriteToPty([]byte(paste))
case g.keyState.RepeatPressed(ebiten.KeyBracketLeft):
g.RequestScreenshot("")
}
case ebiten.IsKeyPressed(ebiten.KeyControl):
for key, ch := range modifiableKeys {
if g.keyState.RepeatPressed(key) {
if ch >= 97 && ch < 123 {
return g.terminal.WriteToPty([]byte{ch - 96})
} else if ch >= 65 && ch < 91 {
return g.terminal.WriteToPty([]byte{ch - 64})
}
}
}
switch true {
case g.keyState.RepeatPressed(ebiten.KeyMinus):
g.fontManager.DecreaseSize()
cellSize := g.fontManager.CharSize()
cols, rows := g.size.X/cellSize.X, g.size.Y/cellSize.Y
if err := g.terminal.SetSize(uint16(rows), uint16(cols)); err != nil {
return err
}
return nil
case g.keyState.RepeatPressed(ebiten.KeyEqual):
g.fontManager.IncreaseSize()
cellSize := g.fontManager.CharSize()
cols, rows := g.size.X/cellSize.X, g.size.Y/cellSize.Y
if err := g.terminal.SetSize(uint16(rows), uint16(cols)); err != nil {
return err
}
return nil
default:
return nil
}
case ebiten.IsKeyPressed(ebiten.KeyAlt):
for key, ch := range modifiableKeys {
if g.keyState.RepeatPressed(key) {
return g.terminal.WriteToPty([]byte{0x1b, ch})
}
}
case g.keyState.RepeatPressed(ebiten.KeyArrowUp):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
return g.terminal.WriteToPty([]byte{
0x1b,
'O',
'A',
})
} else {
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[%sA", g.getModifierStr())))
}
case g.keyState.RepeatPressed(ebiten.KeyArrowDown):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
return g.terminal.WriteToPty([]byte{
0x1b,
'O',
'B',
})
} else {
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[%sB", g.getModifierStr())))
}
case g.keyState.RepeatPressed(ebiten.KeyArrowRight):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
return g.terminal.WriteToPty([]byte{
0x1b,
'O',
'C',
})
} else {
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[%sC", g.getModifierStr())))
}
case g.keyState.RepeatPressed(ebiten.KeyArrowLeft):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
return g.terminal.WriteToPty([]byte{
0x1b,
'O',
'D',
})
} else {
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[%sD", g.getModifierStr())))
}
case g.keyState.RepeatPressed(ebiten.KeyEnter):
if g.terminal.GetActiveBuffer().IsNewLineMode() {
return g.terminal.WriteToPty([]byte{0x0d, 0x0a})
}
return g.terminal.WriteToPty([]byte{0x0d})
case g.keyState.RepeatPressed(ebiten.KeyNumpadEnter):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
g.terminal.WriteToPty([]byte{
0x1b,
'O',
'M',
})
} else {
if g.terminal.GetActiveBuffer().IsNewLineMode() {
if err := g.terminal.WriteToPty([]byte{0x0d, 0x0a}); err != nil {
return err
}
}
return g.terminal.WriteToPty([]byte{0x0d})
}
case g.keyState.RepeatPressed(ebiten.KeyTab):
return g.terminal.WriteToPty([]byte{0x09}) // tab
case g.keyState.RepeatPressed(ebiten.KeyEscape):
return g.terminal.WriteToPty([]byte{0x1b}) // escape
case g.keyState.RepeatPressed(ebiten.KeyBackspace):
if ebiten.IsKeyPressed(ebiten.KeyAlt) {
return g.terminal.WriteToPty([]byte{0x17}) // ctrl-w/delete word
} else {
return g.terminal.WriteToPty([]byte{0x7f}) //0x7f is DEL
}
case g.keyState.RepeatPressed(ebiten.KeyF1):
return g.terminal.WriteToPty([]byte("\x1bOP"))
case g.keyState.RepeatPressed(ebiten.KeyF2):
return g.terminal.WriteToPty([]byte("\x1bOQ"))
case g.keyState.RepeatPressed(ebiten.KeyF3):
return g.terminal.WriteToPty([]byte("\x1bOR"))
case g.keyState.RepeatPressed(ebiten.KeyF4):
return g.terminal.WriteToPty([]byte("\x1bOS"))
case g.keyState.RepeatPressed(ebiten.KeyF5):
return g.terminal.WriteToPty([]byte("\x1b[15~"))
case g.keyState.RepeatPressed(ebiten.KeyF6):
return g.terminal.WriteToPty([]byte("\x1b[17~"))
case g.keyState.RepeatPressed(ebiten.KeyF7):
return g.terminal.WriteToPty([]byte("\x1b[18~"))
case g.keyState.RepeatPressed(ebiten.KeyF8):
return g.terminal.WriteToPty([]byte("\x1b[19~"))
case g.keyState.RepeatPressed(ebiten.KeyF9):
return g.terminal.WriteToPty([]byte("\x1b[20~"))
case g.keyState.RepeatPressed(ebiten.KeyF10):
return g.terminal.WriteToPty([]byte("\x1b[21~"))
case g.keyState.RepeatPressed(ebiten.KeyF11):
return g.terminal.WriteToPty([]byte("\x1b[22~"))
case g.keyState.RepeatPressed(ebiten.KeyF12):
return g.terminal.WriteToPty([]byte("\x1b[23~"))
case g.keyState.RepeatPressed(ebiten.KeyInsert):
return g.terminal.WriteToPty([]byte("\x1b[2~"))
case g.keyState.RepeatPressed(ebiten.KeyDelete):
return g.terminal.WriteToPty([]byte("\x1b[3~"))
case g.keyState.RepeatPressed(ebiten.KeyHome):
if g.terminal.GetActiveBuffer().IsApplicationCursorKeysModeEnabled() {
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[1%s~", g.getModifierStr())))
} else {
return g.terminal.WriteToPty([]byte("\x1b[H"))
}
case g.keyState.RepeatPressed(ebiten.KeyEnd):
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[4%s~", g.getModifierStr())))
case g.keyState.RepeatPressed(ebiten.KeyPageUp):
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[5%s~", g.getModifierStr())))
case g.keyState.RepeatPressed(ebiten.KeyPageDown):
return g.terminal.WriteToPty([]byte(fmt.Sprintf("\x1b[6%s~", g.getModifierStr())))
default:
input := ebiten.AppendInputChars(nil)
for _, runePressed := range input {
if err := g.terminal.WriteToPty([]byte(string(runePressed))); err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,63 @@
package gui
import (
"sync"
"time"
"github.com/hajimehoshi/ebiten/v2"
)
var (
KeyPressDelayNS = 500_000_000
KeyPressRepeatNS = 30_000_000
KeyPressResetNS = 60_000_000
)
type keyState struct {
mu sync.Mutex
keys map[ebiten.Key]press
}
func newKeyState() *keyState {
return &keyState{
keys: make(map[ebiten.Key]press),
}
}
type press struct {
at int64
repeating bool
}
func (k *keyState) AnythingPressed() bool {
return len(k.keys) > 0
}
func (k *keyState) RepeatPressed(key ebiten.Key) bool {
now := time.Now().UnixNano()
k.mu.Lock()
defer k.mu.Unlock()
if ebiten.IsKeyPressed(key) {
event, ok := k.keys[key]
if !ok {
k.keys[key] = press{at: now}
return true
}
since := now - event.at
if !event.repeating && since > int64(KeyPressDelayNS) {
k.keys[key] = press{at: now, repeating: true}
return true
} else if event.repeating && since > int64(KeyPressRepeatNS) {
k.keys[key] = press{at: now, repeating: true}
return true
}
return false
}
delete(k.keys, key)
return false
}

View File

@ -0,0 +1,117 @@
package gui
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
type WindowManipulator struct {
g *GUI
title string
titleStack []string
}
func NewManipulator(g *GUI) *WindowManipulator {
return &WindowManipulator{
g: g,
}
}
func (m *WindowManipulator) ReportError(err error) {
m.g.ShowError(err.Error())
}
func (m *WindowManipulator) CellSizeInPixels() (int, int) {
size := m.g.fontManager.CharSize()
return size.X, size.Y
}
func (m *WindowManipulator) Position() (int, int) {
return ebiten.WindowPosition()
}
func (m *WindowManipulator) GetTitle() string {
return m.title
}
func (m *WindowManipulator) SetTitle(title string) {
m.title = title
ebiten.SetWindowTitle(m.title)
}
func (m *WindowManipulator) SaveTitleToStack() {
m.titleStack = append(m.titleStack, m.title)
}
func (m *WindowManipulator) RestoreTitleFromStack() {
if len(m.titleStack) == 0 {
m.SetTitle("")
}
title := m.titleStack[len(m.titleStack)-1]
m.titleStack = m.titleStack[:len(m.titleStack)-1]
m.SetTitle(title)
}
func (m *WindowManipulator) State() termutil.WindowState {
if ebiten.IsWindowMinimized() {
return termutil.StateMinimised
}
if ebiten.IsWindowMaximized() {
return termutil.StateMaximised
}
return termutil.StateNormal
}
func (m *WindowManipulator) Minimise() {
ebiten.MinimizeWindow()
}
func (m *WindowManipulator) Maximise() {
ebiten.MaximizeWindow()
}
func (m *WindowManipulator) Restore() {
ebiten.RestoreWindow()
}
func (m *WindowManipulator) SizeInPixels() (int, int) {
return m.g.size.X, m.g.size.Y
}
func (m *WindowManipulator) SizeInChars() (int, int) {
return int(m.g.terminal.GetActiveBuffer().ViewWidth()), int(m.g.terminal.GetActiveBuffer().ViewHeight())
}
func (m *WindowManipulator) ResizeInPixels(x int, y int) {
ebiten.SetWindowSize(x, y)
}
func (m *WindowManipulator) ResizeInChars(cols int, rows int) {
x := cols * m.g.fontManager.CharSize().X
y := rows * m.g.fontManager.CharSize().Y
ebiten.SetWindowSize(x, y)
}
func (m *WindowManipulator) ScreenSizeInPixels() (int, int) {
return ebiten.WindowSize()
}
func (m *WindowManipulator) ScreenSizeInChars() (int, int) {
w, h := ebiten.WindowSize()
return w / m.g.fontManager.CharSize().X, h / m.g.fontManager.CharSize().Y
}
func (m *WindowManipulator) Move(x, y int) {
ebiten.SetWindowPosition(x, y)
}
func (m *WindowManipulator) SetFullscreen(enabled bool) {
ebiten.SetFullscreen(enabled)
}
func (m *WindowManipulator) IsFullscreen() bool {
return ebiten.IsFullscreen()
}

View File

@ -0,0 +1,355 @@
package gui
import (
"fmt"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/liamg/darktile/internal/app/darktile/hinters"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
// time allowed between mouse clicks to chain them into e.g. double-click
const clickChainWindowMS = 500
// max duration of a click before it is counted as a drag
const clickMaxDuration = 100
func (g *GUI) handleMouse() error {
_, scrollY := ebiten.Wheel()
if scrollY < 0 {
g.terminal.GetActiveBuffer().ScrollDown(5)
} else if scrollY > 0 {
g.terminal.GetActiveBuffer().ScrollUp(5)
}
x, y := ebiten.CursorPosition()
col := x / g.fontManager.CharSize().X
line := y / g.fontManager.CharSize().Y
var moved bool
if col != int(g.mousePos.Col) || line != int(g.mousePos.Line) {
if col >= 0 && col < int(g.terminal.GetActiveBuffer().ViewWidth()) && line >= 0 && line < int(g.terminal.GetActiveBuffer().ViewHeight()) {
// mouse moved!
moved = true
g.mousePos = termutil.Position{
Col: uint16(col),
Line: uint64(line),
}
if !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
if err := g.handleMouseMove(g.mousePos); err != nil {
return err
}
}
} else if err := g.clearHinters(); err != nil {
return err
}
}
pressedLeft := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft != MouseStatePressed
pressedMiddle := ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle != MouseStatePressed
pressedRight := ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight != MouseStatePressed
released := (!ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && g.mouseStateLeft == MouseStatePressed) ||
(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) && g.mouseStateMiddle == MouseStatePressed) ||
(!ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) && g.mouseStateRight == MouseStatePressed)
defer func() {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
g.mouseStateLeft = MouseStatePressed
} else {
g.mouseStateLeft = MouseStateNone
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
g.mouseStateMiddle = MouseStatePressed
} else {
g.mouseStateMiddle = MouseStateNone
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
g.mouseStateRight = MouseStatePressed
} else {
g.mouseStateRight = MouseStateNone
}
}()
if pressedLeft || pressedMiddle || pressedRight || released {
if g.handleMouseRemotely(x, y, pressedLeft, pressedMiddle, pressedRight, released, moved) {
return nil
}
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
if g.mouseStateLeft == MouseStatePressed {
if g.mouseDrag {
// update selection end
g.terminal.GetActiveBuffer().SetSelectionEnd(termutil.Position{
Line: uint64(line),
Col: uint16(col),
})
} else if time.Since(g.lastClick) > time.Millisecond*clickMaxDuration && !ebiten.IsKeyPressed(ebiten.KeyControl) {
g.mouseDrag = true
}
} else {
if g.clickCount == 0 || time.Since(g.lastClick) < time.Millisecond*clickChainWindowMS {
g.clickCount++
} else {
g.clickCount = 1
}
g.lastClick = time.Now()
handled, err := g.handleClick(g.clickCount, x, y)
if err != nil {
return err
}
if handled {
g.mouseDrag = false
return nil
}
//set selection start
col := x / g.fontManager.CharSize().X
line := y / g.fontManager.CharSize().Y
g.terminal.GetActiveBuffer().SetSelectionStart(termutil.Position{
Line: uint64(line),
Col: uint16(col),
})
}
} else {
g.mouseDrag = false
}
return nil
}
func (g *GUI) clearHinters() error {
if g.activeHinter > -1 {
if err := g.hinters[g.activeHinter].Deactivate(g); err != nil {
return err
}
g.activeHinter = -1
}
return nil
}
// mouse moved to cell (not during click + drag)
func (g *GUI) handleMouseMove(pos termutil.Position) error {
// start uses raw coords
start, _, text, index, ok := g.terminal.GetActiveBuffer().GetBoundedTextAtPosition(pos)
if !ok {
g.clearHinters()
return nil
}
activeHinter := -1
for i, hinter := range g.hinters {
if ok, offset, length := hinter.Match(text, index); ok {
match := text[offset : offset+length]
newStartX := int(start.Col) + offset
newStartY := start.Line
for newStartX >= int(g.terminal.GetActiveBuffer().ViewWidth()) {
newStartX -= int(g.terminal.GetActiveBuffer().ViewWidth())
newStartY++
}
newEndX := newStartX + length - 1
newEndY := newStartY
for newEndX > int(g.terminal.GetActiveBuffer().ViewWidth()) {
newEndX -= int(g.terminal.GetActiveBuffer().ViewWidth())
newEndY++
}
matchStart := termutil.Position{
Col: uint16(newStartX),
Line: newStartY,
}
matchEnd := termutil.Position{
Col: uint16(newEndX),
Line: newEndY,
}
if err := hinter.Activate(g, match, matchStart, matchEnd); err != nil {
return err
}
activeHinter = i
break
}
}
// hinter was just deactivated
if g.activeHinter > -1 && activeHinter == -1 {
if err := g.clearHinters(); err != nil {
return err
}
}
g.activeHinter = activeHinter
return nil
}
func WithHinter(h hinters.Hinter) func(g *GUI) error {
return func(g *GUI) error {
g.hinters = append(g.hinters, h)
return nil
}
}
func (g *GUI) handleClick(clickCount, x, y int) (bool, error) {
switch clickCount {
case 1: // single click
if ebiten.IsKeyPressed(ebiten.KeyControl) { // ctrl + click to run hinters
if g.activeHinter > -1 {
g.hinters[g.activeHinter].Click(g)
}
} else {
g.terminal.GetActiveBuffer().ClearSelection()
}
case 2: //double click
col := uint16(x / g.fontManager.CharSize().X)
line := uint64(y / g.fontManager.CharSize().Y)
g.terminal.GetActiveBuffer().SelectWordAt(termutil.Position{Col: col, Line: line}, wordMatcher)
return true, nil
case 3: // triple click
g.terminal.GetActiveBuffer().ExtendSelectionToEntireLines()
return true, nil
}
return false, nil
}
func alphaMatcher(r rune) bool {
if r >= 65 && r <= 90 {
return true
}
if r >= 97 && r <= 122 {
return true
}
return false
}
func numberMatcher(r rune) bool {
if r >= 48 && r <= 57 {
return true
}
return false
}
func alphaNumericMatcher(r rune) bool {
return alphaMatcher(r) || numberMatcher(r)
}
func wordMatcher(r rune) bool {
if alphaNumericMatcher(r) {
return true
}
if r == '_' {
return true
}
return false
}
func (g *GUI) handleMouseRemotely(x, y int, pressedLeft, pressedMiddle, pressedRight, released, moved bool) bool {
tx, ty := 1+(x/g.fontManager.CharSize().X), 1+(y/g.fontManager.CharSize().Y)
mode := g.terminal.GetActiveBuffer().GetMouseMode()
switch mode {
case termutil.MouseModeNone:
return false
case termutil.MouseModeX10:
var button rune
switch true {
case pressedLeft:
button = 0
case pressedMiddle:
button = 1
case pressedRight:
button = 2
default:
return true
}
packet := fmt.Sprintf("\x1b[M%c%c%c", (rune(button + 32)), (rune(tx + 32)), (rune(ty + 32)))
_ = g.terminal.WriteToPty([]byte(packet))
return true
case termutil.MouseModeVT200, termutil.MouseModeButtonEvent:
var button rune
extMode := g.terminal.GetActiveBuffer().GetMouseExtMode()
switch true {
case pressedLeft:
button = 0
case pressedMiddle:
button = 1
case pressedRight:
button = 2
case released:
if extMode != termutil.MouseExtSGR {
button = 3
}
default:
return true
}
if moved && mode == termutil.MouseModeButtonEvent {
button |= 32
}
if ebiten.IsKeyPressed(ebiten.KeyShift) {
button |= 4
}
if ebiten.IsKeyPressed(ebiten.KeyMeta) {
button |= 8
}
if ebiten.IsKeyPressed(ebiten.KeyControl) {
button |= 16
}
var packet string
if extMode == termutil.MouseExtSGR {
final := 'M'
if released {
final = 'm'
}
packet = fmt.Sprintf("\x1b[<%d;%d;%d%c", button, tx, ty, final)
} else {
packet = fmt.Sprintf("\x1b[M%c%c%c", button+32, tx+32, ty+32)
}
g.terminal.WriteToPty([]byte(packet))
return true
}
return false
}
func (g *GUI) SetCursorToPointer() {
ebiten.SetCursorShape(ebiten.CursorShapePointer)
}
func (g *GUI) ResetCursor() {
ebiten.SetCursorShape(ebiten.CursorShapeDefault)
}

View File

@ -0,0 +1,30 @@
package gui
type Option func(g *GUI) error
func WithFontFamily(family string) func(g *GUI) error {
return func(g *GUI) error {
return g.fontManager.SetFontByFamilyName(family)
}
}
func WithFontSize(size float64) func(g *GUI) error {
return func(g *GUI) error {
g.fontManager.SetSize(size)
return nil
}
}
func WithFontDPI(dpi float64) func(g *GUI) error {
return func(g *GUI) error {
g.fontManager.SetSize(dpi)
return nil
}
}
func WithStartupFunc(f func(g *GUI)) Option {
return func(g *GUI) error {
g.startupFuncs = append(g.startupFuncs, f)
return nil
}
}

View File

@ -0,0 +1,29 @@
package gui
import (
"fmt"
"image/color"
"time"
)
const (
popupMessageDisplayDuration = time.Second * 5
popupErrorDisplayDuration = time.Second * 10
)
func (g *GUI) ShowPopup(msg string, fg color.Color, bg color.Color, duration time.Duration) {
g.popupMessages = append(g.popupMessages, PopupMessage{
Text: msg,
Expiry: time.Now().Add(duration),
Foreground: fg,
Background: bg,
})
}
func (g *GUI) ShowError(msg string) {
g.ShowPopup(fmt.Sprintf("Error!\n%s", msg), color.White, color.RGBA{A: 0xff, R: 0xff}, popupErrorDisplayDuration)
}
func (g *GUI) ShowMessage(msg string) {
g.ShowPopup(msg, color.White, color.RGBA{A: 0xff, G: 0x40, R: 0x40, B: 0xff}, popupMessageDisplayDuration)
}

View File

@ -0,0 +1,35 @@
package gui
import (
"image"
)
// Layout provides the terminal gui size in pixels. Required to implement the ebiten interface.
func (g *GUI) Layout(outsideWidth, outsideHeight int) (int, int) {
w, h := outsideWidth, outsideHeight
if g.size.X != w || g.size.Y != h {
g.size = image.Point{
X: w,
Y: h,
}
g.resize(w, h)
}
return w, h
}
func (g *GUI) resize(w, h int) {
if g.fontManager.CharSize().X == 0 || g.fontManager.CharSize().Y == 0 {
return
}
cols := uint16(w / g.fontManager.CharSize().X)
rows := uint16(h / g.fontManager.CharSize().Y)
if g.terminal != nil && g.terminal.IsRunning() {
_ = g.terminal.SetSize(rows, cols)
}
}

View File

@ -0,0 +1,41 @@
package gui
import (
"fmt"
"image"
"image/png"
"os"
"path/filepath"
"time"
)
func (g *GUI) RequestScreenshot(filename string) {
g.screenshotRequested = true
if filename == "" {
filename = fmt.Sprintf("darktile-screenshot-%d.png", time.Now().UnixNano())
targetdir, err := os.UserHomeDir()
if err != nil {
targetdir = "/tmp"
}
filename = filepath.Join(targetdir, filename)
}
g.screenshotFilename = filename
}
func (g *GUI) takeScreenshot(screen image.Image) {
g.screenshotRequested = false
file, err := os.Create(g.screenshotFilename)
if err != nil {
g.ShowError(fmt.Sprintf("Screenshot failed: %s", err))
return
}
defer file.Close()
if err := png.Encode(file, screen); err != nil {
g.ShowError(fmt.Sprintf("Screenshot failed: %s", err))
return
}
g.ShowMessage(fmt.Sprintf("Screenshot saved: %s", g.screenshotFilename))
}

View File

@ -0,0 +1,51 @@
package gui
import (
"time"
"github.com/hajimehoshi/ebiten/v2"
)
func (g *GUI) getModifierStr() string {
switch true {
case g.keyState.RepeatPressed(ebiten.KeyShift) && g.keyState.RepeatPressed(ebiten.KeyControl) && g.keyState.RepeatPressed(ebiten.KeyAlt):
return ";8"
case g.keyState.RepeatPressed(ebiten.KeyAlt) && g.keyState.RepeatPressed(ebiten.KeyControl):
return ";7"
case g.keyState.RepeatPressed(ebiten.KeyShift) && g.keyState.RepeatPressed(ebiten.KeyControl):
return ";6"
case g.keyState.RepeatPressed(ebiten.KeyControl):
return ";5"
case g.keyState.RepeatPressed(ebiten.KeyShift) && g.keyState.RepeatPressed(ebiten.KeyAlt):
return ";4"
case g.keyState.RepeatPressed(ebiten.KeyAlt):
return ";3"
case g.keyState.RepeatPressed(ebiten.KeyShift):
return ";2"
}
return ""
}
// Update changes the terminal GUI state - all user-initiated modification should happen here.
func (g *GUI) Update() error {
if err := g.handleInput(); err != nil {
return err
}
g.filterPopupMessages()
return nil
}
func (g *GUI) filterPopupMessages() {
var filtered []PopupMessage
for _, msg := range g.popupMessages {
if time.Since(msg.Expiry) >= 0 {
continue
}
filtered = append(filtered, msg)
}
g.popupMessages = filtered
}

View File

@ -0,0 +1,16 @@
package hinters
import (
"image"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
type HintAPI interface {
ShowMessage(msg string)
SetCursorToPointer()
ResetCursor()
Highlight(start termutil.Position, end termutil.Position, label string, img image.Image)
ClearHighlight()
CellSize() image.Point
}

View File

@ -0,0 +1,35 @@
package hinters
import (
"image"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
type TestAPI struct {
highlighted string
}
func (a *TestAPI) ShowMessage(_ string) {
}
func (a *TestAPI) Highlight(start termutil.Position, end termutil.Position, label string, img image.Image) {
a.highlighted = label
}
func (a *TestAPI) ClearHighlight() {
a.highlighted = ""
}
func (a *TestAPI) CellSize() image.Point {
return image.Point{}
}
func (a *TestAPI) SetCursorToPointer() {
}
func (a *TestAPI) ResetCursor() {
}

View File

@ -0,0 +1,63 @@
package hinters
import (
"encoding/base64"
"regexp"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
func init() {
register(&Base64Hinter{}, PriorityVeryLow)
}
type Base64Hinter struct {
target string
}
var base64Matcher = regexp.MustCompile("(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})")
func (h *Base64Hinter) Match(text string, cursorIndex int) (matched bool, offset int, length int) {
matches := base64Matcher.FindAllStringIndex(text, -1)
for _, match := range matches {
if match[0] <= cursorIndex && match[1] > cursorIndex {
result := text[match[0]:match[1]]
if len(result) > 4 && isReadable(result) {
return true, match[0], match[1] - match[0]
}
}
}
return
}
func isReadable(result string) bool {
parts, err := base64.StdEncoding.DecodeString(result)
if err != nil {
return false
}
for i := range parts {
if (parts[i] > 0x7e || parts[i] < 0x20) && parts[i] != 0x0a && parts[i] != 0x0d {
return false
}
}
return true
}
func (h *Base64Hinter) Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error {
h.target = match
result, err := base64.StdEncoding.DecodeString(match)
if err != nil {
return err
}
api.Highlight(start, end, "Base64 decodes to:\n"+string(result), nil)
return nil
}
func (h *Base64Hinter) Deactivate(api HintAPI) error {
api.ClearHighlight()
return nil
}
func (h *Base64Hinter) Click(api HintAPI) error {
return nil
}

View File

@ -0,0 +1,48 @@
package hinters
import (
"testing"
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/stretchr/testify/assert"
)
func Test_b64_hinter_resolves_from_base64_correctly_activated(t *testing.T) {
hinter := &Base64Hinter{}
api := &TestAPI{}
text := "This is the result SGVsbG8gTGlhbQ=="
match, offset, length := hinter.Match(text, 28)
assert.Equal(t, true, match)
hinter.Activate(api, text[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "Base64 decodes to:\nHello Liam", api.highlighted)
}
func Test_b64_hinter_resolves_from_base64_correctly_activated_then_cleared(t *testing.T) {
hinter := &Base64Hinter{}
api := &TestAPI{}
text := "This is the result SGVsbG8gTGlhbQ=="
match, offset, length := hinter.Match(text, 28)
assert.Equal(t, true, match)
hinter.Activate(api, text[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "Base64 decodes to:\nHello Liam", api.highlighted)
hinter.Deactivate(api)
assert.Equal(t, "", api.highlighted)
}
func Test_b64_hinter_doesnt_match_random_junk(t *testing.T) {
hinter := &Base64Hinter{}
text := "This is the result SGVsbG8eTGlhbQ=="
match, _, _ := hinter.Match(text, 10)
assert.Equal(t, false, match)
}

View File

@ -0,0 +1,58 @@
package hinters
import (
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
func init() {
register(&DmesgTimestampHinter{}, PriorityVeryLow)
setSysStartTime()
}
var sysStart time.Time
type DmesgTimestampHinter struct{}
var dmsegTsMatcher = regexp.MustCompile(`^\[\s*\d+.\d{6}\]`)
func (h *DmesgTimestampHinter) Match(text string, cursorIndex int) (matched bool, offset int, length int) {
matches := dmsegTsMatcher.FindAllStringIndex(text, -1)
for _, match := range matches {
if match[0] <= cursorIndex && match[1] > cursorIndex {
return true, match[0], match[1] - match[0]
}
}
return
}
func (h *DmesgTimestampHinter) Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error {
match = strings.Split(strings.Trim(match, "[] "), ".")[0]
seconds, err := strconv.ParseFloat(match, 32)
if err != nil {
return err
}
result := sysStart.Add(time.Duration(seconds) * time.Second).Format(time.ANSIC)
api.Highlight(start, end, result, nil)
return nil
}
func (h *DmesgTimestampHinter) Deactivate(api HintAPI) error {
api.ClearHighlight()
return nil
}
func (h *DmesgTimestampHinter) Click(api HintAPI) error {
return nil
}
func setSysStartTime() {
sysInfo := &syscall.Sysinfo_t{}
_ = syscall.Sysinfo(sysInfo)
sysStart = time.Now().Local().Add(time.Duration(int(sysInfo.Uptime*-1)) * time.Second)
}

View File

@ -0,0 +1,47 @@
package hinters
import (
"testing"
"time"
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/stretchr/testify/assert"
)
func Test_dmesg_hinter_resolves_timestamp(t *testing.T) {
hinter := &DmesgTimestampHinter{}
api := &TestAPI{}
// force system uptime to a known time for consistency in the test
sysStart = time.Date(2021, 5, 21, 1, 1, 1, 1, time.UTC)
input := `[47028.800212] audit: type=1107 audit(1621609618.710:91): pid=1011 uid=103 auid=4294967295 ses=4294967295 subj=unconfined msg='apparmor="DENIED" operation="dbus_signal" bus="system" path="/org/freedesktop/NetworkManager" interface="org.freedesktop.NetworkManager" member="PropertiesChanged" name=":1.12" mask="receive" pid=339538 label="snap.spotify.spotify" peer_pid=1012 peer_label="unconfined"`
match, offset, length := hinter.Match(input, 4)
assert.Equal(t, true, match)
hinter.Activate(api, input[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "Fri May 21 14:04:49 2021", api.highlighted)
}
func Test_dmesg_hinter_resolves_timestamp_then_clears(t *testing.T) {
hinter := &DmesgTimestampHinter{}
api := &TestAPI{}
// force system uptime to a known time for consistency in the test
sysStart = time.Date(2021, 5, 21, 1, 1, 1, 1, time.UTC)
input := `[47028.800212] audit: type=1107 audit(1621609618.710:91): pid=1011 uid=103 auid=4294967295 ses=4294967295 subj=unconfined msg='apparmor="DENIED" operation="dbus_signal" bus="system" path="/org/freedesktop/NetworkManager" interface="org.freedesktop.NetworkManager" member="PropertiesChanged" name=":1.12" mask="receive" pid=339538 label="snap.spotify.spotify" peer_pid=1012 peer_label="unconfined"`
match, offset, length := hinter.Match(input, 4)
assert.Equal(t, true, match)
hinter.Activate(api, input[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "Fri May 21 14:04:49 2021", api.highlighted)
hinter.Deactivate(api)
assert.Equal(t, "", api.highlighted)
}

View File

@ -0,0 +1,73 @@
package hinters
import (
"encoding/hex"
"fmt"
"image"
"image/color"
"regexp"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
func init() {
register(&HexColourHinter{}, PriorityLow)
}
var hexColourRegex = regexp.MustCompile(`#[0-9A-Fa-f]{6}`)
type HexColourHinter struct {
}
func (h *HexColourHinter) Match(text string, cursorIndex int) (matched bool, offset int, length int) {
matches := hexColourRegex.FindAllStringIndex(text, -1)
for _, match := range matches {
if match[0] <= cursorIndex && match[1] > cursorIndex {
return true, match[0], match[1] - match[0]
}
}
return
}
func (h *HexColourHinter) Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error {
colourBytes, err := hex.DecodeString(match[1:])
if err != nil {
return err
}
cellSize := api.CellSize()
size := image.Rectangle{image.Point{}, image.Point{
X: cellSize.X * 18,
Y: cellSize.Y,
}}
img := image.NewRGBA(size)
for x := 0; x < size.Dx(); x++ {
for y := 0; y < size.Dy(); y++ {
img.SetRGBA(x, y, color.RGBA{
R: colourBytes[0],
G: colourBytes[1],
B: colourBytes[2],
A: 0xff,
})
}
}
api.Highlight(start, end, fmt.Sprintf(
`Hex: %s
RGB: %d, %d, %d`,
match,
colourBytes[0],
colourBytes[1],
colourBytes[2],
), img)
return nil
}
func (h *HexColourHinter) Deactivate(api HintAPI) error {
api.ClearHighlight()
return nil
}
func (h *HexColourHinter) Click(api HintAPI) error {
return nil
}

View File

@ -0,0 +1,73 @@
package hinters
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
func init() {
register(&PermsHinter{}, PriorityLow)
}
type PermsHinter struct {
}
var permsMatcher = regexp.MustCompile("^[d|-][rwx-]{9}")
func (h *PermsHinter) Match(text string, cursorIndex int) (matched bool, offset int, length int) {
matches := permsMatcher.FindAllStringIndex(text, -1)
for _, match := range matches {
if match[0] <= cursorIndex && match[1] > cursorIndex {
return true, match[0], match[1] - match[0]
}
}
return
}
func (h *PermsHinter) Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error {
result, err := getPermsFromString(match)
if err != nil {
return err
}
api.Highlight(start, end, result, nil)
return nil
}
func getPermsFromString(match string) (string, error) {
match = strings.NewReplacer(
"r", "1",
"w", "1",
"x", "1",
"-", "0").
Replace(match)
return strings.Join([]string{
"0",
readPermPart(match, 1, 4),
readPermPart(match, 4, 7),
readPermPart(match, 7, 10),
}, ""), nil
}
func readPermPart(match string, from, to int) string {
permPart := match[from:to]
i, err := strconv.ParseInt(permPart, 2, 0)
if err != nil {
return "0"
}
return fmt.Sprint(strconv.FormatInt(i, 8))
}
func (h *PermsHinter) Deactivate(api HintAPI) error {
api.ClearHighlight()
return nil
}
func (h *PermsHinter) Click(api HintAPI) error {
return nil
}

View File

@ -0,0 +1,55 @@
package hinters
import (
"testing"
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_converter(t *testing.T) {
tests := map[string]string{
"dr-xr-xr-x": "0555",
"-r-xr-xr-x": "0555",
"drwxr-xr-x": "0755",
"-rwxr-xr-x": "0755",
"-r--------": "0400",
"-rw-------": "0600",
}
for perm, octet := range tests {
perm, err := getPermsFromString(perm)
require.NoError(t, err)
assert.Equal(t, octet, perm)
}
}
func Test_perm_hinter_resolves_from_string_and_correctly_activates(t *testing.T) {
hinter := &PermsHinter{}
api := &TestAPI{}
input := "dr-xr-xr-x"
match, offset, length := hinter.Match(input, 3)
assert.Equal(t, true, match)
hinter.Activate(api, input[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "0555", api.highlighted)
}
func Test_perm_hinter_resolves_from_string_and_correctly_activates_then_cleared(t *testing.T) {
hinter := &PermsHinter{}
api := &TestAPI{}
input := "drwxr-xr-x"
match, offset, length := hinter.Match(input, 3)
assert.Equal(t, true, match)
hinter.Activate(api, input[offset:offset+length], termutil.Position{}, termutil.Position{})
assert.Equal(t, "0755", api.highlighted)
hinter.Deactivate(api)
assert.Equal(t, "", api.highlighted)
}

View File

@ -0,0 +1,43 @@
package hinters
import (
"github.com/liamg/darktile/internal/app/darktile/termutil"
"github.com/skratchdot/open-golang/open"
"mvdan.cc/xurls"
)
func init() {
register(&URLHinter{}, PriorityHigh)
}
type URLHinter struct {
target string
}
func (h *URLHinter) Match(text string, cursorIndex int) (matched bool, offset int, length int) {
matches := xurls.Strict.FindAllStringIndex(text, -1)
for _, match := range matches {
if match[0] <= cursorIndex && match[1] > cursorIndex {
return true, match[0], match[1] - match[0]
}
}
return
}
func (h *URLHinter) Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error {
h.target = match
api.Highlight(start, end, "CTRL + CLICK: Open in browser", nil)
api.SetCursorToPointer()
return nil
}
func (h *URLHinter) Deactivate(api HintAPI) error {
api.ClearHighlight()
api.ResetCursor()
return nil
}
func (h *URLHinter) Click(api HintAPI) error {
api.ShowMessage("Launching URL in your browser...")
return open.Run(h.target)
}

View File

@ -0,0 +1,63 @@
package hinters
import (
"sort"
"sync"
"github.com/liamg/darktile/internal/app/darktile/termutil"
)
type HinterRegistration struct {
Priority Priority
Hinter Hinter
}
type Priority uint8
const (
PriorityNone = 0
PriorityVeryLow = 48
PriorityLow = 96
PriorityMedium = 128
PriorityHigh = 192
PriorityVeryHigh = 224
PriorityCritical = 255
)
type Hinter interface {
// Match should return the index in the text of the matched occurrence
Match(text string, cursorIndex int) (matched bool, offset int, length int)
// Activate fires when mouseover happens afer a match - takes raw coords
Activate(api HintAPI, match string, start termutil.Position, end termutil.Position) error
Deactivate(api HintAPI) error
Click(api HintAPI) error
}
var hintLock sync.RWMutex
var hinters []HinterRegistration
func register(h Hinter, p Priority) {
hintLock.Lock()
defer hintLock.Unlock()
hinters = append(hinters, HinterRegistration{
Priority: p,
Hinter: h,
})
}
func All() []Hinter {
hintLock.RLock()
defer hintLock.RUnlock()
var output []Hinter
sort.Slice(hinters, func(i, j int) bool {
return hinters[i].Priority > hinters[j].Priority
})
for _, hinter := range hinters {
output = append(output, hinter.Hinter)
}
return output
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
package sixel
import "image/color"
type ColourMap struct {
data [0x100]color.Color
}
func NewColourMap() *ColourMap {
return &ColourMap{}
}
func (m *ColourMap) GetColour(id uint8) color.Color {
return m.data[id]
}
func (m *ColourMap) SetColour(id uint8, c color.Color) {
m.data[id] = c
}
func (m *ColourMap) FindColour(colour color.Color) (uint8, bool) {
for id, c := range m.data {
if c == colour {
return uint8(id), true
}
}
return 0, false
}

View File

@ -0,0 +1,432 @@
package sixel
import (
"fmt"
"image"
"image/color"
"io"
"strconv"
"strings"
)
// See https://vt100.net/docs/vt3xx-gp/chapter14.html for more info.
type decoder struct {
r io.Reader
cursor image.Point
aspectRatio float64 // this is the ratio for vertical:horizontal pixels
bg color.Color
colourMap *ColourMap
currentColour color.Color
size image.Point // does not limit image size, just where bg is drawn!
scratchpad map[int]map[int]color.Color
}
func Decode(reader io.Reader, bg color.Color) (image.Image, error) {
return NewDecoder(reader, bg).Decode()
}
func NewDecoder(reader io.Reader, bg color.Color) *decoder {
return &decoder{
r: reader,
aspectRatio: 2,
bg: bg,
colourMap: NewColourMap(),
scratchpad: make(map[int]map[int]color.Color),
}
}
func (d *decoder) Decode() (image.Image, error) {
if err := d.processHeader(); err != nil {
return nil, fmt.Errorf("error reading sixel header: %s", err)
}
if err := d.processBody(); err != nil {
return nil, fmt.Errorf("error reading sixel header: %s", err)
}
return d.draw(), nil
}
func (d *decoder) readByte() (byte, error) {
buf := make([]byte, 1)
if _, err := d.r.Read(buf); err != nil {
return 0, err
}
return buf[0], nil
}
func (d *decoder) readHeader() ([]byte, error) {
var header []byte
for {
chr, err := d.readByte()
if err != nil {
return nil, err
}
if chr == 'q' {
break
}
header = append(header, chr)
}
return header, nil
}
func (d *decoder) processHeader() error {
data, err := d.readHeader()
if err != nil {
return err
}
header := string(data)
if len(header) == 0 {
return nil
}
params := strings.Split(header, ";")
switch params[1] {
case "0", "1", "5", "6", "":
d.aspectRatio = 2
case "2":
d.aspectRatio = 5
case "3", "4":
d.aspectRatio = 3
case "7", "8", "9":
d.aspectRatio = 1
default:
return fmt.Errorf("invalid P1 in sixel header")
}
if len(params) == 1 {
return nil
}
switch params[1] {
case "0", "2", "":
// use the configured terminal background colour
case "1":
d.bg = color.RGBA{A: 0} // transparent bg
}
// NOTE: we currently ignore P3 if it is specified
if len(params) > 3 {
return fmt.Errorf("unexpected extra parameters in sixel header")
}
return nil
}
func (d *decoder) processBody() error {
for {
byt, err := d.readByte()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
if err := d.processChar(byt); err != nil {
return err
}
}
}
func (d *decoder) handleRepeat() error {
var countStr string
for {
byt, err := d.readByte()
if err != nil {
return err
}
switch true {
case byt >= '0' && byt <= '9':
countStr += string(byt)
default:
count, err := strconv.Atoi(countStr)
if err != nil {
return fmt.Errorf("invalid count in sixel repeat sequence: %s: %s", countStr, err)
}
for i := 0; i < count; i++ {
if err := d.processDataChar(byt); err != nil {
return err
}
}
return nil
}
}
}
func (d *decoder) handleRasterAttributes() error {
var arg string
var args []string
for {
b, err := d.readByte()
if err != nil {
return err
}
switch true {
case b >= '0' && b <= '9':
arg += string(b)
case b == ';':
args = append(args, arg)
arg = ""
default:
args = append(args, arg)
if err := d.setRaster(args); err != nil {
return err
}
return d.processChar(b)
}
}
}
func (d *decoder) setRaster(args []string) error {
if len(args) != 4 {
return fmt.Errorf("invalid raster command: %s", strings.Join(args, ";"))
}
pan, err := strconv.Atoi(args[0])
if err != nil {
return err
}
pad, err := strconv.Atoi(args[1])
if err != nil {
return err
}
d.aspectRatio = float64(pan) / float64(pad)
ph, err := strconv.Atoi(args[2])
if err != nil {
return err
}
pv, err := strconv.Atoi(args[3])
if err != nil {
return err
}
d.size = image.Point{X: ph, Y: pv}
return nil
}
func (d *decoder) handleColour() error {
var arg string
var args []string
for {
b, err := d.readByte()
if err != nil {
return err
}
switch true {
case b >= '0' && b <= '9':
arg += string(b)
case b == ';':
args = append(args, arg)
arg = ""
default:
args = append(args, arg)
if err := d.setColour(args); err != nil {
return err
}
return d.processChar(b)
}
}
}
func (d *decoder) setColour(args []string) error {
if len(args) == 0 {
return fmt.Errorf("invalid colour string - missing identifier")
}
colourID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid colour id: %s", args[0])
}
if len(args) == 1 {
d.currentColour = d.colourMap.GetColour(uint8(colourID))
return nil
}
if len(args) != 5 {
return fmt.Errorf("invalid colour introduction command - wrong number of args (%d): %s", len(args), strings.Join(args, ";"))
}
x, err := strconv.Atoi(args[2])
if err != nil {
return fmt.Errorf("invalid colour value")
}
y, err := strconv.Atoi(args[3])
if err != nil {
return fmt.Errorf("invalid colour value")
}
z, err := strconv.Atoi(args[4])
if err != nil {
return fmt.Errorf("invalid colour value")
}
var colour color.Color
switch args[1] {
case "1":
colour = colourFromHSL(x, z, y)
case "2":
colour = color.RGBA{
R: uint8((x * 255) / 100),
G: uint8((y * 255) / 100),
B: uint8((z * 255) / 100),
A: 0xff,
}
default:
return fmt.Errorf("invalid colour co-ordinate system '%s'", args[1])
}
d.colourMap.SetColour(uint8(colourID), colour)
d.currentColour = colour
return nil
}
func (d *decoder) processChar(b byte) error {
switch b {
case '!':
return d.handleRepeat()
case '"':
return d.handleRasterAttributes()
case '#':
return d.handleColour()
case '$':
// graphics carriage return
d.cursor.X = 0
return nil
case '-':
// graphics new line
d.cursor.Y += 6
d.cursor.X = 0
return nil
default:
return d.processDataChar(b)
}
}
func (d *decoder) processDataChar(b byte) error {
if b < 0x3f || b > 0x7e {
return fmt.Errorf("invalid sixel data value 0x%02x: outside acceptable range", b)
}
sixel := b - 0x3f
for i := 0; i < 6; i++ {
if sixel&(1<<i) > 0 {
d.set(d.cursor.X, d.cursor.Y+i)
}
}
d.cursor.X++
return nil
}
func hueToRGB(v1, v2, h float64) float64 {
if h < 0 {
h += 1
}
if h > 1 {
h -= 1
}
switch {
case 6*h < 1:
return (v1 + (v2-v1)*6*h)
case 2*h < 1:
return v2
case 3*h < 2:
return v1 + (v2-v1)*((2.0/3.0)-h)*6
}
return v1
}
func colourFromHSL(hi, si, li int) color.Color {
h := float64(hi) / 360
s := float64(si) / 100
l := float64(li) / 100
if s == 0 {
// it's gray
return color.RGBA{uint8(l * 0xff), uint8(l * 0xff), uint8(l * 0xff), 0xff}
}
var v1, v2 float64
if l < 0.5 {
v2 = l * (1 + s)
} else {
v2 = (l + s) - (s * l)
}
v1 = 2*l - v2
r := hueToRGB(v1, v2, h+(1.0/3.0))
g := hueToRGB(v1, v2, h)
b := hueToRGB(v1, v2, h-(1.0/3.0))
return color.RGBA{R: uint8(r * 0xff), G: uint8(g * 0xff), B: uint8(b * 0xff), A: 0xff}
}
func (d *decoder) set(x, y int) {
if x > d.size.X {
d.size.X = x
}
if y > d.size.Y {
d.size.Y = y
}
if _, ok := d.scratchpad[x]; !ok {
d.scratchpad[x] = make(map[int]color.Color)
}
d.scratchpad[x][y] = d.currentColour
}
func (d *decoder) draw() image.Image {
img := image.NewRGBA(image.Rect(0, 0, d.size.X, d.size.Y))
for x := 0; x < d.size.X; x++ {
for y := 0; y < d.size.Y; y++ {
c := d.bg
if col, ok := d.scratchpad[x]; ok {
if row, ok := col[y]; ok {
c = row
}
}
img.Set(x, y, c)
}
}
return img
}

View File

@ -0,0 +1,106 @@
package termutil
func (t *Terminal) handleANSI(readChan chan MeasuredRune) (renderRequired bool) {
// if the byte is an escape character, read the next byte to determine which one
r := <-readChan
t.log("ANSI SEQ %c 0x%X", r.Rune, r.Rune)
switch r.Rune {
case '[':
return t.handleCSI(readChan)
case ']':
return t.handleOSC(readChan)
case '(':
return t.handleSCS0(readChan) // select character set into G0
case ')':
return t.handleSCS1(readChan) // select character set into G1
case '*':
return swallowHandler(1)(readChan) // character set bullshit
case '+':
return swallowHandler(1)(readChan) // character set bullshit
case '>':
return swallowHandler(0)(readChan) // numeric char selection
case '=':
return swallowHandler(0)(readChan) // alt char selection
case '7':
t.GetActiveBuffer().saveCursor()
case '8':
t.GetActiveBuffer().restoreCursor()
case 'D':
t.GetActiveBuffer().index()
case 'E':
t.GetActiveBuffer().newLineEx(true)
case 'H':
t.GetActiveBuffer().tabSetAtCursor()
case 'M':
t.GetActiveBuffer().reverseIndex()
case 'P': // sixel
t.handleSixel(readChan)
case 'c':
t.GetActiveBuffer().clear()
case '#':
return t.handleScreenState(readChan)
case '^':
return t.handlePrivacyMessage(readChan)
default:
t.log("UNKNOWN ESCAPE SEQUENCE: 0x%X", r.Rune)
return false
}
return true
}
func swallowHandler(size int) func(pty chan MeasuredRune) bool {
return func(pty chan MeasuredRune) bool {
for i := 0; i < size; i++ {
<-pty
}
return false
}
}
func (t *Terminal) handleScreenState(readChan chan MeasuredRune) bool {
b := <-readChan
switch b.Rune {
case '8': // DECALN -- Screen Alignment Pattern
// hide cursor?
buffer := t.GetActiveBuffer()
buffer.resetVerticalMargins(uint(buffer.viewHeight))
buffer.SetScrollOffset(0)
// Fill the whole screen with E's
count := buffer.ViewHeight() * buffer.ViewWidth()
for count > 0 {
buffer.write(MeasuredRune{Rune: 'E', Width: 1})
count--
if count > 0 && !buffer.modes.AutoWrap && count%buffer.ViewWidth() == 0 {
buffer.index()
buffer.carriageReturn()
}
}
// restore cursor
buffer.setPosition(0, 0)
default:
return false
}
return true
}
func (t *Terminal) handlePrivacyMessage(readChan chan MeasuredRune) bool {
isEscaped := false
for {
b := <-readChan
if b.Rune == 0x18 /*CAN*/ || b.Rune == 0x1a /*SUB*/ || (b.Rune == 0x5c /*backslash*/ && isEscaped) {
break
}
if isEscaped {
isEscaped = false
} else if b.Rune == 0x1b {
isEscaped = true
continue
}
}
return false
}

View File

@ -0,0 +1,877 @@
package termutil
import (
"image"
"image/color"
)
const TabSize = 8
type Buffer struct {
lines []Line
savedCursorPos Position
savedCursorAttr *CellAttributes
savedCharsets []*map[rune]rune
savedCurrentCharset int
topMargin uint // see DECSTBM docs - this is for scrollable regions
bottomMargin uint // see DECSTBM docs - this is for scrollable regions
viewWidth uint16
viewHeight uint16
cursorPosition Position // raw
cursorAttr CellAttributes
scrollLinesFromBottom uint
maxLines uint64
tabStops []uint16
charsets []*map[rune]rune // array of 2 charsets, nil means ASCII (no conversion)
currentCharset int // active charset index in charsets array, valid values are 0 or 1
modes Modes
mouseMode MouseMode
mouseExtMode MouseExtMode
selectionStart *Position
selectionEnd *Position
highlightStart *Position
highlightEnd *Position
highlightAnnotation *Annotation
sixels []Sixel
}
type Annotation struct {
Image image.Image
Text string
Width float64 // Width in cells
Height float64 // Height in cells
}
type Selection struct {
Start Position
End Position
}
type Position struct {
Line uint64
Col uint16
}
// NewBuffer creates a new terminal buffer
func NewBuffer(width, height uint16, maxLines uint64, fg color.Color, bg color.Color) *Buffer {
b := &Buffer{
lines: []Line{},
viewHeight: height,
viewWidth: width,
maxLines: maxLines,
topMargin: 0,
bottomMargin: uint(height - 1),
cursorAttr: CellAttributes{
fgColour: fg,
bgColour: bg,
},
charsets: []*map[rune]rune{nil, nil},
modes: Modes{
LineFeedMode: true,
AutoWrap: true,
ShowCursor: true,
SixelScrolling: true,
},
}
return b
}
func (buffer *Buffer) IsCursorVisible() bool {
return buffer.modes.ShowCursor
}
func (buffer *Buffer) GetMouseMode() MouseMode {
return buffer.mouseMode
}
func (buffer *Buffer) GetMouseExtMode() MouseExtMode {
return buffer.mouseExtMode
}
func (buffer *Buffer) IsApplicationCursorKeysModeEnabled() bool {
return buffer.modes.ApplicationCursorKeys
}
func (buffer *Buffer) HasScrollableRegion() bool {
return buffer.topMargin > 0 || buffer.bottomMargin < uint(buffer.ViewHeight())-1
}
func (buffer *Buffer) InScrollableRegion() bool {
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
return buffer.HasScrollableRegion() && uint(cursorVY) >= buffer.topMargin && uint(cursorVY) <= buffer.bottomMargin
}
// NOTE: bottom is exclusive
func (buffer *Buffer) getAreaScrollRange() (top uint64, bottom uint64) {
top = buffer.convertViewLineToRawLine(uint16(buffer.topMargin))
bottom = buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin)) + 1
if bottom > uint64(len(buffer.lines)) {
bottom = uint64(len(buffer.lines))
}
return top, bottom
}
func (buffer *Buffer) areaScrollDown(lines uint16) {
// NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange()
for i := bottom; i > top; {
i--
if i >= top+uint64(lines) {
buffer.lines[i] = buffer.lines[i-uint64(lines)]
} else {
buffer.lines[i] = newLine()
}
}
}
func (buffer *Buffer) areaScrollUp(lines uint16) {
// NOTE: bottom is exclusive
top, bottom := buffer.getAreaScrollRange()
for i := top; i < bottom; i++ {
from := i + uint64(lines)
if from < bottom {
buffer.lines[i] = buffer.lines[from]
} else {
buffer.lines[i] = newLine()
}
}
}
func (buffer *Buffer) saveCursor() {
copiedAttr := buffer.cursorAttr
buffer.savedCursorAttr = &copiedAttr
buffer.savedCursorPos = buffer.cursorPosition
buffer.savedCharsets = make([]*map[rune]rune, len(buffer.charsets))
copy(buffer.savedCharsets, buffer.charsets)
buffer.savedCurrentCharset = buffer.currentCharset
}
func (buffer *Buffer) restoreCursor() {
// TODO: Do we need to restore attributes on cursor restore? conflicting sources but vim + htop work better without doing so
//if buffer.savedCursorAttr != nil {
// copiedAttr := *buffer.savedCursorAttr
// copiedAttr.bgColour = buffer.defaultCell(false).attr.bgColour
// copiedAttr.fgColour = buffer.defaultCell(false).attr.fgColour
// buffer.cursorAttr = copiedAttr
//}
buffer.cursorPosition = buffer.savedCursorPos
if buffer.savedCharsets != nil {
buffer.charsets = make([]*map[rune]rune, len(buffer.savedCharsets))
copy(buffer.charsets, buffer.savedCharsets)
buffer.currentCharset = buffer.savedCurrentCharset
}
}
func (buffer *Buffer) getCursorAttr() *CellAttributes {
return &buffer.cursorAttr
}
func (buffer *Buffer) GetCell(viewCol uint16, viewRow uint16) *Cell {
rawLine := buffer.convertViewLineToRawLine(viewRow)
return buffer.getRawCell(viewCol, rawLine)
}
func (buffer *Buffer) getRawCell(viewCol uint16, rawLine uint64) *Cell {
if rawLine >= uint64(len(buffer.lines)) {
return nil
}
line := &buffer.lines[rawLine]
if int(viewCol) >= len(line.cells) {
return nil
}
return &line.cells[viewCol]
}
// Column returns cursor column
func (buffer *Buffer) CursorColumn() uint16 {
// @todo originMode and left margin
return buffer.cursorPosition.Col
}
// CursorLineAbsolute returns absolute cursor line coordinate (ignoring Origin Mode) - view format
func (buffer *Buffer) CursorLineAbsolute() uint16 {
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
return cursorVY
}
// CursorLine returns cursor line (in Origin Mode it is relative to the top margin)
func (buffer *Buffer) CursorLine() uint16 {
if buffer.modes.OriginMode {
return buffer.CursorLineAbsolute() - uint16(buffer.topMargin)
}
return buffer.CursorLineAbsolute()
}
func (buffer *Buffer) TopMargin() uint {
return buffer.topMargin
}
func (buffer *Buffer) BottomMargin() uint {
return buffer.bottomMargin
}
// cursor Y (raw)
func (buffer *Buffer) RawLine() uint64 {
return buffer.cursorPosition.Line
}
func (buffer *Buffer) convertViewLineToRawLine(viewLine uint16) uint64 {
rawHeight := buffer.Height()
if int(buffer.viewHeight) > rawHeight {
return uint64(viewLine)
}
return uint64(int(viewLine) + (rawHeight - int(buffer.viewHeight+uint16(buffer.scrollLinesFromBottom))))
}
func (buffer *Buffer) convertRawLineToViewLine(rawLine uint64) uint16 {
rawHeight := buffer.Height()
if int(buffer.viewHeight) > rawHeight {
return uint16(rawLine)
}
return uint16(int(rawLine) - (rawHeight - int(buffer.viewHeight+uint16(buffer.scrollLinesFromBottom))))
}
func (buffer *Buffer) GetVPosition() int {
result := int(uint(buffer.Height()) - uint(buffer.ViewHeight()) - buffer.scrollLinesFromBottom)
if result < 0 {
result = 0
}
return result
}
// Width returns the width of the buffer in columns
func (buffer *Buffer) Width() uint16 {
return buffer.viewWidth
}
func (buffer *Buffer) ViewWidth() uint16 {
return buffer.viewWidth
}
func (buffer *Buffer) Height() int {
return len(buffer.lines)
}
func (buffer *Buffer) ViewHeight() uint16 {
return buffer.viewHeight
}
func (buffer *Buffer) deleteLine() {
index := int(buffer.RawLine())
buffer.lines = buffer.lines[:index+copy(buffer.lines[index:], buffer.lines[index+1:])]
}
func (buffer *Buffer) insertLine() {
if !buffer.InScrollableRegion() {
pos := buffer.RawLine()
maxLines := buffer.GetMaxLines()
newLineCount := uint64(len(buffer.lines) + 1)
if newLineCount > maxLines {
newLineCount = maxLines
}
out := make([]Line, newLineCount)
copy(
out[:pos-(uint64(len(buffer.lines))+1-newLineCount)],
buffer.lines[uint64(len(buffer.lines))+1-newLineCount:pos])
out[pos] = newLine()
copy(out[pos+1:], buffer.lines[pos:])
buffer.lines = out
} else {
topIndex := buffer.convertViewLineToRawLine(uint16(buffer.topMargin))
bottomIndex := buffer.convertViewLineToRawLine(uint16(buffer.bottomMargin))
before := buffer.lines[:topIndex]
after := buffer.lines[bottomIndex+1:]
out := make([]Line, len(buffer.lines))
copy(out[0:], before)
pos := buffer.RawLine()
for i := topIndex; i < bottomIndex; i++ {
if i < pos {
out[i] = buffer.lines[i]
} else {
out[i+1] = buffer.lines[i]
}
}
copy(out[bottomIndex+1:], after)
out[pos] = newLine()
buffer.lines = out
}
}
func (buffer *Buffer) insertBlankCharacters(count int) {
index := int(buffer.RawLine())
for i := 0; i < count; i++ {
cells := buffer.lines[index].cells
buffer.lines[index].cells = append(cells[:buffer.cursorPosition.Col], append([]Cell{buffer.defaultCell(true)}, cells[buffer.cursorPosition.Col:]...)...)
}
}
func (buffer *Buffer) insertLines(count int) {
if buffer.HasScrollableRegion() && !buffer.InScrollableRegion() {
// should have no effect outside of scrollable region
return
}
buffer.cursorPosition.Col = 0
for i := 0; i < count; i++ {
buffer.insertLine()
}
}
func (buffer *Buffer) deleteLines(count int) {
if buffer.HasScrollableRegion() && !buffer.InScrollableRegion() {
// should have no effect outside of scrollable region
return
}
buffer.cursorPosition.Col = 0
for i := 0; i < count; i++ {
buffer.deleteLine()
}
}
func (buffer *Buffer) index() {
// This sequence causes the active position to move downward one line without changing the column position.
// If the active position is at the bottom margin, a scroll up is performed."
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
if buffer.InScrollableRegion() {
if uint(cursorVY) < buffer.bottomMargin {
buffer.cursorPosition.Line++
} else {
buffer.areaScrollUp(1)
}
return
}
if cursorVY >= buffer.ViewHeight()-1 {
buffer.lines = append(buffer.lines, newLine())
maxLines := buffer.GetMaxLines()
if uint64(len(buffer.lines)) > maxLines {
copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:])
buffer.lines = buffer.lines[:maxLines]
}
}
buffer.cursorPosition.Line++
}
func (buffer *Buffer) reverseIndex() {
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
if uint(cursorVY) == buffer.topMargin {
buffer.areaScrollDown(1)
} else if cursorVY > 0 {
buffer.cursorPosition.Line--
}
}
// write will write a rune to the terminal at the position of the cursor, and increment the cursor position
func (buffer *Buffer) write(runes ...MeasuredRune) {
// scroll to bottom on input
buffer.scrollLinesFromBottom = 0
for _, r := range runes {
line := buffer.getCurrentLine()
if buffer.modes.ReplaceMode {
if buffer.CursorColumn() >= buffer.Width() {
if buffer.modes.AutoWrap {
buffer.cursorPosition.Line++
buffer.cursorPosition.Col = 0
line = buffer.getCurrentLine()
} else {
// no more room on line and wrapping is disabled
return
}
}
for int(buffer.CursorColumn()) >= len(line.cells) {
line.append(buffer.defaultCell(int(buffer.CursorColumn()) == len(line.cells)))
}
line.cells[buffer.cursorPosition.Col].attr = buffer.cursorAttr
line.cells[buffer.cursorPosition.Col].setRune(r)
buffer.incrementCursorPosition()
continue
}
if buffer.CursorColumn() >= buffer.Width() { // if we're after the line, move to next
if buffer.modes.AutoWrap {
buffer.newLineEx(true)
newLine := buffer.getCurrentLine()
if len(newLine.cells) == 0 {
newLine.append(buffer.defaultCell(true))
}
cell := &newLine.cells[0]
cell.setRune(r)
cell.attr = buffer.cursorAttr
} else {
// no more room on line and wrapping is disabled
return
}
} else {
for int(buffer.CursorColumn()) >= len(line.cells) {
line.append(buffer.defaultCell(int(buffer.CursorColumn()) == len(line.cells)))
}
cell := &line.cells[buffer.CursorColumn()]
cell.setRune(r)
cell.attr = buffer.cursorAttr
}
buffer.incrementCursorPosition()
}
}
func (buffer *Buffer) incrementCursorPosition() {
// 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() {
buffer.cursorPosition.Col++
}
}
func (buffer *Buffer) inDoWrap() bool {
// xterm uses 'do_wrap' flag for this special terminal state
// we use the cursor position right after the boundary
// let's see how it works out
return buffer.cursorPosition.Col == buffer.viewWidth // @todo rightMargin
}
func (buffer *Buffer) backspace() {
if buffer.cursorPosition.Col == 0 {
line := buffer.getCurrentLine()
if line.wrapped {
buffer.movePosition(int16(buffer.Width()-1), -1)
}
} else if buffer.inDoWrap() {
// the "do_wrap" implementation
buffer.movePosition(-2, 0)
} else {
buffer.movePosition(-1, 0)
}
}
func (buffer *Buffer) carriageReturn() {
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
for {
line := buffer.getCurrentLine()
if line == nil {
break
}
if line.wrapped && cursorVY > 0 {
buffer.cursorPosition.Line--
} else {
break
}
}
buffer.cursorPosition.Col = 0
}
func (buffer *Buffer) tab() {
tabStop := buffer.getNextTabStopAfter(buffer.cursorPosition.Col)
for buffer.cursorPosition.Col < tabStop && buffer.cursorPosition.Col < buffer.viewWidth-1 { // @todo rightMargin
buffer.write(MeasuredRune{Rune: ' ', Width: 1})
}
}
// return next tab stop x pos
func (buffer *Buffer) getNextTabStopAfter(col uint16) uint16 {
defaultStop := col + (TabSize - (col % TabSize))
if defaultStop == col {
defaultStop += TabSize
}
var low uint16
for _, stop := range buffer.tabStops {
if stop > col {
if stop < low || low == 0 {
low = stop
}
}
}
if low == 0 {
return defaultStop
}
return low
}
func (buffer *Buffer) newLine() {
buffer.newLineEx(false)
}
func (buffer *Buffer) verticalTab() {
buffer.index()
for {
line := buffer.getCurrentLine()
if !line.wrapped {
break
}
buffer.index()
}
}
func (buffer *Buffer) newLineEx(forceCursorToMargin bool) {
if buffer.IsNewLineMode() || forceCursorToMargin {
buffer.cursorPosition.Col = 0
}
buffer.index()
for {
line := buffer.getCurrentLine()
if !line.wrapped {
break
}
buffer.index()
}
}
func (buffer *Buffer) movePosition(x int16, y int16) {
var toX uint16
var toY uint16
if int16(buffer.CursorColumn())+x < 0 {
toX = 0
} else {
toX = uint16(int16(buffer.CursorColumn()) + x)
}
// should either use CursorLine() and setPosition() or use absolutes, mind Origin Mode (DECOM)
if int16(buffer.CursorLine())+y < 0 {
toY = 0
} else {
toY = uint16(int16(buffer.CursorLine()) + y)
}
buffer.setPosition(toX, toY)
}
func (buffer *Buffer) setPosition(col uint16, line uint16) {
useCol := col
useLine := line
maxLine := buffer.ViewHeight() - 1
if buffer.modes.OriginMode {
useLine += uint16(buffer.topMargin)
maxLine = uint16(buffer.bottomMargin)
// @todo left and right margins
}
if useLine > maxLine {
useLine = maxLine
}
if useCol >= buffer.ViewWidth() {
useCol = buffer.ViewWidth() - 1
}
buffer.cursorPosition.Col = useCol
buffer.cursorPosition.Line = buffer.convertViewLineToRawLine(useLine)
}
func (buffer *Buffer) GetVisibleLines() []Line {
lines := []Line{}
for i := buffer.Height() - int(buffer.ViewHeight()); i < buffer.Height(); i++ {
y := i - int(buffer.scrollLinesFromBottom)
if y >= 0 && y < len(buffer.lines) {
lines = append(lines, buffer.lines[y])
}
}
return lines
}
// tested to here
func (buffer *Buffer) clear() {
for i := 0; i < int(buffer.ViewHeight()); i++ {
buffer.lines = append(buffer.lines, newLine())
}
buffer.setPosition(0, 0)
}
// creates if necessary
func (buffer *Buffer) getCurrentLine() *Line {
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
return buffer.getViewLine(cursorVY)
}
func (buffer *Buffer) getViewLine(index uint16) *Line {
if index >= buffer.ViewHeight() {
return &buffer.lines[len(buffer.lines)-1]
}
if len(buffer.lines) < int(buffer.ViewHeight()) {
for int(index) >= len(buffer.lines) {
buffer.lines = append(buffer.lines, newLine())
}
return &buffer.lines[int(index)]
}
if raw := int(buffer.convertViewLineToRawLine(index)); raw < len(buffer.lines) {
return &buffer.lines[raw]
}
return nil
}
func (buffer *Buffer) eraseLine() {
buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line)
line := buffer.getCurrentLine()
for i := 0; i < int(buffer.viewWidth); i++ {
if i >= len(line.cells) {
line.cells = append(line.cells, buffer.defaultCell(false))
} else {
line.cells[i] = buffer.defaultCell(false)
}
}
}
func (buffer *Buffer) eraseLineToCursor() {
buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line)
line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.cursorPosition.Col); i++ {
if i < len(line.cells) {
line.cells[i].erase(buffer.cursorAttr.bgColour)
}
}
}
func (buffer *Buffer) eraseLineFromCursor() {
buffer.clearSixelsAtRawLine(buffer.cursorPosition.Line)
line := buffer.getCurrentLine()
for i := buffer.cursorPosition.Col; i < buffer.viewWidth; i++ {
if int(i) >= len(line.cells) {
line.cells = append(line.cells, buffer.defaultCell(false))
} else {
line.cells[i] = buffer.defaultCell(false)
}
}
}
func (buffer *Buffer) eraseDisplay() {
for i := uint16(0); i < (buffer.ViewHeight()); i++ {
rawLine := buffer.convertViewLineToRawLine(i)
buffer.clearSixelsAtRawLine(rawLine)
if int(rawLine) < len(buffer.lines) {
buffer.lines[int(rawLine)].cells = []Cell{}
}
}
}
func (buffer *Buffer) deleteChars(n int) {
line := buffer.getCurrentLine()
if int(buffer.cursorPosition.Col) >= len(line.cells) {
return
}
before := line.cells[:buffer.cursorPosition.Col]
if int(buffer.cursorPosition.Col)+n >= len(line.cells) {
n = len(line.cells) - int(buffer.cursorPosition.Col)
}
after := line.cells[int(buffer.cursorPosition.Col)+n:]
line.cells = append(before, after...)
}
func (buffer *Buffer) eraseCharacters(n int) {
line := buffer.getCurrentLine()
max := int(buffer.cursorPosition.Col) + n
if max > len(line.cells) {
max = len(line.cells)
}
for i := int(buffer.cursorPosition.Col); i < max; i++ {
line.cells[i].erase(buffer.cursorAttr.bgColour)
}
}
func (buffer *Buffer) eraseDisplayFromCursor() {
line := buffer.getCurrentLine()
max := int(buffer.cursorPosition.Col)
if max > len(line.cells) {
max = len(line.cells)
}
line.cells = line.cells[:max]
for rawLine := buffer.cursorPosition.Line + 1; int(rawLine) < len(buffer.lines); rawLine++ {
buffer.clearSixelsAtRawLine(rawLine)
buffer.lines[int(rawLine)].cells = []Cell{}
}
}
func (buffer *Buffer) eraseDisplayToCursor() {
line := buffer.getCurrentLine()
for i := 0; i <= int(buffer.cursorPosition.Col); i++ {
if i >= len(line.cells) {
break
}
line.cells[i].erase(buffer.cursorAttr.bgColour)
}
cursorVY := buffer.convertRawLineToViewLine(buffer.cursorPosition.Line)
for i := uint16(0); i < cursorVY; i++ {
rawLine := buffer.convertViewLineToRawLine(i)
buffer.clearSixelsAtRawLine(rawLine)
if int(rawLine) < len(buffer.lines) {
buffer.lines[int(rawLine)].cells = []Cell{}
}
}
}
func (buffer *Buffer) GetMaxLines() uint64 {
result := buffer.maxLines
if result < uint64(buffer.viewHeight) {
result = uint64(buffer.viewHeight)
}
return result
}
func (buffer *Buffer) setVerticalMargins(top uint, bottom uint) {
buffer.topMargin = top
buffer.bottomMargin = bottom
}
// resetVerticalMargins resets margins to extreme positions
func (buffer *Buffer) resetVerticalMargins(height uint) {
buffer.setVerticalMargins(0, height-1)
}
func (buffer *Buffer) defaultCell(applyEffects bool) Cell {
attr := buffer.cursorAttr
if !applyEffects {
attr.blink = false
attr.bold = false
attr.dim = false
attr.inverse = false
attr.underline = false
attr.dim = false
}
return Cell{attr: attr}
}
func (buffer *Buffer) IsNewLineMode() bool {
return !buffer.modes.LineFeedMode
}
func (buffer *Buffer) tabReset() {
buffer.tabStops = nil
}
func (buffer *Buffer) tabSet(index uint16) {
buffer.tabStops = append(buffer.tabStops, index)
}
func (buffer *Buffer) tabClear(index uint16) {
var filtered []uint16
for _, stop := range buffer.tabStops {
if stop != buffer.cursorPosition.Col {
filtered = append(filtered, stop)
}
}
buffer.tabStops = filtered
}
func (buffer *Buffer) IsTabSetAtCursor() bool {
if buffer.cursorPosition.Col%TabSize > 0 {
return false
}
for _, stop := range buffer.tabStops {
if stop == buffer.cursorPosition.Col {
return true
}
}
return false
}
func (buffer *Buffer) tabClearAtCursor() {
buffer.tabClear(buffer.cursorPosition.Col)
}
func (buffer *Buffer) tabSetAtCursor() {
buffer.tabSet(buffer.cursorPosition.Col)
}
func (buffer *Buffer) GetScrollOffset() uint {
return buffer.scrollLinesFromBottom
}
func (buffer *Buffer) SetScrollOffset(offset uint) {
buffer.scrollLinesFromBottom = offset
}
func (buffer *Buffer) ScrollToEnd() {
buffer.scrollLinesFromBottom = 0
}
func (buffer *Buffer) ScrollUp(lines uint) {
if int(buffer.scrollLinesFromBottom)+int(lines) < len(buffer.lines)-int(buffer.viewHeight) {
buffer.scrollLinesFromBottom += lines
} else {
lines := len(buffer.lines) - int(buffer.viewHeight)
if lines < 0 {
lines = 0
}
buffer.scrollLinesFromBottom = uint(lines)
}
}
func (buffer *Buffer) ScrollDown(lines uint) {
if int(buffer.scrollLinesFromBottom)-int(lines) >= 0 {
buffer.scrollLinesFromBottom -= lines
} else {
buffer.scrollLinesFromBottom = 0
}
}

View File

@ -0,0 +1,688 @@
package termutil
import (
"image/color"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)
func writeRaw(buf *Buffer, runes ...rune) {
for _, r := range runes {
buf.write(MeasuredRune{Rune: r, Width: 1})
}
}
func TestBufferCreation(t *testing.T) {
b := makeBufferForTesting(10, 20)
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(20), b.ViewHeight())
assert.Equal(t, uint16(0), b.CursorColumn())
assert.Equal(t, uint16(0), b.CursorLine())
assert.NotNil(t, b.lines)
}
func TestNewLine(t *testing.T) {
b := makeBufferForTesting(30, 3)
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("goodbye")...)
b.carriageReturn()
b.newLine()
expected := `
hello
goodbye
`
lines := b.GetVisibleLines()
strs := []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(strings.Join(strs, "\n")))
}
func TestTabbing(t *testing.T) {
b := makeBufferForTesting(30, 3)
writeRaw(b, []rune("hello")...)
b.tab()
writeRaw(b, []rune("x")...)
b.tab()
writeRaw(b, []rune("goodbye")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hell")...)
b.tab()
writeRaw(b, []rune("xxx")...)
b.tab()
writeRaw(b, []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.TrimSpace(strings.Join(strs, "\n")))
}
func TestOffsets(t *testing.T) {
b := makeBufferForTesting(10, 3)
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello")...)
assert.Equal(t, uint16(10), b.ViewWidth())
assert.Equal(t, uint16(10), b.Width())
assert.Equal(t, uint16(3), b.ViewHeight())
assert.Equal(t, 5, b.Height())
}
func TestBufferWriteIncrementsCursorCorrectly(t *testing.T) {
b := makeBufferForTesting(5, 4)
/*01234
|-----
0|xxxxx
1|
2|
3|
|-----
*/
writeRaw(b, 'x')
require.Equal(t, uint16(1), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(2), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(3), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(4), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(5), b.CursorColumn())
require.Equal(t, uint16(0), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(1), b.CursorColumn())
require.Equal(t, uint16(1), b.CursorLine())
writeRaw(b, 'x')
require.Equal(t, uint16(2), b.CursorColumn())
require.Equal(t, uint16(1), b.CursorLine())
lines := b.GetVisibleLines()
require.Equal(t, 2, len(lines))
assert.Equal(t, "xxxxx", lines[0].String())
assert.Equal(t, "xx", lines[1].String())
}
func TestWritingNewLineAsFirstRuneOnWrappedLine(t *testing.T) {
b := makeBufferForTesting(3, 20)
b.modes.LineFeedMode = false
writeRaw(b, 'a', 'b', 'c')
assert.Equal(t, uint16(3), b.cursorPosition.Col)
assert.Equal(t, uint64(0), b.cursorPosition.Line)
b.newLine()
assert.Equal(t, uint16(0), b.cursorPosition.Col)
assert.Equal(t, uint64(1), b.cursorPosition.Line)
writeRaw(b, 'd', 'e', 'f')
assert.Equal(t, uint16(3), b.cursorPosition.Col)
assert.Equal(t, uint64(1), b.cursorPosition.Line)
b.newLine()
assert.Equal(t, uint16(0), b.cursorPosition.Col)
assert.Equal(t, uint64(2), b.cursorPosition.Line)
require.Equal(t, 3, len(b.lines))
assert.Equal(t, "abc", b.lines[0].String())
assert.Equal(t, "def", b.lines[1].String())
}
func TestWritingNewLineAsSecondRuneOnWrappedLine(t *testing.T) {
b := makeBufferForTesting(3, 20)
b.modes.LineFeedMode = false
/*
|abc
|d
|ef
|
|
|z
*/
writeRaw(b, 'a', 'b', 'c', 'd')
b.newLine()
writeRaw(b, 'e', 'f')
b.newLine()
b.newLine()
b.newLine()
writeRaw(b, 'z')
assert.Equal(t, "abc", b.lines[0].String())
assert.Equal(t, "d", b.lines[1].String())
assert.Equal(t, "ef", b.lines[2].String())
assert.Equal(t, "", b.lines[3].String())
assert.Equal(t, "", b.lines[4].String())
assert.Equal(t, "z", b.lines[5].String())
}
func TestSetPosition(t *testing.T) {
b := makeBufferForTesting(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.setPosition(60, 10)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 10, int(b.CursorLine()))
b.setPosition(0, 0)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.setPosition(120, 90)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
}
func TestMovePosition(t *testing.T) {
b := makeBufferForTesting(120, 80)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.movePosition(-1, -1)
assert.Equal(t, 0, int(b.CursorColumn()))
assert.Equal(t, 0, int(b.CursorLine()))
b.movePosition(30, 20)
assert.Equal(t, 30, int(b.CursorColumn()))
assert.Equal(t, 20, int(b.CursorLine()))
b.movePosition(30, 20)
assert.Equal(t, 60, int(b.CursorColumn()))
assert.Equal(t, 40, int(b.CursorLine()))
b.movePosition(-1, -1)
assert.Equal(t, 59, int(b.CursorColumn()))
assert.Equal(t, 39, int(b.CursorLine()))
b.movePosition(100, 100)
assert.Equal(t, 119, int(b.CursorColumn()))
assert.Equal(t, 79, int(b.CursorLine()))
}
func TestVisibleLines(t *testing.T) {
b := makeBufferForTesting(80, 10)
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 2")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 3")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 4")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 5")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 6")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 7")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 8")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 9")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 10")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 11")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 12")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 13")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 14")...)
lines := b.GetVisibleLines()
require.Equal(t, 10, len(lines))
assert.Equal(t, "hello 5", lines[0].String())
assert.Equal(t, "hello 14", lines[9].String())
}
func TestClearWithoutFullView(t *testing.T) {
b := makeBufferForTesting(80, 10)
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.clear()
lines := b.GetVisibleLines()
for _, line := range lines {
assert.Equal(t, "", line.String())
}
}
func TestClearWithFullView(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("hello 1")...)
b.clear()
lines := b.GetVisibleLines()
for _, line := range lines {
assert.Equal(t, "", line.String())
}
}
func TestCarriageReturn(t *testing.T) {
b := makeBufferForTesting(80, 20)
writeRaw(b, []rune("hello!")...)
b.carriageReturn()
writeRaw(b, []rune("secret")...)
lines := b.GetVisibleLines()
assert.Equal(t, "secret", lines[0].String())
}
func TestCarriageReturnOnFullLine(t *testing.T) {
b := makeBufferForTesting(20, 20)
writeRaw(b, []rune("abcdeabcdeabcdeabcde")...)
b.carriageReturn()
writeRaw(b, []rune("xxxxxxxxxxxxxxxxxxxx")...)
lines := b.GetVisibleLines()
assert.Equal(t, "xxxxxxxxxxxxxxxxxxxx", lines[0].String())
}
func TestCarriageReturnOnFullLastLine(t *testing.T) {
b := makeBufferForTesting(20, 2)
b.newLine()
writeRaw(b, []rune("abcdeabcdeabcdeabcde")...)
b.carriageReturn()
writeRaw(b, []rune("xxxxxxxxxxxxxxxxxxxx")...)
lines := b.GetVisibleLines()
assert.Equal(t, "", lines[0].String())
assert.Equal(t, "xxxxxxxxxxxxxxxxxxxx", lines[1].String())
}
func TestCarriageReturnOnWrappedLine(t *testing.T) {
b := makeBufferForTesting(80, 6)
writeRaw(b, []rune("hello!")...)
b.carriageReturn()
writeRaw(b, []rune("secret")...)
lines := b.GetVisibleLines()
assert.Equal(t, "secret", lines[0].String())
}
func TestCarriageReturnOnLineThatDoesntExist(t *testing.T) {
b := makeBufferForTesting(6, 10)
b.cursorPosition.Line = 3
b.carriageReturn()
assert.Equal(t, uint16(0), b.cursorPosition.Col)
assert.Equal(t, uint64(3), b.cursorPosition.Line)
}
func TestGetCell(t *testing.T) {
b := makeBufferForTesting(80, 20)
writeRaw(b, []rune("Hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("there")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("something...")...)
cell := b.GetCell(8, 2)
require.NotNil(t, cell)
assert.Equal(t, 'g', cell.Rune().Rune)
}
func TestGetCellWithHistory(t *testing.T) {
b := makeBufferForTesting(80, 2)
writeRaw(b, []rune("Hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("there")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("something...")...)
cell := b.GetCell(8, 1)
require.NotNil(t, cell)
assert.Equal(t, 'g', cell.Rune().Rune)
}
func TestGetCellWithBadCursor(t *testing.T) {
b := makeBufferForTesting(80, 2)
writeRaw(b, []rune("Hello\r\nthere\r\nsomething...")...)
require.Nil(t, b.GetCell(8, 3))
require.Nil(t, b.GetCell(90, 0))
}
func TestCursorPositionQuerying(t *testing.T) {
b := makeBufferForTesting(80, 20)
b.cursorPosition.Col = 17
b.cursorPosition.Line = 9
assert.Equal(t, b.cursorPosition.Col, b.CursorColumn())
assert.Equal(t, b.convertRawLineToViewLine(b.cursorPosition.Line), b.CursorLine())
}
// CSI 2 K
func TestEraseLine(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello, this is a test")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []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())
}
// CSI 1 K
func TestEraseLineToCursor(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello, this is a test")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("deleted")...)
b.movePosition(-3, 0)
b.eraseLineToCursor()
assert.Equal(t, "hello, this is a test", b.lines[0].String())
assert.Equal(t, "\x00\x00\x00\x00\x00ed", b.lines[1].String())
}
// CSI 0 K
func TestEraseLineAfterCursor(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello, this is a test")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("deleted")...)
b.movePosition(-3, 0)
b.eraseLineFromCursor()
assert.Equal(t, "hello, this is a test", b.lines[0].String())
assert.Equal(t, "dele", b.lines[1].String())
}
func TestEraseDisplay(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("asdasd")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("thing")...)
b.movePosition(2, 1)
b.eraseDisplay()
lines := b.GetVisibleLines()
for _, line := range lines {
assert.Equal(t, "", line.String())
}
}
func TestEraseDisplayToCursor(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("asdasd")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("thing")...)
b.movePosition(-2, 0)
b.eraseDisplayToCursor()
lines := b.GetVisibleLines()
assert.Equal(t, "", lines[0].String())
assert.Equal(t, "", lines[1].String())
assert.Equal(t, "\x00\x00\x00\x00g", lines[2].String())
}
func TestEraseDisplayFromCursor(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("asdasd")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("things")...)
b.movePosition(-3, -1)
b.eraseDisplayFromCursor()
lines := b.GetVisibleLines()
assert.Equal(t, "hello", lines[0].String())
assert.Equal(t, "asd", lines[1].String())
assert.Equal(t, "", lines[2].String())
}
func TestBackspace(t *testing.T) {
b := makeBufferForTesting(80, 5)
writeRaw(b, []rune("hello")...)
b.backspace()
b.backspace()
writeRaw(b, []rune("p")...)
lines := b.GetVisibleLines()
assert.Equal(t, "helpo", lines[0].String())
}
func TestHorizontalResizeView(t *testing.T) {
b := makeBufferForTesting(80, 10)
// 60 characters
writeRaw(b, []rune(`hellohellohellohellohellohellohellohellohellohellohellohello`)...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune(`goodbyegoodbye`)...)
require.Equal(t, uint16(14), b.cursorPosition.Col)
require.Equal(t, uint64(1), b.cursorPosition.Line)
b.resizeView(40, 10)
expected := `hellohellohellohellohellohellohellohello
hellohellohellohello
goodbyegoodbye`
require.Equal(t, uint16(14), b.cursorPosition.Col)
require.Equal(t, uint64(2), b.cursorPosition.Line)
lines := b.GetVisibleLines()
strs := []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
b.resizeView(20, 10)
expected = `hellohellohellohello
hellohellohellohello
hellohellohellohello
goodbyegoodbye`
lines = b.GetVisibleLines()
strs = []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
b.resizeView(10, 10)
expected = `hellohello
hellohello
hellohello
hellohello
hellohello
hellohello
goodbyegoo
dbye`
lines = b.GetVisibleLines()
strs = []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
b.resizeView(80, 20)
expected = `hellohellohellohellohellohellohellohellohellohellohellohello
goodbyegoodbye`
lines = b.GetVisibleLines()
strs = []string{}
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
require.Equal(t, uint16(4), b.cursorPosition.Col)
require.Equal(t, uint64(1), b.cursorPosition.Line)
}
func TestBufferMaxLines(t *testing.T) {
b := NewBuffer(80, 2, 2, color.White, color.Black)
b.modes.LineFeedMode = false
writeRaw(b, []rune("hello")...)
b.newLine()
writeRaw(b, []rune("funny")...)
b.newLine()
writeRaw(b, []rune("world")...)
assert.Equal(t, 2, len(b.lines))
assert.Equal(t, "funny", b.lines[0].String())
assert.Equal(t, "world", b.lines[1].String())
}
func TestShrinkingThenGrowing(t *testing.T) {
b := makeBufferForTesting(30, 100)
writeRaw(b, []rune("hellohellohellohellohello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("01234567890123456789")...)
b.carriageReturn()
b.newLine()
b.resizeView(25, 100)
b.resizeView(24, 100)
b.resizeView(30, 100)
expected := `hellohellohellohellohello
01234567890123456789
`
lines := b.GetVisibleLines()
var strs []string
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
}
func TestShrinkingThenRestoring(t *testing.T) {
b := makeBufferForTesting(30, 100)
writeRaw(b, []rune("hellohellohellohellohello")...)
b.carriageReturn()
b.newLine()
writeRaw(b, []rune("01234567890123456789")...)
b.carriageReturn()
b.newLine()
b.cursorPosition.Line = 2
for i := uint16(29); i > 5; i-- {
b.resizeView(i, 100)
}
for i := uint16(15); i < 30; i++ {
b.resizeView(i, 100)
}
expected := `hellohellohellohellohello
01234567890123456789
`
lines := b.GetVisibleLines()
var strs []string
for _, l := range lines {
strs = append(strs, l.String())
}
require.Equal(t, expected, strings.Join(strs, "\n"))
}
func makeBufferForTesting(cols, rows uint16) *Buffer {
return NewBuffer(cols, rows, 100, color.White, color.Black)
}

View File

@ -0,0 +1,59 @@
package termutil
import "image/color"
type Cell struct {
r MeasuredRune
attr CellAttributes
}
func (cell *Cell) Attr() CellAttributes {
return cell.attr
}
func (cell *Cell) Rune() MeasuredRune {
return cell.r
}
func (cell *Cell) Fg() color.Color {
if cell.Attr().inverse {
return cell.attr.bgColour
}
return cell.attr.fgColour
}
func (cell *Cell) Bold() bool {
return cell.attr.bold
}
func (cell *Cell) Dim() bool {
return cell.attr.dim
}
func (cell *Cell) Italic() bool {
return cell.attr.italic
}
func (cell *Cell) Underline() bool {
return cell.attr.underline
}
func (cell *Cell) Strikethrough() bool {
return cell.attr.strikethrough
}
func (cell *Cell) Bg() color.Color {
if cell.Attr().inverse {
return cell.attr.fgColour
}
return cell.attr.bgColour
}
func (cell *Cell) erase(bgColour color.Color) {
cell.setRune(MeasuredRune{Rune: 0})
cell.attr.bgColour = bgColour
}
func (cell *Cell) setRune(r MeasuredRune) {
cell.r = r
}

View File

@ -0,0 +1,18 @@
package termutil
import (
"image/color"
)
type CellAttributes struct {
fgColour color.Color
bgColour color.Color
bold bool
italic bool
dim bool
underline bool
strikethrough bool
blink bool
inverse bool
hidden bool
}

View File

@ -0,0 +1,64 @@
package termutil
var charSets = map[rune]*map[rune]rune{
'0': &decSpecGraphics,
'B': nil, // ASCII
// @todo 1,2,A
}
var decSpecGraphics = map[rune]rune{
0x5f: 0x00A0, // NO-BREAK SPACE
0x60: 0x25C6, // BLACK DIAMOND
0x61: 0x2592, // MEDIUM SHADE
0x62: 0x2409, // SYMBOL FOR HORIZONTAL TABULATION
0x63: 0x240C, // SYMBOL FOR FORM FEED
0x64: 0x240D, // SYMBOL FOR CARRIAGE RETURN
0x65: 0x240A, // SYMBOL FOR LINE FEED
0x66: 0x00B0, // DEGREE SIGN
0x67: 0x00B1, // PLUS-MINUS SIGN
0x68: 0x2424, // SYMBOL FOR NEWLINE
0x69: 0x240B, // SYMBOL FOR VERTICAL TABULATION
0x6a: 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT
0x6b: 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT
0x6c: 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT
0x6d: 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT
0x6e: 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
0x6f: 0x23BA, // HORIZONTAL SCAN LINE-1
0x70: 0x23BB, // HORIZONTAL SCAN LINE-3
0x71: 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL
0x72: 0x23BC, // HORIZONTAL SCAN LINE-7
0x73: 0x23BD, // HORIZONTAL SCAN LINE-9
0x74: 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT
0x75: 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT
0x76: 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL
0x77: 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
0x78: 0x2502, // BOX DRAWINGS LIGHT VERTICAL
0x79: 0x2264, // LESS-THAN OR EQUAL TO
0x7a: 0x2265, // GREATER-THAN OR EQUAL TO
0x7b: 0x03C0, // GREEK SMALL LETTER PI
0x7c: 0x2260, // NOT EQUAL TO
0x7d: 0x00A3, // POUND SIGN
0x7e: 0x00B7, // MIDDLE DOT
}
func (t *Terminal) handleSCS0(pty chan MeasuredRune) bool {
return t.scsHandler(pty, 0)
}
func (t *Terminal) handleSCS1(pty chan MeasuredRune) bool {
return t.scsHandler(pty, 1)
}
func (t *Terminal) scsHandler(pty chan MeasuredRune, which int) bool {
b := <-pty
cs, ok := charSets[b.Rune]
if ok {
//terminal.logger.Debugf("Selected charset %v into G%v", string(b), which)
t.activeBuffer.charsets[which] = cs
return false
}
t.activeBuffer.charsets[which] = nil
return false
}

View File

@ -0,0 +1,5 @@
//+build !windows
package termutil
var oscTerminators = []rune{0x07, 0x5c}

View File

@ -0,0 +1,5 @@
//+build windows
package termutil
var oscTerminators = []rune{0x07, 0x00}

View File

@ -0,0 +1,980 @@
package termutil
import (
"fmt"
"strconv"
"strings"
)
func parseCSI(readChan chan MeasuredRune) (final rune, params []string, intermediate []rune, raw []rune) {
var b MeasuredRune
param := ""
intermediate = []rune{}
CSI:
for {
b = <-readChan
raw = append(raw, b.Rune)
switch true {
case b.Rune >= 0x30 && b.Rune <= 0x3F:
param = param + string(b.Rune)
case b.Rune > 0 && b.Rune <= 0x2F:
intermediate = append(intermediate, b.Rune)
case b.Rune >= 0x40 && b.Rune <= 0x7e:
final = b.Rune
break CSI
}
}
unprocessed := strings.Split(param, ";")
for _, par := range unprocessed {
if par != "" {
par = strings.TrimLeft(par, "0")
if par == "" {
par = "0"
}
params = append(params, par)
}
}
return final, params, intermediate, raw
}
func (t *Terminal) handleCSI(readChan chan MeasuredRune) (renderRequired bool) {
final, params, intermediate, raw := parseCSI(readChan)
t.log("CSI P(%q) I(%q) %c", strings.Join(params, ";"), string(intermediate), final)
for _, b := range intermediate {
t.processRunes(MeasuredRune{
Rune: b,
Width: 1,
})
}
switch final {
case 'c':
return t.csiSendDeviceAttributesHandler(params)
case 'd':
return t.csiLinePositionAbsoluteHandler(params)
case 'f':
return t.csiCursorPositionHandler(params)
case 'g':
return t.csiTabClearHandler(params)
case 'h':
return t.csiSetModeHandler(params)
case 'l':
return t.csiResetModeHandler(params)
case 'm':
return t.sgrSequenceHandler(params)
case 'n':
return t.csiDeviceStatusReportHandler(params)
case 'r':
return t.csiSetMarginsHandler(params)
case 't':
return t.csiWindowManipulation(params)
case 'A':
return t.csiCursorUpHandler(params)
case 'B':
return t.csiCursorDownHandler(params)
case 'C':
return t.csiCursorForwardHandler(params)
case 'D':
return t.csiCursorBackwardHandler(params)
case 'E':
return t.csiCursorNextLineHandler(params)
case 'F':
return t.csiCursorPrecedingLineHandler(params)
case 'G':
return t.csiCursorCharacterAbsoluteHandler(params)
case 'H':
return t.csiCursorPositionHandler(params)
case 'J':
return t.csiEraseInDisplayHandler(params)
case 'K':
return t.csiEraseInLineHandler(params)
case 'L':
return t.csiInsertLinesHandler(params)
case 'M':
return t.csiDeleteLinesHandler(params)
case 'P':
return t.csiDeleteHandler(params)
case 'S':
return t.csiScrollUpHandler(params)
case 'T':
return t.csiScrollDownHandler(params)
case 'X':
return t.csiEraseCharactersHandler(params)
case '@':
return t.csiInsertBlankCharactersHandler(params)
case 'p': // reset handler
if string(intermediate) == "!" {
return t.csiSoftResetHandler(params)
}
return false
default:
// TODO review this:
// if this is an unknown CSI sequence, write it to stdout as we can't handle it?
//_ = t.writeToRealStdOut(append([]rune{0x1b, '['}, raw...)...)
_ = raw
t.log("UNKNOWN CSI P(%s) I(%s) %c", strings.Join(params, ";"), string(intermediate), final)
return false
}
}
type WindowState uint8
const (
StateUnknown WindowState = iota
StateMinimised
StateNormal
StateMaximised
)
type WindowManipulator interface {
State() WindowState
Minimise()
Maximise()
Restore()
SetTitle(title string)
Position() (int, int)
SizeInPixels() (int, int)
CellSizeInPixels() (int, int)
SizeInChars() (int, int)
ResizeInPixels(int, int)
ResizeInChars(int, int)
ScreenSizeInPixels() (int, int)
ScreenSizeInChars() (int, int)
Move(x, y int)
IsFullscreen() bool
SetFullscreen(enabled bool)
GetTitle() string
SaveTitleToStack()
RestoreTitleFromStack()
ReportError(err error)
}
func (t *Terminal) csiWindowManipulation(params []string) (renderRequired bool) {
if t.windowManipulator == nil {
return false
}
for i := 0; i < len(params); i++ {
switch params[i] {
case "1":
t.windowManipulator.Restore()
case "2":
t.windowManipulator.Minimise()
case "3": //move window
if i+2 >= len(params) {
return false
}
x, _ := strconv.Atoi(params[i+1])
y, _ := strconv.Atoi(params[i+2])
i += 2
t.windowManipulator.Move(x, y)
case "4": //resize h,w
w, h := t.windowManipulator.SizeInPixels()
if i+1 < len(params) {
h, _ = strconv.Atoi(params[i+1])
i++
}
if i+2 < len(params) {
w, _ = strconv.Atoi(params[i+2])
i++
}
sw, sh := t.windowManipulator.ScreenSizeInPixels()
if w == 0 {
w = sw
}
if h == 0 {
h = sh
}
t.windowManipulator.ResizeInPixels(w, h)
case "8":
// resize in rows, cols
w, h := t.windowManipulator.SizeInChars()
if i+1 < len(params) {
h, _ = strconv.Atoi(params[i+1])
i++
}
if i+2 < len(params) {
w, _ = strconv.Atoi(params[i+2])
i++
}
sw, sh := t.windowManipulator.ScreenSizeInChars()
if w == 0 {
w = sw
}
if h == 0 {
h = sh
}
t.windowManipulator.ResizeInChars(w, h)
case "9":
if i+1 >= len(params) {
return false
}
switch params[i+1] {
case "0":
t.windowManipulator.Restore()
case "1":
t.windowManipulator.Maximise()
case "2":
w, _ := t.windowManipulator.SizeInPixels()
_, sh := t.windowManipulator.ScreenSizeInPixels()
t.windowManipulator.ResizeInPixels(w, sh)
case "3":
_, h := t.windowManipulator.SizeInPixels()
sw, _ := t.windowManipulator.ScreenSizeInPixels()
t.windowManipulator.ResizeInPixels(sw, h)
}
i++
case "10":
if i+1 >= len(params) {
return false
}
switch params[i+1] {
case "0":
t.windowManipulator.SetFullscreen(false)
case "1":
t.windowManipulator.SetFullscreen(true)
case "2":
// toggle
t.windowManipulator.SetFullscreen(!t.windowManipulator.IsFullscreen())
}
i++
case "11":
if t.windowManipulator.State() != StateMinimised {
t.WriteToPty([]byte("\x1b[1t"))
} else {
t.WriteToPty([]byte("\x1b[2t"))
}
case "13":
if i < len(params)-1 {
i++
}
x, y := t.windowManipulator.Position()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[3;%d;%dt", x, y)))
case "14":
if i < len(params)-1 {
i++
}
w, h := t.windowManipulator.SizeInPixels()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[4;%d;%dt", h, w)))
case "15":
w, h := t.windowManipulator.ScreenSizeInPixels()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[5;%d;%dt", h, w)))
case "16":
w, h := t.windowManipulator.CellSizeInPixels()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[6;%d;%dt", h, w)))
case "18":
w, h := t.windowManipulator.SizeInChars()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[8;%d;%dt", h, w)))
case "19":
w, h := t.windowManipulator.ScreenSizeInChars()
t.WriteToPty([]byte(fmt.Sprintf("\x1b[9;%d;%dt", h, w)))
case "20":
t.WriteToPty([]byte(fmt.Sprintf("\x1b]L%s\x1b\\", t.windowManipulator.GetTitle())))
case "21":
t.WriteToPty([]byte(fmt.Sprintf("\x1b]l%s\x1b\\", t.windowManipulator.GetTitle())))
case "22":
if i < len(params)-1 {
i++
}
t.windowManipulator.SaveTitleToStack()
case "23":
if i < len(params)-1 {
i++
}
t.windowManipulator.RestoreTitleFromStack()
}
}
return true
}
// CSI c
// Send Device Attributes (Primary/Secondary/Tertiary DA)
func (t *Terminal) csiSendDeviceAttributesHandler(params []string) (renderRequired bool) {
// we are VT100
// for DA1 we'll respond ?1;2
// for DA2 we'll respond >0;0;0
response := "?1;2"
if len(params) > 0 && len(params[0]) > 0 && params[0][0] == '>' {
response = ">0;0;0"
}
// write response to source pty
t.WriteToPty([]byte("\x1b[" + response + "c"))
return false
}
// CSI n
// Device Status Report (DSR)
func (t *Terminal) csiDeviceStatusReportHandler(params []string) (renderRequired bool) {
if len(params) == 0 {
return false
}
switch params[0] {
case "5":
t.WriteToPty([]byte("\x1b[0n")) // everything is cool
case "6": // report cursor position
t.WriteToPty([]byte(fmt.Sprintf(
"\x1b[%d;%dR",
t.GetActiveBuffer().CursorLine()+1,
t.GetActiveBuffer().CursorColumn()+1,
)))
}
return false
}
// CSI A
// Cursor Up Ps Times (default = 1) (CUU)
func (t *Terminal) csiCursorUpHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(0, -int16(distance))
return true
}
// CSI B
// Cursor Down Ps Times (default = 1) (CUD)
func (t *Terminal) csiCursorDownHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(0, int16(distance))
return true
}
// CSI C
// Cursor Forward Ps Times (default = 1) (CUF)
func (t *Terminal) csiCursorForwardHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(int16(distance), 0)
return true
}
// CSI D
// Cursor Backward Ps Times (default = 1) (CUB)
func (t *Terminal) csiCursorBackwardHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(-int16(distance), 0)
return true
}
// CSI E
// Cursor Next Line Ps Times (default = 1) (CNL)
func (t *Terminal) csiCursorNextLineHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(0, int16(distance))
t.GetActiveBuffer().setPosition(0, t.GetActiveBuffer().CursorLine())
return true
}
// CSI F
// Cursor Preceding Line Ps Times (default = 1) (CPL)
func (t *Terminal) csiCursorPrecedingLineHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().movePosition(0, -int16(distance))
t.GetActiveBuffer().setPosition(0, t.GetActiveBuffer().CursorLine())
return true
}
// CSI G
// Cursor Horizontal Absolute [column] (default = [row,1]) (CHA)
func (t *Terminal) csiCursorCharacterAbsoluteHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 0 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || params[0] == "" {
distance = 1
}
}
t.GetActiveBuffer().setPosition(uint16(distance-1), t.GetActiveBuffer().CursorLine())
return true
}
func parseCursorPosition(params []string) (x, y int) {
x, y = 1, 1
if len(params) >= 1 {
var err error
if params[0] != "" {
y, err = strconv.Atoi(string(params[0]))
if err != nil || y < 1 {
y = 1
}
}
}
if len(params) >= 2 {
if params[1] != "" {
var err error
x, err = strconv.Atoi(string(params[1]))
if err != nil || x < 1 {
x = 1
}
}
}
return x, y
}
// CSI f
// Horizontal and Vertical Position [row;column] (default = [1,1]) (HVP)
// AND
// CSI H
// Cursor Position [row;column] (default = [1,1]) (CUP)
func (t *Terminal) csiCursorPositionHandler(params []string) (renderRequired bool) {
x, y := parseCursorPosition(params)
t.GetActiveBuffer().setPosition(uint16(x-1), uint16(y-1))
return true
}
// CSI S
// Scroll up Ps lines (default = 1) (SU), VT420, ECMA-48
func (t *Terminal) csiScrollUpHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 1 {
return false
}
if len(params) == 1 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().areaScrollUp(uint16(distance))
return true
}
// CSI @
// Insert Ps (Blank) Character(s) (default = 1) (ICH)
func (t *Terminal) csiInsertBlankCharactersHandler(params []string) (renderRequired bool) {
count := 1
if len(params) > 1 {
return false
}
if len(params) == 1 {
var err error
count, err = strconv.Atoi(params[0])
if err != nil || count < 1 {
count = 1
}
}
t.GetActiveBuffer().insertBlankCharacters(count)
return true
}
// CSI L
// Insert Ps Line(s) (default = 1) (IL)
func (t *Terminal) csiInsertLinesHandler(params []string) (renderRequired bool) {
count := 1
if len(params) > 1 {
return false
}
if len(params) == 1 {
var err error
count, err = strconv.Atoi(params[0])
if err != nil || count < 1 {
count = 1
}
}
t.GetActiveBuffer().insertLines(count)
return true
}
// CSI M
// Delete Ps Line(s) (default = 1) (DL)
func (t *Terminal) csiDeleteLinesHandler(params []string) (renderRequired bool) {
count := 1
if len(params) > 1 {
return false
}
if len(params) == 1 {
var err error
count, err = strconv.Atoi(params[0])
if err != nil || count < 1 {
count = 1
}
}
t.GetActiveBuffer().deleteLines(count)
return true
}
// CSI T
// Scroll down Ps lines (default = 1) (SD), VT420
func (t *Terminal) csiScrollDownHandler(params []string) (renderRequired bool) {
distance := 1
if len(params) > 1 {
return false
}
if len(params) == 1 {
var err error
distance, err = strconv.Atoi(params[0])
if err != nil || distance < 1 {
distance = 1
}
}
t.GetActiveBuffer().areaScrollDown(uint16(distance))
return true
}
// CSI r
// Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100
func (t *Terminal) csiSetMarginsHandler(params []string) (renderRequired bool) {
top := 1
bottom := int(t.GetActiveBuffer().ViewHeight())
if len(params) > 2 {
return false
}
if len(params) > 0 {
var err error
top, err = strconv.Atoi(params[0])
if err != nil || top < 1 {
top = 1
}
if len(params) > 1 {
var err error
bottom, err = strconv.Atoi(params[1])
if err != nil || bottom > int(t.GetActiveBuffer().ViewHeight()) || bottom < 1 {
bottom = int(t.GetActiveBuffer().ViewHeight())
}
}
}
top--
bottom--
t.activeBuffer.setVerticalMargins(uint(top), uint(bottom))
t.GetActiveBuffer().setPosition(0, 0)
return true
}
// CSI X
// Erase Ps Character(s) (default = 1) (ECH)
func (t *Terminal) csiEraseCharactersHandler(params []string) (renderRequired bool) {
count := 1
if len(params) > 0 {
var err error
count, err = strconv.Atoi(params[0])
if err != nil || count < 1 {
count = 1
}
}
t.GetActiveBuffer().eraseCharacters(count)
return true
}
// CSI l
// Reset Mode (RM)
func (t *Terminal) csiResetModeHandler(params []string) (renderRequired bool) {
return t.csiSetModes(params, false)
}
// CSI h
// Set Mode (SM)
func (t *Terminal) csiSetModeHandler(params []string) (renderRequired bool) {
return t.csiSetModes(params, true)
}
func (t *Terminal) csiSetModes(modes []string, enabled bool) bool {
if len(modes) == 0 {
return false
}
if len(modes) == 1 {
return t.csiSetMode(modes[0], enabled)
}
// should we propagate DEC prefix?
const decPrefix = '?'
isDec := len(modes[0]) > 0 && modes[0][0] == decPrefix
var render bool
// iterate through params, propagating DEC prefix to subsequent elements
for i, v := range modes {
updatedMode := v
if i > 0 && isDec {
updatedMode = string(decPrefix) + v
}
render = t.csiSetMode(updatedMode, enabled) || render
}
return render
}
func parseModes(mode string) []string {
var output []string
if mode == "" {
return nil
}
var prefix string
if mode[0] == '?' {
prefix = "?"
mode = mode[1:]
}
for len(mode) > 4 {
output = append(output, prefix+mode[:4])
mode = mode[4:]
}
output = append(output, prefix+mode)
return output
}
func (t *Terminal) csiSetMode(modes string, enabled bool) bool {
for _, modeStr := range parseModes(modes) {
switch modeStr {
case "4":
t.activeBuffer.modes.ReplaceMode = !enabled
case "20":
t.activeBuffer.modes.LineFeedMode = false
case "?1":
t.activeBuffer.modes.ApplicationCursorKeys = enabled
case "?3":
if t.windowManipulator != nil {
if enabled {
// DECCOLM - COLumn mode, 132 characters per line
t.windowManipulator.ResizeInChars(132, int(t.activeBuffer.viewHeight))
} else {
// DECCOLM - 80 characters per line (erases screen)
t.windowManipulator.ResizeInChars(80, int(t.activeBuffer.viewHeight))
}
t.activeBuffer.clear()
}
case "?5": // DECSCNM
t.activeBuffer.modes.ScreenMode = enabled
case "?6":
// DECOM
t.activeBuffer.modes.OriginMode = enabled
case "?7":
// auto-wrap mode
//DECAWM
t.activeBuffer.modes.AutoWrap = enabled
case "?9":
if enabled {
//terminal.logger.Infof("Turning on X10 mouse mode")
t.activeBuffer.mouseMode = (MouseModeX10)
} else {
//terminal.logger.Infof("Turning off X10 mouse mode")
t.activeBuffer.mouseMode = (MouseModeNone)
}
case "?12", "?13":
t.activeBuffer.modes.BlinkingCursor = enabled
case "?25":
t.activeBuffer.modes.ShowCursor = enabled
case "?47", "?1047":
if enabled {
t.useAltBuffer()
} else {
t.useMainBuffer()
}
case "?1000": // ?10061000 seen from htop
// enable mouse tracking
// 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
if enabled {
t.activeBuffer.mouseMode = (MouseModeVT200)
} else {
t.activeBuffer.mouseMode = (MouseModeNone)
}
case "?1002":
if enabled {
//terminal.logger.Infof("Turning on Button Event mouse mode")
t.activeBuffer.mouseMode = (MouseModeButtonEvent)
} else {
//terminal.logger.Infof("Turning off Button Event mouse mode")
t.activeBuffer.mouseMode = (MouseModeNone)
}
case "?1003":
if enabled {
t.activeBuffer.mouseMode = MouseModeAnyEvent
} else {
t.activeBuffer.mouseMode = MouseModeNone
}
case "?1005":
if enabled {
t.activeBuffer.mouseExtMode = MouseExtUTF
} else {
t.activeBuffer.mouseExtMode = MouseExtNone
}
case "?1006":
if enabled {
//.logger.Infof("Turning on SGR ext mouse mode")
t.activeBuffer.mouseExtMode = MouseExtSGR
} else {
//terminal.logger.Infof("Turning off SGR ext mouse mode")
t.activeBuffer.mouseExtMode = (MouseExtNone)
}
case "?1015":
if enabled {
//terminal.logger.Infof("Turning on URXVT ext mouse mode")
t.activeBuffer.mouseExtMode = (MouseExtURXVT)
} else {
//terminal.logger.Infof("Turning off URXVT ext mouse mode")
t.activeBuffer.mouseExtMode = (MouseExtNone)
}
case "?1048":
if enabled {
t.GetActiveBuffer().saveCursor()
} else {
t.GetActiveBuffer().restoreCursor()
}
case "?1049":
if enabled {
t.useAltBuffer()
} else {
t.useMainBuffer()
}
case "?2004":
t.activeBuffer.modes.BracketedPasteMode = enabled
case "?80":
t.activeBuffer.modes.SixelScrolling = enabled
default:
t.log("Unsupported CSI mode %s = %t", modeStr, enabled)
}
}
return false
}
// CSI d
// Line Position Absolute [row] (default = [1,column]) (VPA)
func (t *Terminal) csiLinePositionAbsoluteHandler(params []string) (renderRequired bool) {
row := 1
if len(params) > 0 {
var err error
row, err = strconv.Atoi(params[0])
if err != nil || row < 1 {
row = 1
}
}
t.GetActiveBuffer().setPosition(t.GetActiveBuffer().CursorColumn(), uint16(row-1))
return true
}
// CSI P
// Delete Ps Character(s) (default = 1) (DCH)
func (t *Terminal) csiDeleteHandler(params []string) (renderRequired bool) {
n := 1
if len(params) >= 1 {
var err error
n, err = strconv.Atoi(params[0])
if err != nil || n < 1 {
n = 1
}
}
t.GetActiveBuffer().deleteChars(n)
return true
}
// CSI g
// tab clear (TBC)
func (t *Terminal) csiTabClearHandler(params []string) (renderRequired bool) {
n := "0"
if len(params) > 0 {
n = params[0]
}
switch n {
case "0", "":
t.activeBuffer.tabClearAtCursor()
case "3":
t.activeBuffer.tabReset()
default:
return false
}
return true
}
// CSI J
// Erase in Display (ED), VT100
func (t *Terminal) csiEraseInDisplayHandler(params []string) (renderRequired bool) {
n := "0"
if len(params) > 0 {
n = params[0]
}
switch n {
case "0", "":
t.GetActiveBuffer().eraseDisplayFromCursor()
case "1":
t.GetActiveBuffer().eraseDisplayToCursor()
case "2", "3":
t.GetActiveBuffer().eraseDisplay()
default:
return false
}
return true
}
// CSI K
// Erase in Line (EL), VT100
func (t *Terminal) csiEraseInLineHandler(params []string) (renderRequired bool) {
n := "0"
if len(params) > 0 {
n = params[0]
}
switch n {
case "0", "": //erase adter cursor
t.GetActiveBuffer().eraseLineFromCursor()
case "1": // erase to cursor inclusive
t.GetActiveBuffer().eraseLineToCursor()
case "2": // erase entire
t.GetActiveBuffer().eraseLine()
default:
return false
}
return true
}
// CSI m
// Character Attributes (SGR)
func (t *Terminal) sgrSequenceHandler(params []string) bool {
if len(params) == 0 {
params = []string{"0"}
}
for i := range params {
p := strings.Replace(strings.Replace(params[i], "[", "", -1), "]", "", -1)
switch p {
case "00", "0", "":
attr := t.GetActiveBuffer().getCursorAttr()
*attr = CellAttributes{}
case "1", "01":
t.GetActiveBuffer().getCursorAttr().bold = true
case "2", "02":
t.GetActiveBuffer().getCursorAttr().dim = true
case "3", "03":
t.GetActiveBuffer().getCursorAttr().italic = true
case "4", "04":
t.GetActiveBuffer().getCursorAttr().underline = true
case "5", "05":
t.GetActiveBuffer().getCursorAttr().blink = true
case "7", "07":
t.GetActiveBuffer().getCursorAttr().inverse = true
case "8", "08":
t.GetActiveBuffer().getCursorAttr().hidden = true
case "9", "09":
t.GetActiveBuffer().getCursorAttr().strikethrough = true
case "21":
t.GetActiveBuffer().getCursorAttr().bold = false
case "22":
t.GetActiveBuffer().getCursorAttr().dim = false
case "23":
t.GetActiveBuffer().getCursorAttr().italic = false
case "24":
t.GetActiveBuffer().getCursorAttr().underline = false
case "25":
t.GetActiveBuffer().getCursorAttr().blink = false
case "27":
t.GetActiveBuffer().getCursorAttr().inverse = false
case "28":
t.GetActiveBuffer().getCursorAttr().hidden = false
case "29":
t.GetActiveBuffer().getCursorAttr().strikethrough = false
case "38": // set foreground
t.GetActiveBuffer().getCursorAttr().fgColour, _ = t.theme.ColourFromAnsi(params[i+1:], false)
return false
case "48": // set background
t.GetActiveBuffer().getCursorAttr().bgColour, _ = t.theme.ColourFromAnsi(params[i+1:], true)
return false
case "39":
t.GetActiveBuffer().getCursorAttr().fgColour = t.theme.DefaultForeground()
case "49":
t.GetActiveBuffer().getCursorAttr().bgColour = t.theme.DefaultBackground()
default:
bi, err := strconv.Atoi(p)
if err != nil {
return false
}
i := byte(bi)
switch true {
case i >= 30 && i <= 37, i >= 90 && i <= 97:
t.GetActiveBuffer().getCursorAttr().fgColour = t.theme.ColourFrom4Bit(i)
case i >= 40 && i <= 47, i >= 100 && i <= 107:
t.GetActiveBuffer().getCursorAttr().bgColour = t.theme.ColourFrom4Bit(i)
}
}
}
return false
}
func (t *Terminal) csiSoftResetHandler(params []string) bool {
t.reset()
return true
}

View File

@ -0,0 +1,66 @@
package termutil
import "strings"
type Line struct {
wrapped bool // whether line was wrapped onto from the previous one
cells []Cell
}
func newLine() Line {
return Line{
wrapped: false,
cells: []Cell{},
}
}
func (line *Line) Len() uint16 {
return uint16(len(line.cells))
}
func (line *Line) String() string {
runes := []rune{}
for _, cell := range line.cells {
runes = append(runes, cell.r.Rune)
}
return strings.TrimRight(string(runes), "\x00")
}
func (line *Line) append(cells ...Cell) {
line.cells = append(line.cells, cells...)
}
func (line *Line) shrink(width uint16) {
if line.Len() <= width {
return
}
remove := line.Len() - width
var cells []Cell
for _, cell := range line.cells {
if cell.r.Rune == 0 && remove > 0 {
remove--
} else {
cells = append(cells, cell)
}
}
line.cells = cells
}
func (line *Line) wrap(width uint16) []Line {
var output []Line
var current Line
current.wrapped = line.wrapped
for _, cell := range line.cells {
if len(current.cells) == int(width) {
output = append(output, current)
current = newLine()
current.wrapped = true
}
current.cells = append(current.cells, cell)
}
return append(output, current)
}

View File

@ -0,0 +1,6 @@
package termutil
type MeasuredRune struct {
Rune rune
Width int
}

View File

@ -0,0 +1,30 @@
package termutil
type Modes struct {
ShowCursor bool
ApplicationCursorKeys bool
BlinkingCursor bool
ReplaceMode bool // overwrite character at cursor or insert new
OriginMode bool // see DECOM docs - whether cursor is positioned within the margins or not
LineFeedMode bool
ScreenMode bool // DECSCNM (black on white background)
AutoWrap bool
SixelScrolling bool // DECSDM
BracketedPasteMode bool
}
type MouseMode uint
type MouseExtMode uint
const (
MouseModeNone MouseMode = iota
MouseModeX10
MouseModeVT200
MouseModeVT200Highlight
MouseModeButtonEvent
MouseModeAnyEvent
MouseExtNone MouseExtMode = iota
MouseExtUTF
MouseExtSGR
MouseExtURXVT
)

View File

@ -0,0 +1,37 @@
package termutil
import (
"os"
)
type Option func(t *Terminal)
func WithLogFile(path string) Option {
return func(t *Terminal) {
t.logFile, _ = os.Create(path)
}
}
func WithTheme(theme *Theme) Option {
return func(t *Terminal) {
t.theme = theme
}
}
func WithShell(shell string) Option {
return func(t *Terminal) {
t.shell = shell
}
}
func WithInitialCommand(cmd string) Option {
return func(t *Terminal) {
t.initialCommand = cmd + "\n"
}
}
func WithWindowManipulator(m WindowManipulator) Option {
return func(t *Terminal) {
t.windowManipulator = m
}
}

View File

@ -0,0 +1,69 @@
package termutil
import (
"fmt"
)
func (t *Terminal) handleOSC(readChan chan MeasuredRune) (renderRequired bool) {
params := []string{}
param := ""
READ:
for {
select {
case b := <-readChan:
if t.isOSCTerminator(b.Rune) {
params = append(params, param)
break READ
}
if b.Rune == ';' {
params = append(params, param)
param = ""
continue
}
param = fmt.Sprintf("%s%c", param, b.Rune)
default:
return false
}
}
if len(params) == 0 {
return false
}
pT := params[len(params)-1]
pS := params[:len(params)-1]
if len(pS) == 0 {
pS = []string{pT}
pT = ""
}
switch pS[0] {
case "0", "2", "l":
t.setTitle(pT)
case "10": // get/set foreground colour
if len(pS) > 1 {
if pS[1] == "?" {
t.WriteToPty([]byte("\x1b]10;15"))
}
}
case "11": // get/set background colour
if len(pS) > 1 {
if pS[1] == "?" {
t.WriteToPty([]byte("\x1b]10;0"))
}
}
}
return false
}
func (t *Terminal) isOSCTerminator(r rune) bool {
for _, terminator := range oscTerminators {
if terminator == r {
return true
}
}
return false
}

View File

@ -0,0 +1,93 @@
package termutil
func (buffer *Buffer) shrink(width uint16) {
var replace []Line
prevCursor := int(buffer.cursorPosition.Line)
for i, line := range buffer.lines {
line.shrink(width)
// this line fits within the new width restriction, keep it as is and continue
if line.Len() <= width {
replace = append(replace, line)
continue
}
wrappedLines := line.wrap(width)
if prevCursor >= i {
buffer.cursorPosition.Line += uint64(len(wrappedLines) - 1)
}
replace = append(replace, wrappedLines...)
}
buffer.cursorPosition.Col = buffer.cursorPosition.Col % width
buffer.lines = replace
}
func (buffer *Buffer) grow(width uint16) {
var replace []Line
var current Line
prevCursor := int(buffer.cursorPosition.Line)
for i, line := range buffer.lines {
if !line.wrapped {
if i > 0 {
replace = append(replace, current)
}
current = newLine()
}
if i == prevCursor {
buffer.cursorPosition.Line -= uint64(i - len(replace))
}
for _, cell := range line.cells {
if len(current.cells) == int(width) {
replace = append(replace, current)
current = newLine()
current.wrapped = true
}
current.cells = append(current.cells, cell)
}
}
replace = append(replace, current)
buffer.lines = replace
}
// deprecated
func (buffer *Buffer) resizeView(width uint16, height uint16) {
if buffer.viewHeight == 0 {
buffer.viewWidth = width
buffer.viewHeight = height
return
}
// scroll to bottom
buffer.scrollLinesFromBottom = 0
if width < buffer.viewWidth { // wrap lines if we're shrinking
buffer.shrink(width)
buffer.grow(width)
} else if width > buffer.viewWidth { // unwrap lines if we're growing
buffer.grow(width)
}
buffer.viewWidth = width
buffer.viewHeight = height
buffer.resetVerticalMargins(uint(buffer.viewHeight))
}

View File

@ -0,0 +1,310 @@
package termutil
func (buffer *Buffer) ClearSelection() {
buffer.selectionStart = nil
buffer.selectionEnd = nil
}
func (buffer *Buffer) GetBoundedTextAtPosition(pos Position) (start Position, end Position, text string, textIndex int, found bool) {
return buffer.FindWordAt(pos, func(r rune) bool {
return r > 0 && r < 256
})
}
// if the selection is invalid - e.g. lines are selected that no longer exist in the buffer
func (buffer *Buffer) fixSelection() bool {
if buffer.selectionStart == nil || buffer.selectionEnd == nil {
return false
}
if buffer.selectionStart.Line >= uint64(len(buffer.lines)) {
buffer.selectionStart.Line = uint64(len(buffer.lines)) - 1
}
if buffer.selectionEnd.Line >= uint64(len(buffer.lines)) {
buffer.selectionEnd.Line = uint64(len(buffer.lines)) - 1
}
if buffer.selectionStart.Col >= uint16(len(buffer.lines[buffer.selectionStart.Line].cells)) {
buffer.selectionStart.Col = 0
if buffer.selectionStart.Line < uint64(len(buffer.lines))-1 {
buffer.selectionStart.Line++
}
}
if buffer.selectionEnd.Col >= uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) {
buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1
}
return true
}
func (buffer *Buffer) ExtendSelectionToEntireLines() {
if !buffer.fixSelection() {
return
}
buffer.selectionStart.Col = 0
buffer.selectionEnd.Col = uint16(len(buffer.lines[buffer.selectionEnd.Line].cells)) - 1
}
type RuneMatcher func(r rune) bool
func (buffer *Buffer) SelectWordAt(pos Position, runeMatcher RuneMatcher) {
start, end, _, _, found := buffer.FindWordAt(pos, runeMatcher)
if !found {
return
}
buffer.setRawSelectionStart(start)
buffer.setRawSelectionEnd(end)
}
// takes raw coords
func (buffer *Buffer) Highlight(start Position, end Position, annotation *Annotation) {
buffer.highlightStart = &start
buffer.highlightEnd = &end
buffer.highlightAnnotation = annotation
}
func (buffer *Buffer) ClearHighlight() {
buffer.highlightStart = nil
buffer.highlightEnd = nil
}
// returns raw lines
func (buffer *Buffer) FindWordAt(pos Position, runeMatcher RuneMatcher) (start Position, end Position, text string, textIndex int, found bool) {
line := buffer.convertViewLineToRawLine(uint16(pos.Line))
col := pos.Col
if line >= uint64(len(buffer.lines)) {
return
}
if col >= uint16(len(buffer.lines[line].cells)) {
return
}
if !runeMatcher(buffer.lines[line].cells[col].r.Rune) {
return
}
found = true
start = Position{
Line: line,
Col: col,
}
end = Position{
Line: line,
Col: col,
}
var startCol uint16
BACK:
for y := int(line); y >= 0; y-- {
if y == int(line) {
startCol = col
} else {
if len(buffer.lines[y].cells) < int(buffer.viewWidth) {
break
}
startCol = uint16(len(buffer.lines[y].cells) - 1)
}
for x := int(startCol); x >= 0; x-- {
if runeMatcher(buffer.lines[y].cells[x].r.Rune) {
start = Position{
Line: uint64(y),
Col: uint16(x),
}
text = string(buffer.lines[y].cells[x].r.Rune) + text
} else {
break BACK
}
}
}
textIndex = len([]rune(text)) - 1
FORWARD:
for y := uint64(line); y < uint64(len(buffer.lines)); y++ {
if y == line {
startCol = col + 1
} else {
startCol = 0
}
for x := int(startCol); x < len(buffer.lines[y].cells); x++ {
if runeMatcher(buffer.lines[y].cells[x].r.Rune) {
end = Position{
Line: y,
Col: uint16(x),
}
text = text + string(buffer.lines[y].cells[x].r.Rune)
} else {
break FORWARD
}
}
if len(buffer.lines[y].cells) < int(buffer.viewWidth) {
break
}
}
return
}
func (buffer *Buffer) SetSelectionStart(pos Position) {
buffer.selectionStart = &Position{
Col: pos.Col,
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
}
}
func (buffer *Buffer) setRawSelectionStart(pos Position) {
buffer.selectionStart = &pos
}
func (buffer *Buffer) SetSelectionEnd(pos Position) {
buffer.selectionEnd = &Position{
Col: pos.Col,
Line: buffer.convertViewLineToRawLine(uint16(pos.Line)),
}
}
func (buffer *Buffer) setRawSelectionEnd(pos Position) {
buffer.selectionEnd = &pos
}
func (buffer *Buffer) GetSelection() (string, *Selection) {
if !buffer.fixSelection() {
return "", nil
}
start := *buffer.selectionStart
end := *buffer.selectionEnd
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
swap := end
end = start
start = swap
}
var text string
for y := start.Line; y <= end.Line; y++ {
line := buffer.lines[y]
startX := 0
endX := len(line.cells) - 1
if y == start.Line {
startX = int(start.Col)
}
if y == end.Line {
endX = int(end.Col)
}
if y > start.Line {
text += "\n"
}
for x := startX; x <= endX; x++ {
mr := line.cells[x].Rune()
x += mr.Width - 1
text += string(mr.Rune)
}
}
viewSelection := Selection{
Start: start,
End: end,
}
viewSelection.Start.Line = uint64(buffer.convertRawLineToViewLine(viewSelection.Start.Line))
viewSelection.End.Line = uint64(buffer.convertRawLineToViewLine(viewSelection.End.Line))
return text, &viewSelection
}
func (buffer *Buffer) InSelection(pos Position) bool {
if !buffer.fixSelection() {
return false
}
start := *buffer.selectionStart
end := *buffer.selectionEnd
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
swap := end
end = start
start = swap
}
rY := buffer.convertViewLineToRawLine(uint16(pos.Line))
if rY < start.Line {
return false
}
if rY > end.Line {
return false
}
if rY == start.Line {
if pos.Col < start.Col {
return false
}
}
if rY == end.Line {
if pos.Col > end.Col {
return false
}
}
return true
}
func (buffer *Buffer) GetHighlightAnnotation() *Annotation {
return buffer.highlightAnnotation
}
// takes view coords
func (buffer *Buffer) IsHighlighted(pos Position) bool {
if buffer.highlightStart == nil || buffer.highlightEnd == nil {
return false
}
if buffer.highlightStart.Line >= uint64(len(buffer.lines)) {
return false
}
if buffer.highlightEnd.Line >= uint64(len(buffer.lines)) {
return false
}
if buffer.highlightStart.Col >= uint16(len(buffer.lines[buffer