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.highlightStart.Line].cells)) {
return false
}
if buffer.highlightEnd.Col >= uint16(len(buffer.lines[buffer.highlightEnd.Line].cells)) {
return false
}
start := *buffer.highlightStart
end := *buffer.highlightEnd
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
}

View File

@ -0,0 +1,106 @@
package termutil
import (
"image"
"math"
"strings"
"github.com/liamg/darktile/internal/app/darktile/sixel"
)
type Sixel struct {
X uint16
Y uint64 // raw line
Width uint64
Height uint64
Image image.Image
}
type VisibleSixel struct {
ViewLineOffset int
Sixel Sixel
}
func (b *Buffer) addSixel(img image.Image, widthCells int, heightCells int) {
b.sixels = append(b.sixels, Sixel{
X: b.CursorColumn(),
Y: b.cursorPosition.Line,
Width: uint64(widthCells),
Height: uint64(heightCells),
Image: img,
})
if b.modes.SixelScrolling {
b.cursorPosition.Line += uint64(heightCells)
}
}
func (b *Buffer) clearSixelsAtRawLine(rawLine uint64) {
var filtered []Sixel
for _, sixelImage := range b.sixels {
if sixelImage.Y+sixelImage.Height-1 >= rawLine && sixelImage.Y <= rawLine {
continue
}
filtered = append(filtered, sixelImage)
}
b.sixels = filtered
}
func (b *Buffer) GetVisibleSixels() []VisibleSixel {
firstLine := b.convertViewLineToRawLine(0)
lastLine := b.convertViewLineToRawLine(b.viewHeight - 1)
var visible []VisibleSixel
for _, sixelImage := range b.sixels {
if sixelImage.Y+sixelImage.Height-1 < firstLine {
continue
}
if sixelImage.Y > lastLine {
continue
}
visible = append(visible, VisibleSixel{
ViewLineOffset: int(sixelImage.Y) - int(firstLine),
Sixel: sixelImage,
})
}
return visible
}
func (t *Terminal) handleSixel(readChan chan MeasuredRune) (renderRequired bool) {
var data []rune
var inEscape bool
for {
r := <-readChan
switch r.Rune {
case 0x1b:
inEscape = true
continue
case 0x5c:
if inEscape {
img, err := sixel.Decode(strings.NewReader(string(data)), t.theme.DefaultBackground())
if err != nil {
return false
}
w, h := t.windowManipulator.CellSizeInPixels()
cw := int(math.Ceil(float64(img.Bounds().Dx()) / float64(w)))
ch := int(math.Ceil(float64(img.Bounds().Dy()) / float64(h)))
t.activeBuffer.addSixel(img, cw, ch)
return true
}
}
inEscape = false
data = append(data, r.Rune)
}
}

View File

@ -0,0 +1,295 @@
package termutil
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"os/exec"
"github.com/creack/pty"
"golang.org/x/term"
)
const (
MainBuffer uint8 = 0
AltBuffer uint8 = 1
InternalBuffer uint8 = 2
)
// Terminal communicates with the underlying terminal
type Terminal struct {
windowManipulator WindowManipulator
pty *os.File
updateChan chan struct{}
processChan chan MeasuredRune
closeChan chan struct{}
buffers []*Buffer
activeBuffer *Buffer
logFile *os.File
theme *Theme
running bool
shell string
initialCommand string
}
// NewTerminal creates a new terminal instance
func New(options ...Option) *Terminal {
term := &Terminal{
processChan: make(chan MeasuredRune, 0xffff),
closeChan: make(chan struct{}),
theme: &Theme{},
}
for _, opt := range options {
opt(term)
}
fg := term.theme.DefaultForeground()
bg := term.theme.DefaultBackground()
term.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
term.activeBuffer = term.buffers[0]
return term
}
func (t *Terminal) SetWindowManipulator(m WindowManipulator) {
t.windowManipulator = m
}
func (t *Terminal) log(line string, params ...interface{}) {
if t.logFile != nil {
_, _ = fmt.Fprintf(t.logFile, line+"\n", params...)
}
}
func (t *Terminal) reset() {
fg := t.theme.DefaultForeground()
bg := t.theme.DefaultBackground()
t.buffers = []*Buffer{
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
NewBuffer(1, 1, 0xffff, fg, bg),
}
t.useMainBuffer()
}
// Pty exposes the underlying terminal pty, if it exists
func (t *Terminal) Pty() *os.File {
return t.pty
}
func (t *Terminal) WriteToPty(data []byte) error {
_, err := t.pty.Write(data)
return err
}
func (t *Terminal) GetTitle() string {
return t.windowManipulator.GetTitle()
}
func (t *Terminal) Theme() *Theme {
return t.theme
}
// write takes data from StdOut of the child shell and processes it
func (t *Terminal) Write(data []byte) (n int, err error) {
reader := bufio.NewReader(bytes.NewBuffer(data))
for {
r, size, err := reader.ReadRune()
if err == io.EOF {
break
}
t.processChan <- MeasuredRune{Rune: r, Width: size}
}
return len(data), nil
}
func (t *Terminal) SetSize(rows, cols uint16) error {
if t.pty == nil {
return fmt.Errorf("terminal is not running")
}
t.log("RESIZE %d, %d\n", cols, rows)
t.activeBuffer.resizeView(cols, rows)
if err := pty.Setsize(t.pty, &pty.Winsize{
Rows: rows,
Cols: cols,
}); err != nil {
return err
}
return nil
}
// Run starts the terminal/shell proxying process
func (t *Terminal) Run(updateChan chan struct{}, rows uint16, cols uint16) error {
os.Setenv("TERM", "xterm-256color")
t.updateChan = updateChan
if t.shell == "" {
t.shell = os.Getenv("SHELL")
if t.shell == "" {
t.shell = "/bin/sh"
}
}
// Create arbitrary command.
c := exec.Command(t.shell)
// Start the command with a pty.
var err error
t.pty, err = pty.Start(c)
if err != nil {
return err
}
// Make sure to close the pty at the end.
defer func() { _ = t.pty.Close() }() // Best effort.
if err := t.SetSize(rows, cols); err != nil {
return err
}
// Set stdin in raw mode.
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
t.windowManipulator.ReportError(err)
}
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
go t.process()
t.running = true
t.windowManipulator.SetTitle("darktile")
if t.initialCommand != "" {
if err := t.WriteToPty([]byte(t.initialCommand)); err != nil {
return err
}
}
_, _ = io.Copy(t, t.pty)
close(t.closeChan)
return nil
}
func (t *Terminal) IsRunning() bool {
return t.running
}
func (t *Terminal) requestRender() {
select {
case t.updateChan <- struct{}{}:
default:
}
}
func (t *Terminal) process() {
for {
select {
case <-t.closeChan:
return
case mr := <-t.processChan:
if mr.Rune == 0x1b { // ANSI escape char, which means this is a sequence
if t.handleANSI(t.processChan) {
t.requestRender()
}
} else if t.processRunes(mr) { // otherwise it's just an individual rune we need to process
t.requestRender()
}
}
}
}
func (t *Terminal) processRunes(runes ...MeasuredRune) (renderRequired bool) {
for _, r := range runes {
t.log("%c 0x%X", r.Rune, r.Rune)
switch r.Rune {
case 0x05: //enq
continue
case 0x07: //bell
//DING DING DING
continue
case 0x8: //backspace
t.GetActiveBuffer().backspace()
renderRequired = true
case 0x9: //tab
t.GetActiveBuffer().tab()
renderRequired = true
case 0xa, 0xc: //newLine/form feed
t.GetActiveBuffer().newLine()
renderRequired = true
case 0xb: //vertical tab
t.GetActiveBuffer().verticalTab()
renderRequired = true
case 0xd: //carriageReturn
t.GetActiveBuffer().carriageReturn()
renderRequired = true
case 0xe: //shiftOut
t.GetActiveBuffer().currentCharset = 1
case 0xf: //shiftIn
t.GetActiveBuffer().currentCharset = 0
default:
if r.Rune < 0x20 {
// handle any other control chars here?
continue
}
t.GetActiveBuffer().write(t.translateRune(r))
renderRequired = true
}
}
return renderRequired
}
func (t *Terminal) translateRune(b MeasuredRune) MeasuredRune {
table := t.GetActiveBuffer().charsets[t.GetActiveBuffer().currentCharset]
if table == nil {
return b
}
chr, ok := (*table)[b.Rune]
if ok {
return MeasuredRune{Rune: chr, Width: 1}
}
return b
}
func (t *Terminal) setTitle(title string) {
t.windowManipulator.SetTitle(title)
}
func (t *Terminal) switchBuffer(index uint8) {
var carrySize bool
var w, h uint16
if t.activeBuffer != nil {
w, h = t.activeBuffer.viewWidth, t.activeBuffer.viewHeight
carrySize = true
}
t.activeBuffer = t.buffers[index]
if carrySize {
t.activeBuffer.resizeView(w, h)
}
}
func (t *Terminal) GetActiveBuffer() *Buffer {
return t.activeBuffer
}
func (t *Terminal) useMainBuffer() {
t.switchBuffer(MainBuffer)
}
func (t *Terminal) useAltBuffer() {
t.switchBuffer(AltBuffer)
}

View File

@ -0,0 +1,215 @@
package termutil
import (
"fmt"
"image/color"
"strconv"
)
type Colour uint8
// See https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
const (
ColourBlack Colour = iota
ColourRed
ColourGreen
ColourYellow
ColourBlue
ColourMagenta
ColourCyan
ColourWhite
ColourBrightBlack
ColourBrightRed
ColourBrightGreen
ColourBrightYellow
ColourBrightBlue
ColourBrightMagenta
ColourBrightCyan
ColourBrightWhite
ColourBackground
ColourForeground
ColourSelectionBackground
ColourSelectionForeground
ColourCursorForeground
ColourCursorBackground
)
type Theme struct {
alpha uint8
colourMap map[Colour]color.Color
}
var (
map4Bit = map[uint8]Colour{
30: ColourBlack,
31: ColourRed,
32: ColourGreen,
33: ColourYellow,
34: ColourBlue,
35: ColourMagenta,
36: ColourCyan,
37: ColourWhite,
90: ColourBrightBlack,
91: ColourBrightRed,
92: ColourBrightGreen,
93: ColourBrightYellow,
94: ColourBrightBlue,
95: ColourBrightMagenta,
96: ColourBrightCyan,
97: ColourBrightWhite,
40: ColourBlack,
41: ColourRed,
42: ColourGreen,
43: ColourYellow,
44: ColourBlue,
45: ColourMagenta,
46: ColourCyan,
47: ColourWhite,
100: ColourBrightBlack,
101: ColourBrightRed,
102: ColourBrightGreen,
103: ColourBrightYellow,
104: ColourBrightBlue,
105: ColourBrightMagenta,
106: ColourBrightCyan,
107: ColourBrightWhite,
}
)
func (t *Theme) ColourFrom4Bit(code uint8) color.Color {
colour, ok := map4Bit[code]
if !ok {
return color.Black
}
return t.colourMap[colour]
}
func (t *Theme) DefaultBackground() color.Color {
c, ok := t.colourMap[ColourBackground]
if !ok {
return color.RGBA{0, 0, 0, t.alpha}
}
return c
}
func (t *Theme) DefaultForeground() color.Color {
c, ok := t.colourMap[ColourForeground]
if !ok {
return color.RGBA{255, 255, 255, t.alpha}
}
return c
}
func (t *Theme) SelectionBackground() color.Color {
c, ok := t.colourMap[ColourSelectionBackground]
if !ok {
return color.RGBA{0, 0, 0, t.alpha}
}
return c
}
func (t *Theme) SelectionForeground() color.Color {
c, ok := t.colourMap[ColourSelectionForeground]
if !ok {
return color.RGBA{255, 255, 255, t.alpha}
}
return c
}
func (t *Theme) CursorBackground() color.Color {
c, ok := t.colourMap[ColourCursorBackground]
if !ok {
return color.RGBA{255, 255, 255, t.alpha}
}
return c
}
func (t *Theme) CursorForeground() color.Color {
c, ok := t.colourMap[ColourCursorForeground]
if !ok {
return color.RGBA{0, 0, 0, t.alpha}
}
return c
}
func (t *Theme) ColourFrom8Bit(n string) (color.Color, error) {
index, err := strconv.Atoi(n)
if err != nil {
return nil, err
}
if index < 16 {
return t.colourMap[Colour(index)], nil
}
if index >= 232 {
c := ((index - 232) * 0xff) / 0x18
return color.RGBA{
R: byte(c),
G: byte(c),
B: byte(c),
A: t.alpha,
}, nil
}
var colour color.RGBA
colour.A = t.alpha
indexR := ((index - 16) / 36)
if indexR > 0 {
colour.R = uint8(55 + indexR*40)
}
indexG := (((index - 16) % 36) / 6)
if indexG > 0 {
colour.G = uint8(55 + indexG*40)
}
indexB := ((index - 16) % 6)
if indexB > 0 {
colour.B = uint8(55 + indexB*40)
}
return colour, nil
}
func (t *Theme) ColourFrom24Bit(r, g, b string) (color.Color, error) {
ri, err := strconv.Atoi(r)
if err != nil {
return nil, err
}
gi, err := strconv.Atoi(g)
if err != nil {
return nil, err
}
bi, err := strconv.Atoi(b)
if err != nil {
return nil, err
}
return color.RGBA{
R: byte(ri),
G: byte(gi),
B: byte(bi),
A: t.alpha,
}, nil
}
func (t *Theme) ColourFromAnsi(ansi []string, bg bool) (color.Color, error) {
if len(ansi) == 0 {
return nil, fmt.Errorf("invalid ansi colour code")
}
switch ansi[0] {
case "2":
if len(ansi) != 4 {
return nil, fmt.Errorf("invalid 24-bit ansi colour code")
}
return t.ColourFrom24Bit(ansi[1], ansi[2], ansi[3])
case "5":
if len(ansi) != 2 {
return nil, fmt.Errorf("invalid 8-bit ansi colour code")
}
return t.ColourFrom8Bit(ansi[1])
default:
return nil, fmt.Errorf("invalid ansi colour code")
}
}

View File

@ -0,0 +1,41 @@
package termutil
import "image/color"
type ThemeFactory struct {
theme *Theme
colourMap map[Colour]color.Color
}
func NewThemeFactory() *ThemeFactory {
return &ThemeFactory{
theme: &Theme{
alpha: 0xff,
colourMap: map[Colour]color.Color{},
},
colourMap: make(map[Colour]color.Color),
}
}
func (t *ThemeFactory) Build() *Theme {
for id, col := range t.colourMap {
r, g, b, _ := col.RGBA()
t.theme.colourMap[id] = color.RGBA{
R: uint8(r / 0xff),
G: uint8(g / 0xff),
B: uint8(b / 0xff),
A: t.theme.alpha,
}
}
return t.theme
}
func (t *ThemeFactory) WithOpacity(opacity float64) *ThemeFactory {
t.theme.alpha = uint8(0xff * opacity)
return t
}
func (t *ThemeFactory) WithColour(key Colour, colour color.Color) *ThemeFactory {
t.colourMap[key] = colour
return t
}

View File

@ -0,0 +1,3 @@
package version
var Version = "unknown (local build)"

9
scripts/256-colour.sh Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
for i in {0..255} ; do
printf "\x1b[48;5;%sm%3d\e[0m " "$i" "$i"
if (( i == 15 )) || (( i > 15 )) && (( (i-15) % 16 == 0 )); then
printf "\n";
fi
done

7
scripts/build.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
version=$(git describe --exact-match --tags 2>/dev/null || git describe 2>/dev/null || echo "prerelease")
go build \
-mod=vendor\
-ldflags="-X github.com/liamg/darktile/internal/app/darktile/version.Version=${version}" \
./cmd/darktile

16
scripts/true-colour.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
awk -v term_cols="${width:-$(tput cols || echo 80)}" 'BEGIN{
s=" ";
for (colnum = 0; colnum<term_cols; colnum++) {
r = 255-(colnum*255/term_cols);
g = (colnum*510/term_cols);
b = (colnum*255/term_cols);
if (g>255) g = 510-g;
printf "\033[48;2;%d;%d;%dm", r,g,b;
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
printf "%s\033[0m", substr(s,colnum%2+1,1);
}
printf "\n";
}'

23
vendor/git.wow.st/gmp/clip/README.md vendored Normal file
View File

@ -0,0 +1,23 @@
# Clip
A tiny library to access the MacOS clipboard (a.k.a. NSPasteboard).
```go
go get git.wow.st/gmp/clip
```
## API:
```go
package clip
// Clear clears the general pasteboard
func Clear()
// Set puts a string on the pasteboard, returning true if successful
func Set(string) bool
// Get retrieves the string currently on the pasteboard.
func Get() string
```

34
vendor/git.wow.st/gmp/clip/main.go vendored Normal file
View File

@ -0,0 +1,34 @@
package clip
import (
"git.wow.st/gmp/clip/ns"
)
var pb *ns.NSPasteboard
func Clear() {
if pb == nil {
pb = ns.NSPasteboardGeneralPasteboard()
}
pb.ClearContents()
}
func Set(x string) bool {
if pb == nil {
pb = ns.NSPasteboardGeneralPasteboard()
}
pb.ClearContents()
return pb.SetString(x)
}
func Get() string {
if pb == nil {
pb = ns.NSPasteboardGeneralPasteboard()
}
ret := pb.GetString()
if ret.Ptr() == nil {
return ""
} else {
return ret.String()
}
}

6056
vendor/git.wow.st/gmp/clip/ns/main.go- vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
package ns
/*
#cgo CFLAGS: -x objective-c -fno-objc-arc
#cgo LDFLAGS: -framework AppKit -framework Foundation
#pragma clang diagnostic ignored "-Wformat-security"
#import <Foundation/Foundation.h>
#import <AppKit/NSPasteboard.h>
void
NSObject_inst_Release(void* o) {
@autoreleasepool {
[(NSObject*)o release];
}
}
void
NSString_inst_Release(void* o) {
@autoreleasepool {
[(NSString*)o release];
}
}
const void* _Nullable
NSString_inst_UTF8String(void* o) {
const char* _Nullable ret;
@autoreleasepool {
ret = strdup([(NSString*)o UTF8String]);
}
return ret;
}
void
NSPasteboard_inst_Release(void* o) {
@autoreleasepool {
[(NSPasteboard*)o release];
}
}
void* _Nullable
NSString_StringWithUTF8String(void* nullTerminatedCString) {
NSString* _Nullable ret;
@autoreleasepool {
ret = [NSString stringWithUTF8String:nullTerminatedCString];
if(ret != nil) { [ret retain]; }
}
return ret;
}
void* _Nonnull
NSPasteboard_GeneralPasteboard() {
NSPasteboard* _Nonnull ret;
@autoreleasepool {
ret = [NSPasteboard generalPasteboard];
}
return ret;
}
void
NSPasteboard_inst_ClearContents(void* o) {
@autoreleasepool {
[(NSPasteboard*)o clearContents];
}
}
BOOL
NSPasteboard_inst_SetString(void* o, void* string) {
BOOL ret;
@autoreleasepool {
ret = [(NSPasteboard*)o setString:string forType:NSPasteboardTypeString];
}
return ret;
}
void* _Nullable
NSPasteboard_inst_GetString(void* o) {
NSString* _Nullable ret;
@autoreleasepool {
ret = [(NSPasteboard*)o stringForType:NSPasteboardTypeString];
if (ret != nil && ret != o) { [ret retain]; }
}
return ret;
}
*/
import "C"
import (
"unsafe"
"runtime"
)
type Id struct {
ptr unsafe.Pointer
}
func (o *Id) Ptr() unsafe.Pointer { if o == nil { return nil }; return o.ptr }
type NSObject interface {
Ptr() unsafe.Pointer
}
func (o *Id) Release() {
C.NSObject_inst_Release(o.Ptr())
runtime.KeepAlive(o)
}
func (o *NSPasteboard) Release() {
C.NSPasteboard_inst_Release(o.Ptr())
runtime.KeepAlive(o)
}
func (o *NSString) Release() {
C.NSString_inst_Release(o.Ptr())
runtime.KeepAlive(o)
}
func (c *Char) Free() {
C.free(unsafe.Pointer(c))
}
type BOOL C.uchar
type NSString struct { Id }
func (o *NSString) Ptr() unsafe.Pointer { if o == nil { return nil }; return o.ptr }
func (o *Id) NSString() *NSString {
return (*NSString)(unsafe.Pointer(o))
}
func (o *NSString) UTF8String() *Char {
ret := (*Char)(unsafe.Pointer(C.NSString_inst_UTF8String(o.Ptr())))
runtime.KeepAlive(o)
return ret
}
func (o *NSString) String() string {
utf8 := o.UTF8String()
ret := utf8.String()
utf8.Free()
runtime.KeepAlive(o)
return ret
}
type NSPasteboard struct { Id }
func (o *NSPasteboard) Ptr() unsafe.Pointer { if o == nil { return nil }; return o.ptr }
func (o *Id) NSPasteboard() *NSPasteboard {
return (*NSPasteboard)(unsafe.Pointer(o))
}
type Char C.char
func CharWithGoString(s string) *Char {
return (*Char)(unsafe.Pointer(C.CString(s)))
}
func (c *Char) String() string {
return C.GoString((*C.char)(c))
}
func NSStringWithUTF8String(nullTerminatedCString *Char) *NSString {
ret := &NSString{}
ret.ptr = unsafe.Pointer(C.NSString_StringWithUTF8String(unsafe.Pointer(nullTerminatedCString)))
if ret.ptr == nil { return ret }
runtime.SetFinalizer(ret, func(o *NSString) {
o.Release()
})
return ret
}
func NSStringWithGoString(string string) *NSString {
string_chr := CharWithGoString(string)
defer string_chr.Free()
ret := NSStringWithUTF8String(string_chr)
return ret
}
func NSPasteboardGeneralPasteboard() *NSPasteboard {
ret := &NSPasteboard{}
ret.ptr = unsafe.Pointer(C.NSPasteboard_GeneralPasteboard())
if ret.ptr == nil { return ret }
return ret
}
func (o *NSPasteboard) ClearContents() {
C.NSPasteboard_inst_ClearContents(o.Ptr())
runtime.KeepAlive(o)
}
func (o *NSPasteboard) SetString(s string) bool {
string := NSStringWithGoString(s)
ret := (C.NSPasteboard_inst_SetString(o.Ptr(), string.Ptr())) != 0
runtime.KeepAlive(o)
runtime.KeepAlive(string)
return ret
}
func (o *NSPasteboard) GetString() *NSString {
ret := &NSString{}
ret.ptr = unsafe.Pointer(C.NSPasteboard_inst_GetString(o.Ptr()))
if ret.ptr == nil { runtime.KeepAlive(o); return ret }
if ret.ptr == o.ptr { runtime.KeepAlive(o); return (*NSString)(unsafe.Pointer(o)) }
runtime.SetFinalizer(ret, func(o *NSString) {
o.Release()
})
runtime.KeepAlive(o)
return ret
}

11
vendor/git.wow.st/gmp/clip/nswrap.yaml vendored Normal file
View File

@ -0,0 +1,11 @@
# binding generator for git.wow.st/gmp/nswrap
# original binding is in ns/main.go- and is not used.
inputfiles:
- /System/Library/Frameworks/AppKit.framework/Headers/NSPasteboard.h
classes:
- NSPasteboard
- NSString
enums:
- NSPasteboard.*
frameworks: [ AppKit, Foundation ]
pragma: [ clang diagnostic ignored "-Wformat-security" ]

2
vendor/github.com/BurntSushi/xgb/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
xgbgen/xgbgen
.*.swp

18
vendor/github.com/BurntSushi/xgb/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,18 @@
Andrew Gallant is the maintainer of this fork. What follows is the original
list of authors for the x-go-binding.
# This is the official list of XGB authors for copyright purposes.
# This file is distinct from the CONTRIBUTORS files.
# See the latter for an explanation.
# Names should be added to this file as
# Name or Organization <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
Anthony Martin <ality@pbrane.org>
Firmansyah Adiputra <frm.adiputra@gmail.com>
Google Inc.
Scott Lawrence <bytbox@gmail.com>
Tor Andersson <tor.andersson@gmail.com>

39
vendor/github.com/BurntSushi/xgb/CONTRIBUTORS generated vendored Normal file
View File

@ -0,0 +1,39 @@
Andrew Gallant is the maintainer of this fork. What follows is the original
list of contributors for the x-go-binding.
# This is the official list of people who can contribute
# (and typically have contributed) code to the XGB repository.
# The AUTHORS file lists the copyright holders; this file
# lists people. For example, Google employees are listed here
# but not in AUTHORS, because Google holds the copyright.
#
# The submission process automatically checks to make sure
# that people submitting code are listed in this file (by email address).
#
# Names should be added to this file only after verifying that
# the individual or the individual's organization has agreed to
# the appropriate Contributor License Agreement, found here:
#
# http://code.google.com/legal/individual-cla-v1.0.html
# http://code.google.com/legal/corporate-cla-v1.0.html
#
# The agreement for individuals can be filled out on the web.
#
# When adding J Random Contributor's name to this file,
# either J's name or J's organization's name should be
# added to the AUTHORS file, depending on whether the
# individual or corporate CLA was used.
# Names should be added to this file like so:
# Name <email address>
# Please keep the list sorted.
Anthony Martin <ality@pbrane.org>
Firmansyah Adiputra <frm.adiputra@gmail.com>
Ian Lance Taylor <iant@golang.org>
Nigel Tao <nigeltao@golang.org>
Robert Griesemer <gri@golang.org>
Russ Cox <rsc@golang.org>
Scott Lawrence <bytbox@gmail.com>
Tor Andersson <tor.andersson@gmail.com>

42
vendor/github.com/BurntSushi/xgb/LICENSE generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2009 The XGB Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Subject to the terms and conditions of this License, Google hereby
// grants to You a perpetual, worldwide, non-exclusive, no-charge,
// royalty-free, irrevocable (except as stated in this section) patent
// license to make, have made, use, offer to sell, sell, import, and
// otherwise transfer this implementation of XGB, where such license
// applies only to those patent claims licensable by Google that are
// necessarily infringed by use of this implementation of XGB. If You
// institute patent litigation against any entity (including a
// cross-claim or counterclaim in a lawsuit) alleging that this
// implementation of XGB or a Contribution incorporated within this
// implementation of XGB constitutes direct or contributory patent
// infringement, then any patent licenses granted to You under this
// License for this implementation of XGB shall terminate as of the date
// such litigation is filed.

78
vendor/github.com/BurntSushi/xgb/Makefile generated vendored Normal file
View File

@ -0,0 +1,78 @@
# This Makefile is used by the developer. It is not needed in any way to build
# a checkout of the XGB repository.
# It will be useful, however, if you are hacking at the code generator.
# i.e., after making a change to the code generator, run 'make' in the
# xgb directory. This will build xgbgen and regenerate each sub-package.
# 'make test' will then run any appropriate tests (just tests xproto right now).
# 'make bench' will test a couple of benchmarks.
# 'make build-all' will then try to build each extension. This isn't strictly
# necessary, but it's a good idea to make sure each sub-package is a valid
# Go package.
# My path to the X protocol XML descriptions.
XPROTO=/usr/share/xcb
# All of the XML files in my /usr/share/xcb directory EXCEPT XKB. -_-
# This is intended to build xgbgen and generate Go code for each supported
# extension.
all: build-xgbgen \
bigreq.xml composite.xml damage.xml dpms.xml dri2.xml \
ge.xml glx.xml randr.xml record.xml render.xml res.xml \
screensaver.xml shape.xml shm.xml xc_misc.xml \
xevie.xml xf86dri.xml xf86vidmode.xml xfixes.xml xinerama.xml \
xprint.xml xproto.xml xselinux.xml xtest.xml \
xvmc.xml xv.xml
build-xgbgen:
(cd xgbgen && go build)
# Builds each individual sub-package to make sure its valid Go code.
build-all: bigreq.b composite.b damage.b dpms.b dri2.b ge.b glx.b randr.b \
record.b render.b res.b screensaver.b shape.b shm.b xcmisc.b \
xevie.b xf86dri.b xf86vidmode.b xfixes.b xinerama.b \
xprint.b xproto.b xselinux.b xtest.b xv.b xvmc.b
%.b:
(cd $* ; go build)
# Installs each individual sub-package.
install: bigreq.i composite.i damage.i dpms.i dri2.i ge.i glx.i randr.i \
record.i render.i res.i screensaver.i shape.i shm.i xcmisc.i \
xevie.i xf86dri.i xf86vidmode.i xfixes.i xinerama.i \
xprint.i xproto.i xselinux.i xtest.i xv.i xvmc.i
go install
%.i:
(cd $* ; go install)
# xc_misc is special because it has an underscore.
# There's probably a way to do this better, but Makefiles aren't my strong suit.
xc_misc.xml: build-xgbgen
mkdir -p xcmisc
xgbgen/xgbgen --proto-path $(XPROTO) $(XPROTO)/xc_misc.xml > xcmisc/xcmisc.go
%.xml: build-xgbgen
mkdir -p $*
xgbgen/xgbgen --proto-path $(XPROTO) $(XPROTO)/$*.xml > $*/$*.go
# Just test the xproto core protocol for now.
test:
(cd xproto ; go test)
# Force all xproto benchmarks to run and no tests.
bench:
(cd xproto ; go test -run 'nomatch' -bench '.*' -cpu 1,2,3,6)
# gofmt all non-auto-generated code.
# (auto-generated code is already gofmt'd.)
# Also do a column check (80 cols) after a gofmt.
# But don't check columns on auto-generated code, since I don't care if they
# break 80 cols.
gofmt:
gofmt -w *.go xgbgen/*.go examples/*.go examples/*/*.go xproto/xproto_test.go
colcheck *.go xgbgen/*.go examples/*.go examples/*/*.go xproto/xproto_test.go
push:
git push origin master
git push github master

54
vendor/github.com/BurntSushi/xgb/README generated vendored Normal file
View File

@ -0,0 +1,54 @@
XGB is the X Go Binding, which is a low-level API to communicate with the
core X protocol and many of the X extensions. It is closely modeled after
XCB and xpyb.
It is thread safe and gets immediate improvement from parallelism when
GOMAXPROCS > 1. (See the benchmarks in xproto/xproto_test.go for evidence.)
Please see doc.go for more info.
Note that unless you know you need XGB, you can probably make your life
easier by using a slightly higher level library: xgbutil.
Quick Usage
===========
go get github.com/BurntSushi/xgb
go run go/path/src/github.com/BurntSushi/xgb/examples/create-window/main.go
BurntSushi's Fork
=================
I've forked the XGB repository from Google Code due to inactivty upstream.
Godoc documentation can be found here:
https://godoc.org/github.com/BurntSushi/xgb
Much of the code has been rewritten in an effort to support thread safety
and multiple extensions. Namely, go_client.py has been thrown away in favor
of an xgbgen package.
The biggest parts that *haven't* been rewritten by me are the connection and
authentication handshakes. They're inherently messy, and there's really no
reason to re-work them. The rest of XGB has been completely rewritten.
I like to release my code under the WTFPL, but since I'm starting with someone
else's work, I'm leaving the original license/contributor/author information
in tact.
I suppose I can legitimately release xgbgen under the WTFPL. To be fair, it is
at least as complex as XGB itself. *sigh*
What follows is the original README:
XGB README
==========
XGB is the X protocol Go language Binding.
It is the Go equivalent of XCB, the X protocol C-language Binding
(http://xcb.freedesktop.org/).
Unless otherwise noted, the XGB source files are distributed
under the BSD-style license found in the LICENSE file.
Contributions should follow the same procedure as for the Go project:
http://golang.org/doc/contribute.html

29
vendor/github.com/BurntSushi/xgb/STYLE generated vendored Normal file
View File

@ -0,0 +1,29 @@
I like to keep all my code to 80 columns or less. I have plenty of screen real
estate, but enjoy 80 columns so that I can have multiple code windows open side
to side and not be plagued by the ugly auto-wrapping of a text editor.
If you don't oblige me, I will fix any patch you submit to abide 80 columns.
Note that this style restriction does not preclude gofmt, but introduces a few
peculiarities. The first is that gofmt will occasionally add spacing (typically
to comments) that ends up going over 80 columns. Either shorten the comment or
put it on its own line.
The second and more common hiccup is when a function definition extends beyond
80 columns. If one adds line breaks to keep it below 80 columns, gofmt will
indent all subsequent lines in a function definition to the same indentation
level of the function body. This results in a less-than-ideal separation
between function definition and function body. To remedy this, simply add a
line break like so:
func RestackWindowExtra(xu *xgbutil.XUtil, win xproto.Window, stackMode int,
sibling xproto.Window, source int) error {
return ClientEvent(xu, win, "_NET_RESTACK_WINDOW", source, int(sibling),
stackMode)
}
Something similar should also be applied to long 'if' or 'for' conditionals,
although it would probably be preferrable to break up the conditional to
smaller chunks with a few helper variables.

110
vendor/github.com/BurntSushi/xgb/auth.go generated vendored Normal file
View File

@ -0,0 +1,110 @@
package xgb
/*
auth.go contains functions to facilitate the parsing of .Xauthority files.
It is largely unmodified from the original XGB package that I forked.
*/
import (
"encoding/binary"
"errors"
"io"
"os"
)
// readAuthority reads the X authority file for the DISPLAY.
// If hostname == "" or hostname == "localhost",
// then use the system's hostname (as returned by os.Hostname) instead.
func readAuthority(hostname, display string) (
name string, data []byte, err error) {
// b is a scratch buffer to use and should be at least 256 bytes long
// (i.e. it should be able to hold a hostname).
b := make([]byte, 256)
// As per /usr/include/X11/Xauth.h.
const familyLocal = 256
const familyWild = 65535
if len(hostname) == 0 || hostname == "localhost" {
hostname, err = os.Hostname()
if err != nil {
return "", nil, err
}
}
fname := os.Getenv("XAUTHORITY")
if len(fname) == 0 {
home := os.Getenv("HOME")
if len(home) == 0 {
err = errors.New("Xauthority not found: $XAUTHORITY, $HOME not set")
return "", nil, err
}
fname = home + "/.Xauthority"
}
r, err := os.Open(fname)
if err != nil {
return "", nil, err
}
defer r.Close()
for {
var family uint16
if err := binary.Read(r, binary.BigEndian, &family); err != nil {
return "", nil, err
}
addr, err := getString(r, b)
if err != nil {
return "", nil, err
}
disp, err := getString(r, b)
if err != nil {
return "", nil, err
}
name0, err := getString(r, b)
if err != nil {
return "", nil, err
}
data0, err := getBytes(r, b)
if err != nil {
return "", nil, err
}
addrmatch := (family == familyWild) ||
(family == familyLocal && addr == hostname)
dispmatch := (disp == "") || (disp == display)
if addrmatch && dispmatch {
return name0, data0, nil
}
}
panic("unreachable")
}
func getBytes(r io.Reader, b []byte) ([]byte, error) {
var n uint16
if err := binary.Read(r, binary.BigEndian, &n); err != nil {
return nil, err
} else if n > uint16(len(b)) {
return nil, errors.New("bytes too long for buffer")
}
if _, err := io.ReadFull(r, b[0:n]); err != nil {
return nil, err
}
return b[0:n], nil
}
func getString(r io.Reader, b []byte) (string, error) {
b, err := getBytes(r, b)
if err != nil {
return "", err
}
return string(b), nil
}

185
vendor/github.com/BurntSushi/xgb/conn.go generated vendored Normal file
View File

@ -0,0 +1,185 @@
package xgb
/*
conn.go contains a couple of functions that do some real dirty work related
to the initial connection handshake with X.
This code is largely unmodified from the original XGB package that I forked.
*/
import (
"errors"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
)
// connect connects to the X server given in the 'display' string,
// and does all the necessary setup handshaking.
// If 'display' is empty it will be taken from os.Getenv("DISPLAY").
// Note that you should read and understand the "Connection Setup" of the
// X Protocol Reference Manual before changing this function:
// http://goo.gl/4zGQg
func (c *Conn) connect(display string) error {
err := c.dial(display)
if err != nil {
return err
}
return c.postConnect()
}
// connect init from to the net.Conn,
func (c *Conn) connectNet(netConn net.Conn) error {
c.conn = netConn
return c.postConnect()
}
// do the postConnect action after Conn get it's underly net.Conn
func (c *Conn) postConnect() error {
// Get authentication data
authName, authData, err := readAuthority(c.host, c.display)
noauth := false
if err != nil {
Logger.Printf("Could not get authority info: %v", err)
Logger.Println("Trying connection without authority info...")
authName = ""
authData = []byte{}
noauth = true
}
// Assume that the authentication protocol is "MIT-MAGIC-COOKIE-1".
if !noauth && (authName != "MIT-MAGIC-COOKIE-1" || len(authData) != 16) {
return errors.New("unsupported auth protocol " + authName)
}
buf := make([]byte, 12+Pad(len(authName))+Pad(len(authData)))
buf[0] = 0x6c
buf[1] = 0
Put16(buf[2:], 11)
Put16(buf[4:], 0)
Put16(buf[6:], uint16(len(authName)))
Put16(buf[8:], uint16(len(authData)))
Put16(buf[10:], 0)
copy(buf[12:], []byte(authName))
copy(buf[12+Pad(len(authName)):], authData)
if _, err = c.conn.Write(buf); err != nil {
return err
}
head := make([]byte, 8)
if _, err = io.ReadFull(c.conn, head[0:8]); err != nil {
return err
}
code := head[0]
reasonLen := head[1]
major := Get16(head[2:])
minor := Get16(head[4:])
dataLen := Get16(head[6:])
if major != 11 || minor != 0 {
return fmt.Errorf("x protocol version mismatch: %d.%d", major, minor)
}
buf = make([]byte, int(dataLen)*4+8, int(dataLen)*4+8)
copy(buf, head)
if _, err = io.ReadFull(c.conn, buf[8:]); err != nil {
return err
}
if code == 0 {
reason := buf[8 : 8+reasonLen]
return fmt.Errorf("x protocol authentication refused: %s",
string(reason))
}
// Unfortunately, it isn't really feasible to read the setup bytes here,
// since the code to do so is in a different package.
// Users must call 'xproto.Setup(X)' to get the setup info.
c.SetupBytes = buf
// But also read stuff that we *need* to get started.
c.setupResourceIdBase = Get32(buf[12:])
c.setupResourceIdMask = Get32(buf[16:])
return nil
}
// dial initializes the actual net connection with X.
func (c *Conn) dial(display string) error {
if len(display) == 0 {
display = os.Getenv("DISPLAY")
}
display0 := display
if len(display) == 0 {
return errors.New("empty display string")
}
colonIdx := strings.LastIndex(display, ":")
if colonIdx < 0 {
return errors.New("bad display string: " + display0)
}
var protocol, socket string
if display[0] == '/' {
socket = display[0:colonIdx]
} else {
slashIdx := strings.LastIndex(display, "/")
if slashIdx >= 0 {
protocol = display[0:slashIdx]
c.host = display[slashIdx+1 : colonIdx]
} else {
c.host = display[0:colonIdx]
}
}
display = display[colonIdx+1 : len(display)]
if len(display) == 0 {
return errors.New("bad display string: " + display0)
}
var scr string
dotIdx := strings.LastIndex(display, ".")
if dotIdx < 0 {
c.display = display[0:]
} else {
c.display = display[0:dotIdx]
scr = display[dotIdx+1:]
}
var err error
c.DisplayNumber, err = strconv.Atoi(c.display)
if err != nil || c.DisplayNumber < 0 {
return errors.New("bad display string: " + display0)
}
if len(scr) != 0 {
c.DefaultScreen, err = strconv.Atoi(scr)
if err != nil {
return errors.New("bad display string: " + display0)
}
}
// Connect to server
if len(socket) != 0 {
c.conn, err = net.Dial("unix", socket+":"+c.display)
} else if len(c.host) != 0 {
if protocol == "" {
protocol = "tcp"
}
c.conn, err = net.Dial(protocol,
c.host+":"+strconv.Itoa(6000+c.DisplayNumber))
} else {
c.conn, err = net.Dial("unix", "/tmp/.X11-unix/X"+c.display)
}
if err != nil {
return errors.New("cannot connect to " + display0 + ": " + err.Error())
}
return nil
}

165
vendor/github.com/BurntSushi/xgb/cookie.go generated vendored Normal file
View File

@ -0,0 +1,165 @@
package xgb
import (
"errors"
)
// Cookie is the internal representation of a cookie, where one is generated
// for *every* request sent by XGB.
// 'cookie' is most frequently used by embedding it into a more specific
// kind of cookie, i.e., 'GetInputFocusCookie'.
type Cookie struct {
conn *Conn
Sequence uint16
replyChan chan []byte
errorChan chan error
pingChan chan bool
}
// NewCookie creates a new cookie with the correct channels initialized
// depending upon the values of 'checked' and 'reply'. Together, there are
// four different kinds of cookies. (See more detailed comments in the
// function for more info on those.)
// Note that a sequence number is not set until just before the request
// corresponding to this cookie is sent over the wire.
//
// Unless you're building requests from bytes by hand, this method should
// not be used.
func (c *Conn) NewCookie(checked, reply bool) *Cookie {
cookie := &Cookie{
conn: c,
Sequence: 0, // we add the sequence id just before sending a request
replyChan: nil,
errorChan: nil,
pingChan: nil,
}
// There are four different kinds of cookies:
// Checked requests with replies get a reply channel and an error channel.
// Unchecked requests with replies get a reply channel and a ping channel.
// Checked requests w/o replies get a ping channel and an error channel.
// Unchecked requests w/o replies get no channels.
// The reply channel is used to send reply data.
// The error channel is used to send error data.
// The ping channel is used when one of the 'reply' or 'error' channels
// is missing but the other is present. The ping channel is way to force
// the blocking to stop and basically say "the error has been received
// in the main event loop" (when the ping channel is coupled with a reply
// channel) or "the request you made that has no reply was successful"
// (when the ping channel is coupled with an error channel).
if checked {
cookie.errorChan = make(chan error, 1)
if !reply {
cookie.pingChan = make(chan bool, 1)
}
}
if reply {
cookie.replyChan = make(chan []byte, 1)
if !checked {
cookie.pingChan = make(chan bool, 1)
}
}
return cookie
}
// Reply detects whether this is a checked or unchecked cookie, and calls
// 'replyChecked' or 'replyUnchecked' appropriately.
//
// Unless you're building requests from bytes by hand, this method should
// not be used.
func (c Cookie) Reply() ([]byte, error) {
// checked
if c.errorChan != nil {
return c.replyChecked()
}
return c.replyUnchecked()
}
// replyChecked waits for a response on either the replyChan or errorChan
// channels. If the former arrives, the bytes are returned with a nil error.
// If the latter arrives, no bytes are returned (nil) and the error received
// is returned.
//
// Unless you're building requests from bytes by hand, this method should
// not be used.
func (c Cookie) replyChecked() ([]byte, error) {
if c.replyChan == nil {
return nil, errors.New("Cannot call 'replyChecked' on a cookie that " +
"is not expecting a *reply* or an error.")
}
if c.errorChan == nil {
return nil, errors.New("Cannot call 'replyChecked' on a cookie that " +
"is not expecting a reply or an *error*.")
}
select {
case reply := <-c.replyChan:
return reply, nil
case err := <-c.errorChan:
return nil, err
}
}
// replyUnchecked waits for a response on either the replyChan or pingChan
// channels. If the former arrives, the bytes are returned with a nil error.
// If the latter arrives, no bytes are returned (nil) and a nil error
// is returned. (In the latter case, the corresponding error can be retrieved
// from (Wait|Poll)ForEvent asynchronously.)
// In all honesty, you *probably* don't want to use this method.
//
// Unless you're building requests from bytes by hand, this method should
// not be used.
func (c Cookie) replyUnchecked() ([]byte, error) {
if c.replyChan == nil {
return nil, errors.New("Cannot call 'replyUnchecked' on a cookie " +
"that is not expecting a *reply*.")
}
select {
case reply := <-c.replyChan:
return reply, nil
case <-c.pingChan:
return nil, nil
}
}
// Check is used for checked requests that have no replies. It is a mechanism
// by which to report "success" or "error" in a synchronous fashion. (Therefore,
// unchecked requests without replies cannot use this method.)
// If the request causes an error, it is sent to this cookie's errorChan.
// If the request was successful, there is no response from the server.
// Thus, pingChan is sent a value when the *next* reply is read.
// If no more replies are being processed, we force a round trip request with
// GetInputFocus.
//
// Unless you're building requests from bytes by hand, this method should
// not be used.
func (c Cookie) Check() error {
if c.replyChan != nil {
return errors.New("Cannot call 'Check' on a cookie that is " +
"expecting a *reply*. Use 'Reply' instead.")
}
if c.errorChan == nil {
return errors.New("Cannot call 'Check' on a cookie that is " +
"not expecting a possible *error*.")
}
// First do a quick non-blocking check to see if we've been pinged.
select {
case err := <-c.errorChan:
return err
case <-c.pingChan:
return nil
default:
}
// Now force a round trip and try again, but block this time.
c.conn.Sync()
select {
case err := <-c.errorChan:
return err
case <-c.pingChan:
return nil
}
}

146
vendor/github.com/BurntSushi/xgb/doc.go generated vendored Normal file
View File

@ -0,0 +1,146 @@
/*
Package XGB provides the X Go Binding, which is a low-level API to communicate
with the core X protocol and many of the X extensions.
It is *very* closely modeled on XCB, so that experience with XCB (or xpyb) is
easily translatable to XGB. That is, it uses the same cookie/reply model
and is thread safe. There are otherwise no major differences (in the API).
Most uses of XGB typically fall under the realm of window manager and GUI kit
development, but other applications (like pagers, panels, tilers, etc.) may
also require XGB. Moreover, it is a near certainty that if you need to work
with X, xgbutil will be of great use to you as well:
https://github.com/BurntSushi/xgbutil
Example
This is an extremely terse example that demonstrates how to connect to X,
create a window, listen to StructureNotify events and Key{Press,Release}
events, map the window, and print out all events received. An example with
accompanying documentation can be found in examples/create-window.
package main
import (
"fmt"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
)
func main() {
X, err := xgb.NewConn()
if err != nil {
fmt.Println(err)
return
}
wid, _ := xproto.NewWindowId(X)
screen := xproto.Setup(X).DefaultScreen(X)
xproto.CreateWindow(X, screen.RootDepth, wid, screen.Root,
0, 0, 500, 500, 0,
xproto.WindowClassInputOutput, screen.RootVisual,
xproto.CwBackPixel | xproto.CwEventMask,
[]uint32{ // values must be in the order defined by the protocol
0xffffffff,
xproto.EventMaskStructureNotify |
xproto.EventMaskKeyPress |
xproto.EventMaskKeyRelease})
xproto.MapWindow(X, wid)
for {
ev, xerr := X.WaitForEvent()
if ev == nil && xerr == nil {
fmt.Println("Both event and error are nil. Exiting...")
return
}
if ev != nil {
fmt.Printf("Event: %s\n", ev)
}
if xerr != nil {
fmt.Printf("Error: %s\n", xerr)
}
}
}
Xinerama Example
This is another small example that shows how to query Xinerama for geometry
information of each active head. Accompanying documentation for this example
can be found in examples/xinerama.
package main
import (
"fmt"
"log"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xinerama"
)
func main() {
X, err := xgb.NewConn()
if err != nil {
log.Fatal(err)
}
// Initialize the Xinerama extension.
// The appropriate 'Init' function must be run for *every*
// extension before any of its requests can be used.
err = xinerama.Init(X)
if err != nil {
log.Fatal(err)
}
reply, err := xinerama.QueryScreens(X).Reply()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Number of heads: %d\n", reply.Number)
for i, screen := range reply.ScreenInfo {
fmt.Printf("%d :: X: %d, Y: %d, Width: %d, Height: %d\n",
i, screen.XOrg, screen.YOrg, screen.Width, screen.Height)
}
}
Parallelism
XGB can benefit greatly from parallelism due to its concurrent design. For
evidence of this claim, please see the benchmarks in xproto/xproto_test.go.
Tests
xproto/xproto_test.go contains a number of contrived tests that stress
particular corners of XGB that I presume could be problem areas. Namely:
requests with no replies, requests with replies, checked errors, unchecked
errors, sequence number wrapping, cookie buffer flushing (i.e., forcing a round
trip every N requests made that don't have a reply), getting/setting properties
and creating a window and listening to StructureNotify events.
Code Generator
Both XCB and xpyb use the same Python module (xcbgen) for a code generator. XGB
(before this fork) used the same code generator as well, but in my attempt to
add support for more extensions, I found the code generator extremely difficult
to work with. Therefore, I re-wrote the code generator in Go. It can be found
in its own sub-package, xgbgen, of xgb. My design of xgbgen includes a rough
consideration that it could be used for other languages.
What works
I am reasonably confident that the core X protocol is in full working form. I've
also tested the Xinerama and RandR extensions sparingly. Many of the other
existing extensions have Go source generated (and are compilable) and are
included in this package, but I am currently unsure of their status. They
*should* work.
What does not work
XKB is the only extension that intentionally does not work, although I suspect
that GLX also does not work (however, there is Go source code for GLX that
compiles, unlike XKB). I don't currently have any intention of getting XKB
working, due to its complexity and my current mental incapacity to test it.
*/
package xgb

105
vendor/github.com/BurntSushi/xgb/help.go generated vendored Normal file
View File

@ -0,0 +1,105 @@
package xgb
/*
help.go is meant to contain a rough hodge podge of functions that are mainly
used in the auto generated code. Indeed, several functions here are simple
wrappers so that the sub-packages don't need to be smart about which stdlib
packages to import.
Also, the 'Get..' and 'Put..' functions are used through the core xgb package
too. (xgbutil uses them too.)
*/
import (
"fmt"
"strings"
)
// StringsJoin is an alias to strings.Join. It allows us to avoid having to
// import 'strings' in each of the generated Go files.
func StringsJoin(ss []string, sep string) string {
return strings.Join(ss, sep)
}
// Sprintf is so we don't need to import 'fmt' in the generated Go files.
func Sprintf(format string, v ...interface{}) string {
return fmt.Sprintf(format, v...)
}
// Errorf is just a wrapper for fmt.Errorf. Exists for the same reason
// that 'stringsJoin' and 'sprintf' exists.
func Errorf(format string, v ...interface{}) error {
return fmt.Errorf(format, v...)
}
// Pad a length to align on 4 bytes.
func Pad(n int) int {
return (n + 3) & ^3
}
// PopCount counts the number of bits set in a value list mask.
func PopCount(mask0 int) int {
mask := uint32(mask0)
n := 0
for i := uint32(0); i < 32; i++ {
if mask&(1<<i) != 0 {
n++
}
}
return n
}
// Put16 takes a 16 bit integer and copies it into a byte slice.
func Put16(buf []byte, v uint16) {
buf[0] = byte(v)
buf[1] = byte(v >> 8)
}
// Put32 takes a 32 bit integer and copies it into a byte slice.
func Put32(buf []byte, v uint32) {
buf[0] = byte(v)
buf[1] = byte(v >> 8)
buf[2] = byte(v >> 16)
buf[3] = byte(v >> 24)
}
// Put64 takes a 64 bit integer and copies it into a byte slice.
func Put64(buf []byte, v uint64) {
buf[0] = byte(v)
buf[1] = byte(v >> 8)
buf[2] = byte(v >> 16)
buf[3] = byte(v >> 24)
buf[4] = byte(v >> 32)
buf[5] = byte(v >> 40)
buf[6] = byte(v >> 48)
buf[7] = byte(v >> 56)
}
// Get16 constructs a 16 bit integer from the beginning of a byte slice.
func Get16(buf []byte) uint16 {
v := uint16(buf[0])
v |= uint16(buf[1]) << 8
return v
}
// Get32 constructs a 32 bit integer from the beginning of a byte slice.
func Get32(buf []byte) uint32 {
v := uint32(buf[0])
v |= uint32(buf[1]) << 8
v |= uint32(buf[2]) << 16
v |= uint32(buf[3]) << 24
return v
}
// Get64 constructs a 64 bit integer from the beginning of a byte slice.
func Get64(buf []byte) uint64 {
v := uint64(buf[0])
v |= uint64(buf[1]) << 8
v |= uint64(buf[2]) << 16
v |= uint64(buf[3]) << 24
v |= uint64(buf[4]) << 32
v |= uint64(buf[5]) << 40
v |= uint64(buf[6]) << 48
v |= uint64(buf[7]) << 56
return v
}

29
vendor/github.com/BurntSushi/xgb/sync.go generated vendored Normal file
View File

@ -0,0 +1,29 @@
package xgb
// Sync sends a round trip request and waits for the response.
// This forces all pending cookies to be dealt with.
// You actually shouldn't need to use this like you might with Xlib. Namely,
// buffers are automatically flushed using Go's channels and round trip requests
// are forced where appropriate automatically.
func (c *Conn) Sync() {
cookie := c.NewCookie(true, true)
c.NewRequest(c.getInputFocusRequest(), cookie)
cookie.Reply() // wait for the buffer to clear
}
// getInputFocusRequest writes the raw bytes to a buffer.
// It is duplicated from xproto/xproto.go.
func (c *Conn) getInputFocusRequest() []byte {
size := 4
b := 0
buf := make([]byte, size)
buf[b] = 43 // request opcode
b += 1
b += 1 // padding
Put16(buf[b:], uint16(size/4)) // write request size in 4-byte units
b += 2
return buf
}

554
vendor/github.com/BurntSushi/xgb/xgb.go generated vendored Normal file
View File

@ -0,0 +1,554 @@
package xgb
import (
"errors"
"io"
"log"
"net"
"os"
"sync"
)
var (
// Where to log error-messages. Defaults to stderr.
// To disable logging, just set this to log.New(ioutil.Discard, "", 0)
Logger = log.New(os.Stderr, "XGB: ", log.Lshortfile)
)
const (
// cookieBuffer represents the queue size of cookies existing at any
// point in time. The size of the buffer is really only important when
// there are many requests without replies made in sequence. Once the
// buffer fills, a round trip request is made to clear the buffer.
cookieBuffer = 1000
// xidBuffer represents the queue size of the xid channel.
// I don't think this value matters much, since xid generation is not
// that expensive.
xidBuffer = 5
// seqBuffer represents the queue size of the sequence number channel.
// I don't think this value matters much, since sequence number generation
// is not that expensive.
seqBuffer = 5
// reqBuffer represents the queue size of the number of requests that
// can be made until new ones block. This value seems OK.
reqBuffer = 100
// eventBuffer represents the queue size of the number of events or errors
// that can be loaded off the wire and not grabbed with WaitForEvent
// until reading an event blocks. This value should be big enough to handle
// bursts of events.
eventBuffer = 5000
)
// A Conn represents a connection to an X server.
type Conn struct {
host string
conn net.Conn
display string
DisplayNumber int
DefaultScreen int
SetupBytes []byte
setupResourceIdBase uint32
setupResourceIdMask uint32
eventChan chan eventOrError
cookieChan chan *Cookie
xidChan chan xid
seqChan chan uint16
reqChan chan *request
closing chan chan struct{}
// ExtLock is a lock used whenever new extensions are initialized.
// It should not be used. It is exported for use in the extension
// sub-packages.
ExtLock sync.RWMutex
// Extensions is a map from extension name to major opcode. It should
// not be used. It is exported for use in the extension sub-packages.
Extensions map[string]byte
}
// NewConn creates a new connection instance. It initializes locks, data
// structures, and performs the initial handshake. (The code for the handshake
// has been relegated to conn.go.)
func NewConn() (*Conn, error) {
return NewConnDisplay("")
}
// NewConnDisplay is just like NewConn, but allows a specific DISPLAY
// string to be used.
// If 'display' is empty it will be taken from os.Getenv("DISPLAY").
//
// Examples:
// NewConn(":1") -> net.Dial("unix", "", "/tmp/.X11-unix/X1")
// NewConn("/tmp/launch-12/:0") -> net.Dial("unix", "", "/tmp/launch-12/:0")
// NewConn("hostname:2.1") -> net.Dial("tcp", "", "hostname:6002")
// NewConn("tcp/hostname:1.0") -> net.Dial("tcp", "", "hostname:6001")
func NewConnDisplay(display string) (*Conn, error) {
conn := &Conn{}
// First connect. This reads authority, checks DISPLAY environment
// variable, and loads the initial Setup info.
err := conn.connect(display)
if err != nil {
return nil, err
}
return postNewConn(conn)
}
// NewConnDisplay is just like NewConn, but allows a specific net.Conn
// to be used.
func NewConnNet(netConn net.Conn) (*Conn, error) {
conn := &Conn{}
// First connect. This reads authority, checks DISPLAY environment
// variable, and loads the initial Setup info.
err := conn.connectNet(netConn)
if err != nil {
return nil, err
}
return postNewConn(conn)
}
func postNewConn(conn *Conn) (*Conn, error) {
conn.Extensions = make(map[string]byte)
conn.cookieChan = make(chan *Cookie, cookieBuffer)
conn.xidChan = make(chan xid, xidBuffer)
conn.seqChan = make(chan uint16, seqBuffer)
conn.reqChan = make(chan *request, reqBuffer)
conn.eventChan = make(chan eventOrError, eventBuffer)
conn.closing = make(chan chan struct{}, 1)
go conn.generateXIds()
go conn.generateSeqIds()
go conn.sendRequests()
go conn.readResponses()
return conn, nil
}
// Close gracefully closes the connection to the X server.
func (c *Conn) Close() {
close(c.reqChan)
}
// Event is an interface that can contain any of the events returned by the
// server. Use a type assertion switch to extract the Event structs.
type Event interface {
Bytes() []byte
String() string
}
// NewEventFun is the type of function use to construct events from raw bytes.
// It should not be used. It is exported for use in the extension sub-packages.
type NewEventFun func(buf []byte) Event
// NewEventFuncs is a map from event numbers to functions that create
// the corresponding event. It should not be used. It is exported for use
// in the extension sub-packages.
var NewEventFuncs = make(map[int]NewEventFun)
// NewExtEventFuncs is a temporary map that stores event constructor functions
// for each extension. When an extension is initialized, each event for that
// extension is added to the 'NewEventFuncs' map. It should not be used. It is
// exported for use in the extension sub-packages.
var NewExtEventFuncs = make(map[string]map[int]NewEventFun)
// Error is an interface that can contain any of the errors returned by
// the server. Use a type assertion switch to extract the Error structs.
type Error interface {
SequenceId() uint16
BadId() uint32
Error() string
}
// NewErrorFun is the type of function use to construct errors from raw bytes.
// It should not be used. It is exported for use in the extension sub-packages.
type NewErrorFun func(buf []byte) Error
// NewErrorFuncs is a map from error numbers to functions that create
// the corresponding error. It should not be used. It is exported for use in
// the extension sub-packages.
var NewErrorFuncs = make(map[int]NewErrorFun)
// NewExtErrorFuncs is a temporary map that stores error constructor functions
// for each extension. When an extension is initialized, each error for that
// extension is added to the 'NewErrorFuncs' map. It should not be used. It is
// exported for use in the extension sub-packages.
var NewExtErrorFuncs = make(map[string]map[int]NewErrorFun)
// eventOrError corresponds to values that can be either an event or an
// error.
type eventOrError interface{}
// NewId generates a new unused ID for use with requests like CreateWindow.
// If no new ids can be generated, the id returned is 0 and error is non-nil.
// This shouldn't be used directly, and is exported for use in the extension
// sub-packages.
// If you need identifiers, use the appropriate constructor.
// e.g., For a window id, use xproto.NewWindowId. For
// a new pixmap id, use xproto.NewPixmapId. And so on.
func (c *Conn) NewId() (uint32, error) {
xid := <-c.xidChan
if xid.err != nil {
return 0, xid.err
}
return xid.id, nil
}
// xid encapsulates a resource identifier being sent over the Conn.xidChan
// channel. If no new resource id can be generated, id is set to 0 and a
// non-nil error is set in xid.err.
type xid struct {
id uint32
err error
}
// generateXids sends new Ids down the channel for NewId to use.
// generateXids should be run in its own goroutine.
// This needs to be updated to use the XC Misc extension once we run out of
// new ids.
// Thanks to libxcb/src/xcb_xid.c. This code is greatly inspired by it.
func (conn *Conn) generateXIds() {
defer close(conn.xidChan)
// This requires some explanation. From the horse's mouth:
// "The resource-id-mask contains a single contiguous set of bits (at least
// 18). The client allocates resource IDs for types WINDOW, PIXMAP,
// CURSOR, FONT, GCONTEXT, and COLORMAP by choosing a value with only some
// subset of these bits set and ORing it with resource-id-base. Only values
// constructed in this way can be used to name newly created resources over
// this connection."
// So for example (using 8 bit integers), the mask might look like:
// 00111000
// So that valid values would be 00101000, 00110000, 00001000, and so on.
// Thus, the idea is to increment it by the place of the last least
// significant '1'. In this case, that value would be 00001000. To get
// that value, we can AND the original mask with its two's complement:
// 00111000 & 11001000 = 00001000.
// And we use that value to increment the last resource id to get a new one.
// (And then, of course, we OR it with resource-id-base.)
inc := conn.setupResourceIdMask & -conn.setupResourceIdMask
max := conn.setupResourceIdMask
last := uint32(0)
for {
// TODO: Use the XC Misc extension to look for released ids.
if last > 0 && last >= max-inc+1 {
conn.xidChan <- xid{
id: 0,
err: errors.New("There are no more available resource" +
"identifiers."),
}
}
last += inc
conn.xidChan <- xid{
id: last | conn.setupResourceIdBase,
err: nil,
}
}
}
// newSeqId fetches the next sequence id from the Conn.seqChan channel.
func (c *Conn) newSequenceId() uint16 {
return <-c.seqChan
}
// generateSeqIds returns new sequence ids. It is meant to be run in its
// own goroutine.
// A sequence id is generated for *every* request. It's the identifier used
// to match up replies with requests.
// Since sequence ids can only be 16 bit integers we start over at zero when it
// comes time to wrap.
// N.B. As long as the cookie buffer is less than 2^16, there are no limitations
// on the number (or kind) of requests made in sequence.
func (c *Conn) generateSeqIds() {
defer close(c.seqChan)
seqid := uint16(1)
for {
c.seqChan <- seqid
if seqid == uint16((1<<16)-1) {
seqid = 0
} else {
seqid++
}
}
}
// request encapsulates a buffer of raw bytes (containing the request data)
// and a cookie, which when combined represents a single request.
// The cookie is used to match up the reply/error.
type request struct {
buf []byte
cookie *Cookie
// seq is closed when the request (cookie) has been sequenced by the Conn.
seq chan struct{}
}
// NewRequest takes the bytes and a cookie of a particular request, constructs
// a request type, and sends it over the Conn.reqChan channel.
// Note that the sequence number is added to the cookie after it is sent
// over the request channel, but before it is sent to X.
//
// Note that you may safely use NewRequest to send arbitrary byte requests
// to X. The resulting cookie can be used just like any normal cookie and
// abides by the same rules, except that for replies, you'll get back the
// raw byte data. This may be useful for performance critical sections where
// every allocation counts, since all X requests in XGB allocate a new byte
// slice. In contrast, NewRequest allocates one small request struct and
// nothing else. (Except when the cookie buffer is full and has to be flushed.)
//
// If you're using NewRequest manually, you'll need to use NewCookie to create
// a new cookie.
//
// In all likelihood, you should be able to copy and paste with some minor
// edits the generated code for the request you want to issue.
func (c *Conn) NewRequest(buf []byte, cookie *Cookie) {
seq := make(chan struct{})
c.reqChan <- &request{buf: buf, cookie: cookie, seq: seq}
<-seq
}
// sendRequests is run as a single goroutine that takes requests and writes
// the bytes to the wire and adds the cookie to the cookie queue.
// It is meant to be run as its own goroutine.
func (c *Conn) sendRequests() {
defer close(c.cookieChan)
for req := range c.reqChan {
// ho there! if the cookie channel is nearly full, force a round
// trip to clear out the cookie buffer.
// Note that we circumvent the request channel, because we're *in*
// the request channel.
if len(c.cookieChan) == cookieBuffer-1 {
if err := c.noop(); err != nil {
// Shut everything down.
break
}
}
req.cookie.Sequence = c.newSequenceId()
c.cookieChan <- req.cookie
c.writeBuffer(req.buf)
close(req.seq)
}
response := make(chan struct{})
c.closing <- response
c.noop() // Flush the response reading goroutine, ignore error.
<-response
c.conn.Close()
}
// noop circumvents the usual request sending goroutines and forces a round
// trip request manually.
func (c *Conn) noop() error {
cookie := c.NewCookie(true, true)
cookie.Sequence = c.newSequenceId()
c.cookieChan <- cookie
if err := c.writeBuffer(c.getInputFocusRequest()); err != nil {
return err
}
cookie.Reply() // wait for the buffer to clear
return nil
}
// writeBuffer is a convenience function for writing a byte slice to the wire.
func (c *Conn) writeBuffer(buf []byte) error {
if _, err := c.conn.Write(buf); err != nil {
Logger.Printf("A write error is unrecoverable: %s", err)
return err
} else {
return nil
}
}
// readResponses is a goroutine that reads events, errors and
// replies off the wire.
// When an event is read, it is always added to the event channel.
// When an error is read, if it corresponds to an existing checked cookie,
// it is sent to that cookie's error channel. Otherwise it is added to the
// event channel.
// When a reply is read, it is added to the corresponding cookie's reply
// channel. (It is an error if no such cookie exists in this case.)
// Finally, cookies that came "before" this reply are always cleaned up.
func (c *Conn) readResponses() {
defer close(c.eventChan)
var (
err Error
seq uint16
replyBytes []byte
)
for {
select {
case respond := <-c.closing:
respond <- struct{}{}
return
default:
}
buf := make([]byte, 32)
err, seq = nil, 0
if _, err := io.ReadFull(c.conn, buf); err != nil {
Logger.Printf("A read error is unrecoverable: %s", err)
c.eventChan <- err
c.Close()
continue
}
switch buf[0] {
case 0: // This is an error
// Use the constructor function for this error (that is auto
// generated) by looking it up by the error number.
newErrFun, ok := NewErrorFuncs[int(buf[1])]
if !ok {
Logger.Printf("BUG: Could not find error constructor function "+
"for error with number %d.", buf[1])
continue
}
err = newErrFun(buf)
seq = err.SequenceId()
// This error is either sent to the event channel or a specific
// cookie's error channel below.
case 1: // This is a reply
seq = Get16(buf[2:])
// check to see if this reply has more bytes to be read
size := Get32(buf[4:])
if size > 0 {
byteCount := 32 + size*4
biggerBuf := make([]byte, byteCount)
copy(biggerBuf[:32], buf)
if _, err := io.ReadFull(c.conn, biggerBuf[32:]); err != nil {
Logger.Printf("A read error is unrecoverable: %s", err)
c.eventChan <- err
c.Close()
continue
}
replyBytes = biggerBuf
} else {
replyBytes = buf
}
// This reply is sent to its corresponding cookie below.
default: // This is an event
// Use the constructor function for this event (like for errors,
// and is also auto generated) by looking it up by the event number.
// Note that we AND the event number with 127 so that we ignore
// the most significant bit (which is set when it was sent from
// a SendEvent request).
evNum := int(buf[0] & 127)
newEventFun, ok := NewEventFuncs[evNum]
if !ok {
Logger.Printf("BUG: Could not find event construct function "+
"for event with number %d.", evNum)
continue
}
c.eventChan <- newEventFun(buf)
continue
}
// At this point, we have a sequence number and we're either
// processing an error or a reply, which are both responses to
// requests. So all we have to do is find the cookie corresponding
// to this error/reply, and send the appropriate data to it.
// In doing so, we make sure that any cookies that came before it
// are marked as successful if they are void and checked.
// If there's a cookie that requires a reply that is before this
// reply, then something is wrong.
for cookie := range c.cookieChan {
// This is the cookie we're looking for. Process and break.
if cookie.Sequence == seq {
if err != nil { // this is an error to a request
// synchronous processing
if cookie.errorChan != nil {
cookie.errorChan <- err
} else { // asynchronous processing
c.eventChan <- err
// if this is an unchecked reply, ping the cookie too
if cookie.pingChan != nil {
cookie.pingChan <- true
}
}
} else { // this is a reply
if cookie.replyChan == nil {
Logger.Printf("Reply with sequence id %d does not "+
"have a cookie with a valid reply channel.", seq)
continue
} else {
cookie.replyChan <- replyBytes
}
}
break
}
switch {
// Checked requests with replies
case cookie.replyChan != nil && cookie.errorChan != nil:
Logger.Printf("Found cookie with sequence id %d that is "+
"expecting a reply but will never get it. Currently "+
"on sequence number %d", cookie.Sequence, seq)
// Unchecked requests with replies
case cookie.replyChan != nil && cookie.pingChan != nil:
Logger.Printf("Found cookie with sequence id %d that is "+
"expecting a reply (and not an error) but will never "+
"get it. Currently on sequence number %d",
cookie.Sequence, seq)
// Checked requests without replies
case cookie.pingChan != nil && cookie.errorChan != nil:
cookie.pingChan <- true
// Unchecked requests without replies don't have any channels,
// so we can't do anything with them except let them pass by.
}
}
}
}
// processEventOrError takes an eventOrError, type switches on it,
// and returns it in Go idiomatic style.
func processEventOrError(everr eventOrError) (Event, Error) {
switch ee := everr.(type) {
case Event:
return ee, nil
case Error:
return nil, ee
default:
Logger.Printf("Invalid event/error type: %T", everr)
return nil, nil
}
}
// WaitForEvent returns the next event from the server.
// It will block until an event is available.
// WaitForEvent returns either an Event or an Error. (Returning both
// is a bug.) Note than an Error here is an X error and not an XGB error. That
// is, X errors are sometimes completely expected (and you may want to ignore
// them in some cases).
//
// If both the event and error are nil, then the connection has been closed.
func (c *Conn) WaitForEvent() (Event, Error) {
return processEventOrError(<-c.eventChan)
}
// PollForEvent returns the next event from the server if one is available in
// the internal queue without blocking. Note that unlike WaitForEvent, both
// Event and Error could be nil. Indeed, they are both nil when the event queue
// is empty.
func (c *Conn) PollForEvent() (Event, Error) {
select {
case everr := <-c.eventChan:
return processEventOrError(everr)
default:
return nil, nil
}
}

14910
vendor/github.com/BurntSushi/xgb/xproto/xproto.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

4
vendor/github.com/creack/pty/.gitignore generated vendored Normal file
View File

@ -0,0 +1,4 @@
[568].out
_go*
_test*
_obj

17
vendor/github.com/creack/pty/Dockerfile.golang generated vendored Normal file
View File

@ -0,0 +1,17 @@
ARG GOVERSION=1.14
FROM golang:${GOVERSION}
# Set base env.
ARG GOOS=linux
ARG GOARCH=amd64
ENV GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 GOFLAGS='-v -ldflags=-s -ldflags=-w'
# Pre compile the stdlib for 386/arm (32bits).
RUN go build -a std
# Add the code to the image.
WORKDIR pty
ADD . .
# Build the lib.
RUN go build

23
vendor/github.com/creack/pty/Dockerfile.riscv generated vendored Normal file
View File

@ -0,0 +1,23 @@
# NOTE: Using 1.13 as a base to build the RISCV compiler, the resulting version is based on go1.6.
FROM golang:1.13
# Clone and complie a riscv compatible version of the go compiler.
RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go
# riscvdev branch HEAD as of 2019-06-29.
RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf
ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH
RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash
ENV GOROOT=/riscv-go
# Set the base env.
ENV GOOS=linux GOARCH=riscv CGO_ENABLED=0 GOFLAGS='-v -ldflags=-s -ldflags=-w'
# Pre compile the stdlib.
RUN go build -a std
# Add the code to the image.
WORKDIR pty
ADD . .
# Build the lib.
RUN go build

23
vendor/github.com/creack/pty/LICENSE generated vendored Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2011 Keith Rarick
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

107
vendor/github.com/creack/pty/README.md generated vendored Normal file
View File

@ -0,0 +1,107 @@
# pty
Pty is a Go package for using unix pseudo-terminals.
## Install
```sh
go get github.com/creack/pty
```
## Examples
Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment.
### Command
```go
package main
import (
"io"
"os"
"os/exec"
"github.com/creack/pty"
)
func main() {
c := exec.Command("grep", "--color=auto", "bar")
f, err := pty.Start(c)
if err != nil {
panic(err)
}
go func() {
f.Write([]byte("foo\n"))
f.Write([]byte("bar\n"))
f.Write([]byte("baz\n"))
f.Write([]byte{4}) // EOT
}()
io.Copy(os.Stdout, f)
}
```
### Shell
```go
package main
import (
"io"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"github.com/creack/pty"
"golang.org/x/term"
)
func test() error {
// Create arbitrary command.
c := exec.Command("bash")
// Start the command with a pty.
ptmx, err := pty.Start(c)
if err != nil {
return err
}
// Make sure to close the pty at the end.
defer func() { _ = ptmx.Close() }() // Best effort.
// Handle pty size.
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGWINCH)
go func() {
for range ch {
if err := pty.InheritSize(os.Stdin, ptmx); err != nil {
log.Printf("error resizing pty: %s", err)
}
}
}()
ch <- syscall.SIGWINCH // Initial resize.
defer func() { signal.Stop(ch); close(ch) }() // Cleanup signals when done.
// Set stdin in raw mode.
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort.
// Copy stdin to the pty and the pty to stdout.
// NOTE: The goroutine will keep reading until the next keystroke before returning.
go func() { _, _ = io.Copy(ptmx, os.Stdin) }()
_, _ = io.Copy(os.Stdout, ptmx)
return nil
}
func main() {
if err := test(); err != nil {
log.Fatal(err)
}
}
```

16
vendor/github.com/creack/pty/doc.go generated vendored Normal file
View File

@ -0,0 +1,16 @@
// Package pty provides functions for working with Unix terminals.
package pty
import (
"errors"
"os"
)
// ErrUnsupported is returned if a function is not
// available on the current platform.
var ErrUnsupported = errors.New("unsupported")
// Open a pty and its corresponding tty.
func Open() (pty, tty *os.File, err error) {
return open()
}

4
vendor/github.com/creack/pty/go.mod generated vendored Normal file
View File

@ -0,0 +1,4 @@
module github.com/creack/pty
go 1.13

13
vendor/github.com/creack/pty/ioctl.go generated vendored Normal file
View File

@ -0,0 +1,13 @@
// +build !windows,!solaris
package pty
import "syscall"
func ioctl(fd, cmd, ptr uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr)
if e != 0 {
return e
}
return nil
}

39
vendor/github.com/creack/pty/ioctl_bsd.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
// +build darwin dragonfly freebsd netbsd openbsd
package pty
// from <sys/ioccom.h>
const (
_IOC_VOID uintptr = 0x20000000
_IOC_OUT uintptr = 0x40000000
_IOC_IN uintptr = 0x80000000
_IOC_IN_OUT uintptr = _IOC_OUT | _IOC_IN
_IOC_DIRMASK = _IOC_VOID | _IOC_OUT | _IOC_IN
_IOC_PARAM_SHIFT = 13
_IOC_PARAM_MASK = (1 << _IOC_PARAM_SHIFT) - 1
)
func _IOC_PARM_LEN(ioctl uintptr) uintptr {
return (ioctl >> 16) & _IOC_PARAM_MASK
}
func _IOC(inout uintptr, group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return inout | (param_len&_IOC_PARAM_MASK)<<16 | uintptr(group)<<8 | ioctl_num
}
func _IO(group byte, ioctl_num uintptr) uintptr {
return _IOC(_IOC_VOID, group, ioctl_num, 0)
}
func _IOR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_OUT, group, ioctl_num, param_len)
}
func _IOW(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_IN, group, ioctl_num, param_len)
}
func _IOWR(group byte, ioctl_num uintptr, param_len uintptr) uintptr {
return _IOC(_IOC_IN_OUT, group, ioctl_num, param_len)
}

31
vendor/github.com/creack/pty/ioctl_solaris.go generated vendored Normal file
View File

@ -0,0 +1,31 @@
package pty
import (
"unsafe"
"golang.org/x/sys/unix"
)
const (
// see /usr/include/sys/stropts.h
I_PUSH = uintptr((int32('S')<<8 | 002))
I_STR = uintptr((int32('S')<<8 | 010))
I_FIND = uintptr((int32('S')<<8 | 013))
// see /usr/include/sys/ptms.h
ISPTM = (int32('P') << 8) | 1
UNLKPT = (int32('P') << 8) | 2
PTSSTTY = (int32('P') << 8) | 3
ZONEPT = (int32('P') << 8) | 4
OWNERPT = (int32('P') << 8) | 5
)
type strioctl struct {
ic_cmd int32
ic_timout int32
ic_len int32
ic_dp unsafe.Pointer
}
func ioctl(fd, cmd, ptr uintptr) error {
return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr))
}

Some files were not shown because too many files have changed in this diff Show More