Initial commit
This commit is contained in:
commit
e60a0b0427
|
@ -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 }}
|
|
@ -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 ./...
|
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
/darktile
|
|
@ -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
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
},
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
//+build !windows
|
||||
|
||||
package termutil
|
||||
|
||||
var oscTerminators = []rune{0x07, 0x5c}
|
|
@ -0,0 +1,5 @@
|
|||
//+build windows
|
||||
|
||||
package termutil
|
||||
|
||||
var oscTerminators = []rune{0x07, 0x00}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package termutil
|
||||
|
||||
type MeasuredRune struct {
|
||||
Rune rune
|
||||
Width int
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package version
|
||||
|
||||
var Version = "unknown (local build)"
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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";
|
||||
}'
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
||||
|
|
@ -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" ]
|
|
@ -0,0 +1,2 @@
|
|||
xgbgen/xgbgen
|
||||
.*.swp
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
[568].out
|
||||
_go*
|
||||
_test*
|
||||
_obj
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
module github.com/creack/pty
|
||||
|
||||
go 1.13
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue