fix out of date dependencies
This commit is contained in:
parent
e06d8dd4ec
commit
2042488d20
|
@ -8,10 +8,10 @@
|
||||||
version = "v1.5.0"
|
version = "v1.5.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
name = "github.com/gizak/termui"
|
||||||
name = "github.com/gosuri/uilive"
|
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "ac356e6e42cd31fcef8e6aec13ae9ed6fe87713e"
|
revision = "24acd523c756fd9728824cdfac66aad9d8982fb7"
|
||||||
|
version = "v2.2.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/jawher/mow.cli"
|
name = "github.com/jawher/mow.cli"
|
||||||
|
@ -19,6 +19,12 @@
|
||||||
revision = "0e80ee9f63156ea1954dc2375c33a1c7e752c25c"
|
revision = "0e80ee9f63156ea1954dc2375c33a1c7e752c25c"
|
||||||
version = "v1.0.3"
|
version = "v1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/maruel/panicparse"
|
||||||
|
packages = ["stack"]
|
||||||
|
revision = "766956aceb8ff49664065ae50bef0ae8a0a83ec4"
|
||||||
|
version = "v1.0.2"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/mattn/go-colorable"
|
name = "github.com/mattn/go-colorable"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
|
@ -31,12 +37,30 @@
|
||||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
version = "v0.0.3"
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-runewidth"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||||
|
version = "v0.0.2"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/mattn/go-sqlite3"
|
name = "github.com/mattn/go-sqlite3"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
|
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
|
||||||
version = "v1.6.0"
|
version = "v1.6.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mitchellh/go-wordwrap"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "ad45545899c7b13c020ea92b2072220eefad42b8"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/nsf/termbox-go"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "88b7b944be8bc8d8ec6195fca97c5869ba20f99d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
|
@ -46,6 +70,6 @@
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "1a1f721ab61fd1976d54ba03bad816beaefd926604a77b24c318305d4082f779"
|
inputs-digest = "59a956a9d4768c1668abfcf5af67434d0f8251797d381c64b9dcf013a3dfcb34"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
|
@ -24,10 +24,6 @@
|
||||||
name = "github.com/fatih/color"
|
name = "github.com/fatih/color"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/gosuri/uilive"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/jawher/mow.cli"
|
name = "github.com/jawher/mow.cli"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
.DS_Store
|
||||||
|
/vendor
|
|
@ -0,0 +1,6 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
|
||||||
|
script: go test -v ./
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Zack Guo
|
||||||
|
|
||||||
|
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,151 @@
|
||||||
|
# termui [![Build Status](https://travis-ci.org/gizak/termui.svg?branch=master)](https://travis-ci.org/gizak/termui) [![Doc Status](https://godoc.org/github.com/gizak/termui?status.png)](https://godoc.org/github.com/gizak/termui)
|
||||||
|
|
||||||
|
<img src="./_example/dashboard.gif" alt="demo cast under osx 10.10; Terminal.app; Menlo Regular 12pt.)" width="80%">
|
||||||
|
|
||||||
|
`termui` is a cross-platform, easy-to-compile, and fully-customizable terminal dashboard. It is inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib), but purely in Go.
|
||||||
|
|
||||||
|
Now version v2 has arrived! It brings new event system, new theme system, new `Buffer` interface and specific colour text rendering. (some docs are missing, but it will be completed soon!)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`master` mirrors v2 branch, to install:
|
||||||
|
|
||||||
|
go get -u github.com/gizak/termui
|
||||||
|
|
||||||
|
It is recommanded to use locked deps by using [glide](https://glide.sh): move to `termui` src directory then run `glide up`.
|
||||||
|
|
||||||
|
For the compatible reason, you can choose to install the legacy version of `termui`:
|
||||||
|
|
||||||
|
go get gopkg.in/gizak/termui.v1
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
To use `termui`, the very first thing you may want to know is how to manage layout. `termui` offers two ways of doing this, known as absolute layout and grid layout.
|
||||||
|
|
||||||
|
__Absolute layout__
|
||||||
|
|
||||||
|
Each widget has an underlying block structure which basically is a box model. It has border, label and padding properties. A border of a widget can be chosen to hide or display (with its border label), you can pick a different front/back colour for the border as well. To display such a widget at a specific location in terminal window, you need to assign `.X`, `.Y`, `.Height`, `.Width` values for each widget before sending it to `.Render`. Let's demonstrate these by a code snippet:
|
||||||
|
|
||||||
|
`````go
|
||||||
|
import ui "github.com/gizak/termui" // <- ui shortcut, optional
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := ui.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
p := ui.NewPar(":PRESS q TO QUIT DEMO")
|
||||||
|
p.Height = 3
|
||||||
|
p.Width = 50
|
||||||
|
p.TextFgColor = ui.ColorWhite
|
||||||
|
p.BorderLabel = "Text Box"
|
||||||
|
p.BorderFg = ui.ColorCyan
|
||||||
|
|
||||||
|
g := ui.NewGauge()
|
||||||
|
g.Percent = 50
|
||||||
|
g.Width = 50
|
||||||
|
g.Height = 3
|
||||||
|
g.Y = 11
|
||||||
|
g.BorderLabel = "Gauge"
|
||||||
|
g.BarColor = ui.ColorRed
|
||||||
|
g.BorderFg = ui.ColorWhite
|
||||||
|
g.BorderLabelFg = ui.ColorCyan
|
||||||
|
|
||||||
|
ui.Render(p, g) // feel free to call Render, it's async and non-block
|
||||||
|
|
||||||
|
// event handler...
|
||||||
|
}
|
||||||
|
`````
|
||||||
|
|
||||||
|
Note that components can be overlapped (I'd rather call this a feature...), `Render(rs ...Renderer)` renders its args from left to right (i.e. each component's weight is arising from left to right).
|
||||||
|
|
||||||
|
__Grid layout:__
|
||||||
|
|
||||||
|
<img src="./_example/grid.gif" alt="grid" width="60%">
|
||||||
|
|
||||||
|
Grid layout uses [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp) with expressive syntax. To use `Grid`, all we need to do is build a widget tree consisting of `Row`s and `Col`s (Actually a `Col` is also a `Row` but with a widget endpoint attached).
|
||||||
|
|
||||||
|
```go
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
// init and create widgets...
|
||||||
|
|
||||||
|
// build
|
||||||
|
ui.Body.AddRows(
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(6, 0, widget0),
|
||||||
|
ui.NewCol(6, 0, widget1)),
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(3, 0, widget2),
|
||||||
|
ui.NewCol(3, 0, widget30, widget31, widget32),
|
||||||
|
ui.NewCol(6, 0, widget4)))
|
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
ui.Body.Align()
|
||||||
|
|
||||||
|
ui.Render(ui.Body)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
`termui` ships with a http-like event mux handling system. All events are channeled up from different sources (typing, click, windows resize, custom event) and then encoded as universal `Event` object. `Event.Path` indicates the event type and `Event.Data` stores the event data struct. Add a handler to a certain event is easy as below:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// handle key q pressing
|
||||||
|
ui.Handle("/sys/kbd/q", func(ui.Event) {
|
||||||
|
// press q to quit
|
||||||
|
ui.StopLoop()
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.Handle("/sys/kbd/C-x", func(ui.Event) {
|
||||||
|
// handle Ctrl + x combination
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.Handle("/sys/kbd", func(ui.Event) {
|
||||||
|
// handle all other key pressing
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle a 1s timer
|
||||||
|
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||||
|
t := e.Data.(ui.EvtTimer)
|
||||||
|
// t is a EvtTimer
|
||||||
|
if t.Count%2 ==0 {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.Loop() // block until StopLoop is called
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widgets
|
||||||
|
|
||||||
|
Click image to see the corresponding demo codes.
|
||||||
|
|
||||||
|
[<img src="./_example/par.png" alt="par" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/par.go)
|
||||||
|
[<img src="./_example/list.png" alt="list" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/list.go)
|
||||||
|
[<img src="./_example/gauge.png" alt="gauge" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/gauge.go)
|
||||||
|
[<img src="./_example/linechart.png" alt="linechart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/linechart.go)
|
||||||
|
[<img src="./_example/barchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/barchart.go)
|
||||||
|
[<img src="./_example/mbarchart.png" alt="barchart" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/mbarchart.go)
|
||||||
|
[<img src="./_example/sparklines.png" alt="sparklines" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/sparklines.go)
|
||||||
|
[<img src="./_example/table.png" alt="table" type="image/png" width="45%">](https://github.com/gizak/termui/blob/master/_example/table.go)
|
||||||
|
|
||||||
|
## GoDoc
|
||||||
|
|
||||||
|
[godoc](https://godoc.org/github.com/gizak/termui)
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [x] Grid layout
|
||||||
|
- [x] Event system
|
||||||
|
- [x] Canvas widget
|
||||||
|
- [x] Refine APIs
|
||||||
|
- [ ] Focusable widgets
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
## License
|
||||||
|
This library is under the [MIT License](http://opensource.org/licenses/MIT)
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// BarChart creates multiple bars in a widget:
|
||||||
|
/*
|
||||||
|
bc := termui.NewBarChart()
|
||||||
|
data := []int{3, 2, 5, 3, 9, 5}
|
||||||
|
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
|
bc.BorderLabel = "Bar Chart"
|
||||||
|
bc.Data = data
|
||||||
|
bc.Width = 26
|
||||||
|
bc.Height = 10
|
||||||
|
bc.DataLabels = bclabels
|
||||||
|
bc.TextColor = termui.ColorGreen
|
||||||
|
bc.BarColor = termui.ColorRed
|
||||||
|
bc.NumColor = termui.ColorYellow
|
||||||
|
*/
|
||||||
|
type BarChart struct {
|
||||||
|
Block
|
||||||
|
BarColor Attribute
|
||||||
|
TextColor Attribute
|
||||||
|
NumColor Attribute
|
||||||
|
Data []int
|
||||||
|
DataLabels []string
|
||||||
|
BarWidth int
|
||||||
|
BarGap int
|
||||||
|
CellChar rune
|
||||||
|
labels [][]rune
|
||||||
|
dataNum [][]rune
|
||||||
|
numBar int
|
||||||
|
scale float64
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBarChart returns a new *BarChart with current theme.
|
||||||
|
func NewBarChart() *BarChart {
|
||||||
|
bc := &BarChart{Block: *NewBlock()}
|
||||||
|
bc.BarColor = ThemeAttr("barchart.bar.bg")
|
||||||
|
bc.NumColor = ThemeAttr("barchart.num.fg")
|
||||||
|
bc.TextColor = ThemeAttr("barchart.text.fg")
|
||||||
|
bc.BarGap = 1
|
||||||
|
bc.BarWidth = 3
|
||||||
|
bc.CellChar = ' '
|
||||||
|
return bc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BarChart) layout() {
|
||||||
|
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
|
||||||
|
bc.labels = make([][]rune, bc.numBar)
|
||||||
|
bc.dataNum = make([][]rune, len(bc.Data))
|
||||||
|
|
||||||
|
for i := 0; i < bc.numBar && i < len(bc.DataLabels) && i < len(bc.Data); i++ {
|
||||||
|
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
|
||||||
|
n := bc.Data[i]
|
||||||
|
s := fmt.Sprint(n)
|
||||||
|
bc.dataNum[i] = trimStr2Runes(s, bc.BarWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
//bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range
|
||||||
|
// Asign a negative value to get maxvalue auto-populates
|
||||||
|
if bc.max == 0 {
|
||||||
|
bc.max = -1
|
||||||
|
}
|
||||||
|
for i := 0; i < len(bc.Data); i++ {
|
||||||
|
if bc.max < bc.Data[i] {
|
||||||
|
bc.max = bc.Data[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *BarChart) SetMax(max int) {
|
||||||
|
|
||||||
|
if max > 0 {
|
||||||
|
bc.max = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (bc *BarChart) Buffer() Buffer {
|
||||||
|
buf := bc.Block.Buffer()
|
||||||
|
bc.layout()
|
||||||
|
|
||||||
|
for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ {
|
||||||
|
h := int(float64(bc.Data[i]) / bc.scale)
|
||||||
|
oftX := i * (bc.BarWidth + bc.BarGap)
|
||||||
|
|
||||||
|
barBg := bc.Bg
|
||||||
|
barFg := bc.BarColor
|
||||||
|
|
||||||
|
if bc.CellChar == ' ' {
|
||||||
|
barBg = bc.BarColor
|
||||||
|
barFg = ColorDefault
|
||||||
|
if bc.BarColor == ColorDefault { // the same as above
|
||||||
|
barBg |= AttrReverse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// plot bar
|
||||||
|
for j := 0; j < bc.BarWidth; j++ {
|
||||||
|
for k := 0; k < h; k++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.CellChar,
|
||||||
|
Bg: barBg,
|
||||||
|
Fg: barFg,
|
||||||
|
}
|
||||||
|
|
||||||
|
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// plot text
|
||||||
|
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
|
||||||
|
w := charWidth(bc.labels[i][j])
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.labels[i][j],
|
||||||
|
Bg: bc.Bg,
|
||||||
|
Fg: bc.TextColor,
|
||||||
|
}
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
|
||||||
|
x := bc.innerArea.Min.X + oftX + k
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
k += w
|
||||||
|
}
|
||||||
|
// plot num
|
||||||
|
for j := 0; j < len(bc.dataNum[i]); j++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.dataNum[i][j],
|
||||||
|
Fg: bc.NumColor,
|
||||||
|
Bg: barBg,
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == 0 {
|
||||||
|
c.Bg = bc.Bg
|
||||||
|
}
|
||||||
|
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Hline is a horizontal line.
|
||||||
|
type Hline struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Len int
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vline is a vertical line.
|
||||||
|
type Vline struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Len int
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a horizontal line.
|
||||||
|
func (l Hline) Buffer() Buffer {
|
||||||
|
if l.Len <= 0 {
|
||||||
|
return NewBuffer()
|
||||||
|
}
|
||||||
|
return NewFilledBuffer(l.X, l.Y, l.X+l.Len, l.Y+1, HORIZONTAL_LINE, l.Fg, l.Bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a vertical line.
|
||||||
|
func (l Vline) Buffer() Buffer {
|
||||||
|
if l.Len <= 0 {
|
||||||
|
return NewBuffer()
|
||||||
|
}
|
||||||
|
return NewFilledBuffer(l.X, l.Y, l.X+1, l.Y+l.Len, VERTICAL_LINE, l.Fg, l.Bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer draws a box border.
|
||||||
|
func (b Block) drawBorder(buf Buffer) {
|
||||||
|
if !b.Border {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
min := b.area.Min
|
||||||
|
max := b.area.Max
|
||||||
|
|
||||||
|
x0 := min.X
|
||||||
|
y0 := min.Y
|
||||||
|
x1 := max.X - 1
|
||||||
|
y1 := max.Y - 1
|
||||||
|
|
||||||
|
// draw lines
|
||||||
|
if b.BorderTop {
|
||||||
|
buf.Merge(Hline{x0, y0, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderBottom {
|
||||||
|
buf.Merge(Hline{x0, y1, x1 - x0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderLeft {
|
||||||
|
buf.Merge(Vline{x0, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
if b.BorderRight {
|
||||||
|
buf.Merge(Vline{x1, y0, y1 - y0, b.BorderFg, b.BorderBg}.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw corners
|
||||||
|
if b.BorderTop && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 0 {
|
||||||
|
buf.Set(x0, y0, Cell{TOP_LEFT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderTop && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 0 {
|
||||||
|
buf.Set(x1, y0, Cell{TOP_RIGHT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderBottom && b.BorderLeft && b.area.Dx() > 0 && b.area.Dy() > 1 {
|
||||||
|
buf.Set(x0, y1, Cell{BOTTOM_LEFT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
if b.BorderBottom && b.BorderRight && b.area.Dx() > 1 && b.area.Dy() > 1 {
|
||||||
|
buf.Set(x1, y1, Cell{BOTTOM_RIGHT, b.BorderFg, b.BorderBg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) drawBorderLabel(buf Buffer) {
|
||||||
|
maxTxtW := b.area.Dx() - 2
|
||||||
|
tx := DTrimTxCls(DefaultTxBuilder.Build(b.BorderLabel, b.BorderLabelFg, b.BorderLabelBg), maxTxtW)
|
||||||
|
|
||||||
|
for i, w := 0, 0; i < len(tx); i++ {
|
||||||
|
buf.Set(b.area.Min.X+1+w, b.area.Min.Y, tx[i])
|
||||||
|
w += tx[i].Width()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block is a base struct for all other upper level widgets,
|
||||||
|
// consider it as css: display:block.
|
||||||
|
// Normally you do not need to create it manually.
|
||||||
|
type Block struct {
|
||||||
|
area image.Rectangle
|
||||||
|
innerArea image.Rectangle
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Border bool
|
||||||
|
BorderFg Attribute
|
||||||
|
BorderBg Attribute
|
||||||
|
BorderLeft bool
|
||||||
|
BorderRight bool
|
||||||
|
BorderTop bool
|
||||||
|
BorderBottom bool
|
||||||
|
BorderLabel string
|
||||||
|
BorderLabelFg Attribute
|
||||||
|
BorderLabelBg Attribute
|
||||||
|
Display bool
|
||||||
|
Bg Attribute
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
PaddingTop int
|
||||||
|
PaddingBottom int
|
||||||
|
PaddingLeft int
|
||||||
|
PaddingRight int
|
||||||
|
id string
|
||||||
|
Float Align
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBlock returns a *Block which inherits styles from current theme.
|
||||||
|
func NewBlock() *Block {
|
||||||
|
b := Block{}
|
||||||
|
b.Display = true
|
||||||
|
b.Border = true
|
||||||
|
b.BorderLeft = true
|
||||||
|
b.BorderRight = true
|
||||||
|
b.BorderTop = true
|
||||||
|
b.BorderBottom = true
|
||||||
|
b.BorderBg = ThemeAttr("border.bg")
|
||||||
|
b.BorderFg = ThemeAttr("border.fg")
|
||||||
|
b.BorderLabelBg = ThemeAttr("label.bg")
|
||||||
|
b.BorderLabelFg = ThemeAttr("label.fg")
|
||||||
|
b.Bg = ThemeAttr("block.bg")
|
||||||
|
b.Width = 2
|
||||||
|
b.Height = 2
|
||||||
|
b.id = GenId()
|
||||||
|
b.Float = AlignNone
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) Id() string {
|
||||||
|
return b.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align computes box model
|
||||||
|
func (b *Block) Align() {
|
||||||
|
// outer
|
||||||
|
b.area.Min.X = 0
|
||||||
|
b.area.Min.Y = 0
|
||||||
|
b.area.Max.X = b.Width
|
||||||
|
b.area.Max.Y = b.Height
|
||||||
|
|
||||||
|
// float
|
||||||
|
b.area = AlignArea(TermRect(), b.area, b.Float)
|
||||||
|
b.area = MoveArea(b.area, b.X, b.Y)
|
||||||
|
|
||||||
|
// inner
|
||||||
|
b.innerArea.Min.X = b.area.Min.X + b.PaddingLeft
|
||||||
|
b.innerArea.Min.Y = b.area.Min.Y + b.PaddingTop
|
||||||
|
b.innerArea.Max.X = b.area.Max.X - b.PaddingRight
|
||||||
|
b.innerArea.Max.Y = b.area.Max.Y - b.PaddingBottom
|
||||||
|
|
||||||
|
if b.Border {
|
||||||
|
if b.BorderLeft {
|
||||||
|
b.innerArea.Min.X++
|
||||||
|
}
|
||||||
|
if b.BorderRight {
|
||||||
|
b.innerArea.Max.X--
|
||||||
|
}
|
||||||
|
if b.BorderTop {
|
||||||
|
b.innerArea.Min.Y++
|
||||||
|
}
|
||||||
|
if b.BorderBottom {
|
||||||
|
b.innerArea.Max.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerBounds returns the internal bounds of the block after aligning and
|
||||||
|
// calculating the padding and border, if any.
|
||||||
|
func (b *Block) InnerBounds() image.Rectangle {
|
||||||
|
b.Align()
|
||||||
|
return b.innerArea
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
// Draw background and border (if any).
|
||||||
|
func (b *Block) Buffer() Buffer {
|
||||||
|
b.Align()
|
||||||
|
|
||||||
|
buf := NewBuffer()
|
||||||
|
buf.SetArea(b.area)
|
||||||
|
buf.Fill(' ', ColorDefault, b.Bg)
|
||||||
|
|
||||||
|
b.drawBorder(buf)
|
||||||
|
b.drawBorderLabel(buf)
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight implements GridBufferer.
|
||||||
|
// It returns current height of the block.
|
||||||
|
func (b Block) GetHeight() int {
|
||||||
|
return b.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetX implements GridBufferer interface, which sets block's x position.
|
||||||
|
func (b *Block) SetX(x int) {
|
||||||
|
b.X = x
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetY implements GridBufferer interface, it sets y position for block.
|
||||||
|
func (b *Block) SetY(y int) {
|
||||||
|
b.Y = y
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWidth implements GridBuffer interface, it sets block's width.
|
||||||
|
func (b *Block) SetWidth(w int) {
|
||||||
|
b.Width = w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerWidth() int {
|
||||||
|
return b.innerArea.Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerHeight() int {
|
||||||
|
return b.innerArea.Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerX() int {
|
||||||
|
return b.innerArea.Min.X
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Block) InnerY() int { return b.innerArea.Min.Y }
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const TOP_RIGHT = '┐'
|
||||||
|
const VERTICAL_LINE = '│'
|
||||||
|
const HORIZONTAL_LINE = '─'
|
||||||
|
const TOP_LEFT = '┌'
|
||||||
|
const BOTTOM_RIGHT = '┘'
|
||||||
|
const BOTTOM_LEFT = '└'
|
||||||
|
const VERTICAL_LEFT = '┤'
|
||||||
|
const VERTICAL_RIGHT = '├'
|
||||||
|
const HORIZONTAL_DOWN = '┬'
|
||||||
|
const HORIZONTAL_UP = '┴'
|
||||||
|
const QUOTA_LEFT = '«'
|
||||||
|
const QUOTA_RIGHT = '»'
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBlockFloat(t *testing.T) {
|
||||||
|
Init()
|
||||||
|
defer Close()
|
||||||
|
|
||||||
|
b := NewBlock()
|
||||||
|
b.X = 10
|
||||||
|
b.Y = 20
|
||||||
|
|
||||||
|
b.Float = AlignCenter
|
||||||
|
b.Align()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockInnerBounds(t *testing.T) {
|
||||||
|
Init()
|
||||||
|
defer Close()
|
||||||
|
|
||||||
|
b := NewBlock()
|
||||||
|
b.X = 10
|
||||||
|
b.Y = 11
|
||||||
|
b.Width = 12
|
||||||
|
b.Height = 13
|
||||||
|
|
||||||
|
assert := func(name string, x, y, w, h int) {
|
||||||
|
t.Log(name)
|
||||||
|
area := b.InnerBounds()
|
||||||
|
cx := area.Min.X
|
||||||
|
cy := area.Min.Y
|
||||||
|
cw := area.Dx()
|
||||||
|
ch := area.Dy()
|
||||||
|
|
||||||
|
if cx != x {
|
||||||
|
t.Errorf("expected x to be %d but got %d", x, cx)
|
||||||
|
}
|
||||||
|
if cy != y {
|
||||||
|
t.Errorf("expected y to be %d but got %d\n%+v", y, cy, area)
|
||||||
|
}
|
||||||
|
if cw != w {
|
||||||
|
t.Errorf("expected width to be %d but got %d", w, cw)
|
||||||
|
}
|
||||||
|
if ch != h {
|
||||||
|
t.Errorf("expected height to be %d but got %d", h, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Border = false
|
||||||
|
assert("no border, no padding", 10, 11, 12, 13)
|
||||||
|
|
||||||
|
b.Border = true
|
||||||
|
assert("border, no padding", 11, 12, 10, 11)
|
||||||
|
|
||||||
|
b.PaddingBottom = 2
|
||||||
|
assert("border, 2b padding", 11, 12, 10, 9)
|
||||||
|
|
||||||
|
b.PaddingTop = 3
|
||||||
|
assert("border, 2b 3t padding", 11, 15, 10, 6)
|
||||||
|
|
||||||
|
b.PaddingLeft = 4
|
||||||
|
assert("border, 2b 3t 4l padding", 15, 15, 6, 6)
|
||||||
|
|
||||||
|
b.PaddingRight = 5
|
||||||
|
assert("border, 2b 3t 4l 5r padding", 15, 15, 1, 6)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const TOP_RIGHT = '+'
|
||||||
|
const VERTICAL_LINE = '|'
|
||||||
|
const HORIZONTAL_LINE = '-'
|
||||||
|
const TOP_LEFT = '+'
|
||||||
|
const BOTTOM_RIGHT = '+'
|
||||||
|
const BOTTOM_LEFT = '+'
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Cell is a rune with assigned Fg and Bg
|
||||||
|
type Cell struct {
|
||||||
|
Ch rune
|
||||||
|
Fg Attribute
|
||||||
|
Bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer is a renderable rectangle cell data container.
|
||||||
|
type Buffer struct {
|
||||||
|
Area image.Rectangle // selected drawing area
|
||||||
|
CellMap map[image.Point]Cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the cell at (x,y).
|
||||||
|
func (b Buffer) At(x, y int) Cell {
|
||||||
|
return b.CellMap[image.Pt(x, y)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set assigns a char to (x,y)
|
||||||
|
func (b Buffer) Set(x, y int, c Cell) {
|
||||||
|
b.CellMap[image.Pt(x, y)] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds returns the domain for which At can return non-zero color.
|
||||||
|
func (b Buffer) Bounds() image.Rectangle {
|
||||||
|
x0, y0, x1, y1 := 0, 0, 0, 0
|
||||||
|
for p := range b.CellMap {
|
||||||
|
if p.X > x1 {
|
||||||
|
x1 = p.X
|
||||||
|
}
|
||||||
|
if p.X < x0 {
|
||||||
|
x0 = p.X
|
||||||
|
}
|
||||||
|
if p.Y > y1 {
|
||||||
|
y1 = p.Y
|
||||||
|
}
|
||||||
|
if p.Y < y0 {
|
||||||
|
y0 = p.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image.Rect(x0, y0, x1+1, y1+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetArea assigns a new rect area to Buffer b.
|
||||||
|
func (b *Buffer) SetArea(r image.Rectangle) {
|
||||||
|
b.Area.Max = r.Max
|
||||||
|
b.Area.Min = r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync sets drawing area to the buffer's bound
|
||||||
|
func (b *Buffer) Sync() {
|
||||||
|
b.SetArea(b.Bounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCell returns a new cell
|
||||||
|
func NewCell(ch rune, fg, bg Attribute) Cell {
|
||||||
|
return Cell{ch, fg, bg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges bs Buffers onto b
|
||||||
|
func (b *Buffer) Merge(bs ...Buffer) {
|
||||||
|
for _, buf := range bs {
|
||||||
|
for p, v := range buf.CellMap {
|
||||||
|
b.Set(p.X, p.Y, v)
|
||||||
|
}
|
||||||
|
b.SetArea(b.Area.Union(buf.Area))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuffer returns a new Buffer
|
||||||
|
func NewBuffer() Buffer {
|
||||||
|
return Buffer{
|
||||||
|
CellMap: make(map[image.Point]Cell),
|
||||||
|
Area: image.Rectangle{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill fills the Buffer b with ch,fg and bg.
|
||||||
|
func (b Buffer) Fill(ch rune, fg, bg Attribute) {
|
||||||
|
for x := b.Area.Min.X; x < b.Area.Max.X; x++ {
|
||||||
|
for y := b.Area.Min.Y; y < b.Area.Max.Y; y++ {
|
||||||
|
b.Set(x, y, Cell{ch, fg, bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFilledBuffer returns a new Buffer filled with ch, fb and bg.
|
||||||
|
func NewFilledBuffer(x0, y0, x1, y1 int, ch rune, fg, bg Attribute) Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
buf.Area.Min = image.Pt(x0, y0)
|
||||||
|
buf.Area.Max = image.Pt(x1, y1)
|
||||||
|
|
||||||
|
for x := buf.Area.Min.X; x < buf.Area.Max.X; x++ {
|
||||||
|
for y := buf.Area.Min.Y; y < buf.Area.Max.Y; y++ {
|
||||||
|
buf.Set(x, y, Cell{ch, fg, bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBufferUnion(t *testing.T) {
|
||||||
|
b0 := NewBuffer()
|
||||||
|
b1 := NewBuffer()
|
||||||
|
|
||||||
|
b1.Area.Max.X = 100
|
||||||
|
b1.Area.Max.Y = 100
|
||||||
|
b0.Area.Max.X = 50
|
||||||
|
b0.Merge(b1)
|
||||||
|
if b0.Area.Max.X != 100 {
|
||||||
|
t.Errorf("Buffer.Merge unions Area failed: should:%v, actual %v,%v", image.Rect(0, 0, 50, 0).Union(image.Rect(0, 0, 100, 100)), b1.Area, b0.Area)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
/*
|
||||||
|
dots:
|
||||||
|
,___,
|
||||||
|
|1 4|
|
||||||
|
|2 5|
|
||||||
|
|3 6|
|
||||||
|
|7 8|
|
||||||
|
`````
|
||||||
|
*/
|
||||||
|
|
||||||
|
var brailleBase = '\u2800'
|
||||||
|
|
||||||
|
var brailleOftMap = [4][2]rune{
|
||||||
|
{'\u0001', '\u0008'},
|
||||||
|
{'\u0002', '\u0010'},
|
||||||
|
{'\u0004', '\u0020'},
|
||||||
|
{'\u0040', '\u0080'}}
|
||||||
|
|
||||||
|
// Canvas contains drawing map: i,j -> rune
|
||||||
|
type Canvas map[[2]int]rune
|
||||||
|
|
||||||
|
// NewCanvas returns an empty Canvas
|
||||||
|
func NewCanvas() Canvas {
|
||||||
|
return make(map[[2]int]rune)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chOft(x, y int) rune {
|
||||||
|
return brailleOftMap[y%4][x%2]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Canvas) rawCh(x, y int) rune {
|
||||||
|
if ch, ok := c[[2]int{x, y}]; ok {
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
return '\u0000' //brailleOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// return coordinate in terminal
|
||||||
|
func chPos(x, y int) (int, int) {
|
||||||
|
return y / 4, x / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a point (x,y) in the virtual coordinate
|
||||||
|
func (c Canvas) Set(x, y int) {
|
||||||
|
i, j := chPos(x, y)
|
||||||
|
ch := c.rawCh(i, j)
|
||||||
|
ch |= chOft(x, y)
|
||||||
|
c[[2]int{i, j}] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset removes point (x,y)
|
||||||
|
func (c Canvas) Unset(x, y int) {
|
||||||
|
i, j := chPos(x, y)
|
||||||
|
ch := c.rawCh(i, j)
|
||||||
|
ch &= ^chOft(x, y)
|
||||||
|
c[[2]int{i, j}] = ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer returns un-styled points
|
||||||
|
func (c Canvas) Buffer() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
for k, v := range c {
|
||||||
|
buf.Set(k[0], k[1], Cell{Ch: v + brailleBase})
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build ignore
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCanvasSet(t *testing.T) {
|
||||||
|
c := NewCanvas()
|
||||||
|
c.Set(0, 0)
|
||||||
|
c.Set(0, 1)
|
||||||
|
c.Set(0, 2)
|
||||||
|
c.Set(0, 3)
|
||||||
|
c.Set(1, 3)
|
||||||
|
c.Set(2, 3)
|
||||||
|
c.Set(3, 3)
|
||||||
|
c.Set(4, 3)
|
||||||
|
c.Set(5, 3)
|
||||||
|
spew.Dump(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanvasUnset(t *testing.T) {
|
||||||
|
c := NewCanvas()
|
||||||
|
c.Set(0, 0)
|
||||||
|
c.Set(0, 1)
|
||||||
|
c.Set(0, 2)
|
||||||
|
c.Unset(0, 2)
|
||||||
|
spew.Dump(c)
|
||||||
|
c.Unset(0, 3)
|
||||||
|
spew.Dump(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanvasBuffer(t *testing.T) {
|
||||||
|
c := NewCanvas()
|
||||||
|
c.Set(0, 0)
|
||||||
|
c.Set(0, 1)
|
||||||
|
c.Set(0, 2)
|
||||||
|
c.Set(0, 3)
|
||||||
|
c.Set(1, 3)
|
||||||
|
c.Set(2, 3)
|
||||||
|
c.Set(3, 3)
|
||||||
|
c.Set(4, 3)
|
||||||
|
c.Set(5, 3)
|
||||||
|
c.Set(6, 3)
|
||||||
|
c.Set(7, 2)
|
||||||
|
c.Set(8, 1)
|
||||||
|
c.Set(9, 0)
|
||||||
|
bufs := c.Buffer()
|
||||||
|
spew.Dump(bufs)
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
|
||||||
|
copyright = """// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
exclude_dirs = [".git", "_docs"]
|
||||||
|
exclude_files = []
|
||||||
|
include_dirs = [".", "debug", "extra", "test", "_example"]
|
||||||
|
|
||||||
|
|
||||||
|
def is_target(fpath):
|
||||||
|
if os.path.splitext(fpath)[-1] == ".go":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_copyright(fpath):
|
||||||
|
print("processing " + fpath)
|
||||||
|
f = io.open(fpath, 'r', encoding='utf-8')
|
||||||
|
fstr = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# remove old
|
||||||
|
m = re.search('^// Copyright .+?\r?\n\r?\n', fstr, re.MULTILINE|re.DOTALL)
|
||||||
|
if m:
|
||||||
|
fstr = fstr[m.end():]
|
||||||
|
|
||||||
|
# add new
|
||||||
|
fstr = copyright + fstr
|
||||||
|
f = io.open(fpath, 'w',encoding='utf-8')
|
||||||
|
f.write(fstr)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for d in include_dirs:
|
||||||
|
files = [
|
||||||
|
os.path.join(d, f) for f in os.listdir(d)
|
||||||
|
if os.path.isfile(os.path.join(d, f))
|
||||||
|
]
|
||||||
|
for f in files:
|
||||||
|
if is_target(f):
|
||||||
|
update_copyright(f)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Package termui is a library designed for creating command line UI. For more info, goto http://github.com/gizak/termui
|
||||||
|
|
||||||
|
A simplest example:
|
||||||
|
package main
|
||||||
|
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err:=ui.Init(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
g := ui.NewGauge()
|
||||||
|
g.Percent = 50
|
||||||
|
g.Width = 50
|
||||||
|
g.BorderLabel = "Gauge"
|
||||||
|
|
||||||
|
ui.Render(g)
|
||||||
|
|
||||||
|
ui.Loop()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package termui
|
|
@ -0,0 +1,323 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Type string
|
||||||
|
Path string
|
||||||
|
From string
|
||||||
|
To string
|
||||||
|
Data interface{}
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysEvtChs []chan Event
|
||||||
|
|
||||||
|
type EvtKbd struct {
|
||||||
|
KeyStr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func evtKbd(e termbox.Event) EvtKbd {
|
||||||
|
ek := EvtKbd{}
|
||||||
|
|
||||||
|
k := string(e.Ch)
|
||||||
|
pre := ""
|
||||||
|
mod := ""
|
||||||
|
|
||||||
|
if e.Mod == termbox.ModAlt {
|
||||||
|
mod = "M-"
|
||||||
|
}
|
||||||
|
if e.Ch == 0 {
|
||||||
|
if e.Key > 0xFFFF-12 {
|
||||||
|
k = "<f" + strconv.Itoa(0xFFFF-int(e.Key)+1) + ">"
|
||||||
|
} else if e.Key > 0xFFFF-25 {
|
||||||
|
ks := []string{"<insert>", "<delete>", "<home>", "<end>", "<previous>", "<next>", "<up>", "<down>", "<left>", "<right>"}
|
||||||
|
k = ks[0xFFFF-int(e.Key)-12]
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Key <= 0x7F {
|
||||||
|
pre = "C-"
|
||||||
|
k = string('a' - 1 + int(e.Key))
|
||||||
|
kmap := map[termbox.Key][2]string{
|
||||||
|
termbox.KeyCtrlSpace: {"C-", "<space>"},
|
||||||
|
termbox.KeyBackspace: {"", "<backspace>"},
|
||||||
|
termbox.KeyTab: {"", "<tab>"},
|
||||||
|
termbox.KeyEnter: {"", "<enter>"},
|
||||||
|
termbox.KeyEsc: {"", "<escape>"},
|
||||||
|
termbox.KeyCtrlBackslash: {"C-", "\\"},
|
||||||
|
termbox.KeyCtrlSlash: {"C-", "/"},
|
||||||
|
termbox.KeySpace: {"", "<space>"},
|
||||||
|
termbox.KeyCtrl8: {"C-", "8"},
|
||||||
|
}
|
||||||
|
if sk, ok := kmap[e.Key]; ok {
|
||||||
|
pre = sk[0]
|
||||||
|
k = sk[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ek.KeyStr = pre + mod + k
|
||||||
|
return ek
|
||||||
|
}
|
||||||
|
|
||||||
|
func crtTermboxEvt(e termbox.Event) Event {
|
||||||
|
systypemap := map[termbox.EventType]string{
|
||||||
|
termbox.EventKey: "keyboard",
|
||||||
|
termbox.EventResize: "window",
|
||||||
|
termbox.EventMouse: "mouse",
|
||||||
|
termbox.EventError: "error",
|
||||||
|
termbox.EventInterrupt: "interrupt",
|
||||||
|
}
|
||||||
|
ne := Event{From: "/sys", Time: time.Now().Unix()}
|
||||||
|
typ := e.Type
|
||||||
|
ne.Type = systypemap[typ]
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case termbox.EventKey:
|
||||||
|
kbd := evtKbd(e)
|
||||||
|
ne.Path = "/sys/kbd/" + kbd.KeyStr
|
||||||
|
ne.Data = kbd
|
||||||
|
case termbox.EventResize:
|
||||||
|
wnd := EvtWnd{}
|
||||||
|
wnd.Width = e.Width
|
||||||
|
wnd.Height = e.Height
|
||||||
|
ne.Path = "/sys/wnd/resize"
|
||||||
|
ne.Data = wnd
|
||||||
|
case termbox.EventError:
|
||||||
|
err := EvtErr(e.Err)
|
||||||
|
ne.Path = "/sys/err"
|
||||||
|
ne.Data = err
|
||||||
|
case termbox.EventMouse:
|
||||||
|
m := EvtMouse{}
|
||||||
|
m.X = e.MouseX
|
||||||
|
m.Y = e.MouseY
|
||||||
|
ne.Path = "/sys/mouse"
|
||||||
|
ne.Data = m
|
||||||
|
}
|
||||||
|
return ne
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtWnd struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtMouse struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Press string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtErr error
|
||||||
|
|
||||||
|
func hookTermboxEvt() {
|
||||||
|
for {
|
||||||
|
e := termbox.PollEvent()
|
||||||
|
|
||||||
|
for _, c := range sysEvtChs {
|
||||||
|
go func(ch chan Event) {
|
||||||
|
ch <- crtTermboxEvt(e)
|
||||||
|
}(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSysEvtCh() chan Event {
|
||||||
|
ec := make(chan Event)
|
||||||
|
sysEvtChs = append(sysEvtChs, ec)
|
||||||
|
return ec
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultEvtStream = NewEvtStream()
|
||||||
|
|
||||||
|
type EvtStream struct {
|
||||||
|
sync.RWMutex
|
||||||
|
srcMap map[string]chan Event
|
||||||
|
stream chan Event
|
||||||
|
wg sync.WaitGroup
|
||||||
|
sigStopLoop chan Event
|
||||||
|
Handlers map[string]func(Event)
|
||||||
|
hook func(Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEvtStream() *EvtStream {
|
||||||
|
return &EvtStream{
|
||||||
|
srcMap: make(map[string]chan Event),
|
||||||
|
stream: make(chan Event),
|
||||||
|
Handlers: make(map[string]func(Event)),
|
||||||
|
sigStopLoop: make(chan Event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Init() {
|
||||||
|
es.Merge("internal", es.sigStopLoop)
|
||||||
|
go func() {
|
||||||
|
es.wg.Wait()
|
||||||
|
close(es.stream)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(p string) string {
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
if p[0] != '/' {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
return path.Clean(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPathMatch(pattern, path string) bool {
|
||||||
|
if len(pattern) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n := len(pattern)
|
||||||
|
return len(path) >= n && path[0:n] == pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Merge(name string, ec chan Event) {
|
||||||
|
es.Lock()
|
||||||
|
defer es.Unlock()
|
||||||
|
|
||||||
|
es.wg.Add(1)
|
||||||
|
es.srcMap[name] = ec
|
||||||
|
|
||||||
|
go func(a chan Event) {
|
||||||
|
for n := range a {
|
||||||
|
n.From = name
|
||||||
|
es.stream <- n
|
||||||
|
}
|
||||||
|
es.wg.Done()
|
||||||
|
}(ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Handle(path string, handler func(Event)) {
|
||||||
|
es.Handlers[cleanPath(path)] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMatch(mux map[string]func(Event), path string) string {
|
||||||
|
n := -1
|
||||||
|
pattern := ""
|
||||||
|
for m := range mux {
|
||||||
|
if !isPathMatch(m, path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(m) > n {
|
||||||
|
pattern = m
|
||||||
|
n = len(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
}
|
||||||
|
// Remove all existing defined Handlers from the map
|
||||||
|
func (es *EvtStream) ResetHandlers() {
|
||||||
|
for Path, _ := range es.Handlers {
|
||||||
|
delete(es.Handlers, Path)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) match(path string) string {
|
||||||
|
return findMatch(es.Handlers, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Hook(f func(Event)) {
|
||||||
|
es.hook = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) Loop() {
|
||||||
|
for e := range es.stream {
|
||||||
|
switch e.Path {
|
||||||
|
case "/sig/stoploop":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(a Event) {
|
||||||
|
es.RLock()
|
||||||
|
defer es.RUnlock()
|
||||||
|
if pattern := es.match(a.Path); pattern != "" {
|
||||||
|
es.Handlers[pattern](a)
|
||||||
|
}
|
||||||
|
}(e)
|
||||||
|
if es.hook != nil {
|
||||||
|
es.hook(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *EvtStream) StopLoop() {
|
||||||
|
go func() {
|
||||||
|
e := Event{
|
||||||
|
Path: "/sig/stoploop",
|
||||||
|
}
|
||||||
|
es.sigStopLoop <- e
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Merge(name string, ec chan Event) {
|
||||||
|
DefaultEvtStream.Merge(name, ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handle(path string, handler func(Event)) {
|
||||||
|
DefaultEvtStream.Handle(path, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Loop() {
|
||||||
|
DefaultEvtStream.Loop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopLoop() {
|
||||||
|
DefaultEvtStream.StopLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type EvtTimer struct {
|
||||||
|
Duration time.Duration
|
||||||
|
Count uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimerCh(du time.Duration) chan Event {
|
||||||
|
t := make(chan Event)
|
||||||
|
|
||||||
|
go func(a chan Event) {
|
||||||
|
n := uint64(0)
|
||||||
|
for {
|
||||||
|
n++
|
||||||
|
time.Sleep(du)
|
||||||
|
e := Event{}
|
||||||
|
e.Type = "timer"
|
||||||
|
e.Path = "/timer/" + du.String()
|
||||||
|
e.Time = time.Now().Unix()
|
||||||
|
e.Data = EvtTimer{
|
||||||
|
Duration: du,
|
||||||
|
Count: n,
|
||||||
|
}
|
||||||
|
t <- e
|
||||||
|
|
||||||
|
}
|
||||||
|
}(t)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefualtHandler = func(e Event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
var usrEvtCh = make(chan Event)
|
||||||
|
|
||||||
|
func SendCustomEvt(path string, data interface{}) {
|
||||||
|
e := Event{}
|
||||||
|
e.Path = path
|
||||||
|
e.Data = data
|
||||||
|
e.Time = time.Now().Unix()
|
||||||
|
usrEvtCh <- e
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var ps = []string{
|
||||||
|
"",
|
||||||
|
"/",
|
||||||
|
"/a",
|
||||||
|
"/b",
|
||||||
|
"/a/c",
|
||||||
|
"/a/b",
|
||||||
|
"/a/b/c",
|
||||||
|
"/a/b/c/d",
|
||||||
|
"/a/b/c/d/"}
|
||||||
|
|
||||||
|
func TestMatchScore(t *testing.T) {
|
||||||
|
chk := func(a, b string, s bool) {
|
||||||
|
if c := isPathMatch(a, b); c != s {
|
||||||
|
t.Errorf("\na:%s\nb:%s\nshould:%t\nactual:%t", a, b, s, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chk(ps[1], ps[1], true)
|
||||||
|
chk(ps[1], ps[2], true)
|
||||||
|
chk(ps[2], ps[1], false)
|
||||||
|
chk(ps[4], ps[1], false)
|
||||||
|
chk(ps[6], ps[2], false)
|
||||||
|
chk(ps[4], ps[5], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCrtEvt(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gauge is a progress bar like widget.
|
||||||
|
// A simple example:
|
||||||
|
/*
|
||||||
|
g := termui.NewGauge()
|
||||||
|
g.Percent = 40
|
||||||
|
g.Width = 50
|
||||||
|
g.Height = 3
|
||||||
|
g.BorderLabel = "Slim Gauge"
|
||||||
|
g.BarColor = termui.ColorRed
|
||||||
|
g.PercentColor = termui.ColorBlue
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ColorUndef Attribute = Attribute(^uint16(0))
|
||||||
|
|
||||||
|
type Gauge struct {
|
||||||
|
Block
|
||||||
|
Percent int
|
||||||
|
BarColor Attribute
|
||||||
|
PercentColor Attribute
|
||||||
|
PercentColorHighlighted Attribute
|
||||||
|
Label string
|
||||||
|
LabelAlign Align
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGauge return a new gauge with current theme.
|
||||||
|
func NewGauge() *Gauge {
|
||||||
|
g := &Gauge{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
PercentColor: ThemeAttr("gauge.percent.fg"),
|
||||||
|
BarColor: ThemeAttr("gauge.bar.bg"),
|
||||||
|
Label: "{{percent}}%",
|
||||||
|
LabelAlign: AlignCenter,
|
||||||
|
PercentColorHighlighted: ColorUndef,
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Width = 12
|
||||||
|
g.Height = 5
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (g *Gauge) Buffer() Buffer {
|
||||||
|
buf := g.Block.Buffer()
|
||||||
|
|
||||||
|
// plot bar
|
||||||
|
w := g.Percent * g.innerArea.Dx() / 100
|
||||||
|
for i := 0; i < g.innerArea.Dy(); i++ {
|
||||||
|
for j := 0; j < w; j++ {
|
||||||
|
c := Cell{}
|
||||||
|
c.Ch = ' '
|
||||||
|
c.Bg = g.BarColor
|
||||||
|
if c.Bg == ColorDefault {
|
||||||
|
c.Bg |= AttrReverse
|
||||||
|
}
|
||||||
|
buf.Set(g.innerArea.Min.X+j, g.innerArea.Min.Y+i, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// plot percentage
|
||||||
|
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1)
|
||||||
|
pry := g.innerArea.Min.Y + g.innerArea.Dy()/2
|
||||||
|
rs := str2runes(s)
|
||||||
|
var pos int
|
||||||
|
switch g.LabelAlign {
|
||||||
|
case AlignLeft:
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
case AlignCenter:
|
||||||
|
pos = (g.innerArea.Dx() - strWidth(s)) / 2
|
||||||
|
|
||||||
|
case AlignRight:
|
||||||
|
pos = g.innerArea.Dx() - strWidth(s) - 1
|
||||||
|
}
|
||||||
|
pos += g.innerArea.Min.X
|
||||||
|
|
||||||
|
for i, v := range rs {
|
||||||
|
c := Cell{
|
||||||
|
Ch: v,
|
||||||
|
Fg: g.PercentColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
if w+g.innerArea.Min.X > pos+i {
|
||||||
|
c.Bg = g.BarColor
|
||||||
|
if c.Bg == ColorDefault {
|
||||||
|
c.Bg |= AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.PercentColorHighlighted != ColorUndef {
|
||||||
|
c.Fg = g.PercentColorHighlighted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.Bg = g.Block.Bg
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Set(1+pos+i, pry, c)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
hash: 7a754ba100256404a978b2fc8738aee337beb822458e4b6060399fb89ebd215c
|
||||||
|
updated: 2016-11-03T17:39:24.323773674-04:00
|
||||||
|
imports:
|
||||||
|
- name: github.com/maruel/panicparse
|
||||||
|
version: ad661195ed0e88491e0f14be6613304e3b1141d6
|
||||||
|
subpackages:
|
||||||
|
- stack
|
||||||
|
- name: github.com/mattn/go-runewidth
|
||||||
|
version: 737072b4e32b7a5018b4a7125da8d12de90e8045
|
||||||
|
- name: github.com/mitchellh/go-wordwrap
|
||||||
|
version: ad45545899c7b13c020ea92b2072220eefad42b8
|
||||||
|
- name: github.com/nsf/termbox-go
|
||||||
|
version: b6acae516ace002cb8105a89024544a1480655a5
|
||||||
|
- name: golang.org/x/net
|
||||||
|
version: 569280fa63be4e201b975e5411e30a92178f0118
|
||||||
|
subpackages:
|
||||||
|
- websocket
|
||||||
|
testImports:
|
||||||
|
- name: github.com/davecgh/go-spew
|
||||||
|
version: 346938d642f2ec3594ed81d874461961cd0faa76
|
||||||
|
subpackages:
|
||||||
|
- spew
|
||||||
|
- name: github.com/pmezard/go-difflib
|
||||||
|
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||||
|
subpackages:
|
||||||
|
- difflib
|
||||||
|
- name: github.com/stretchr/testify
|
||||||
|
version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506
|
||||||
|
subpackages:
|
||||||
|
- assert
|
|
@ -0,0 +1,9 @@
|
||||||
|
package: github.com/gizak/termui
|
||||||
|
import:
|
||||||
|
- package: github.com/mattn/go-runewidth
|
||||||
|
- package: github.com/mitchellh/go-wordwrap
|
||||||
|
- package: github.com/nsf/termbox-go
|
||||||
|
- package: golang.org/x/net
|
||||||
|
subpackages:
|
||||||
|
- websocket
|
||||||
|
- package: github.com/maruel/panicparse
|
|
@ -0,0 +1,279 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
// GridBufferer introduces a Bufferer that can be manipulated by Grid.
|
||||||
|
type GridBufferer interface {
|
||||||
|
Bufferer
|
||||||
|
GetHeight() int
|
||||||
|
SetWidth(int)
|
||||||
|
SetX(int)
|
||||||
|
SetY(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row builds a layout tree
|
||||||
|
type Row struct {
|
||||||
|
Cols []*Row //children
|
||||||
|
Widget GridBufferer // root
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Span int
|
||||||
|
Offset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate and set the underlying layout tree's x, y, height and width.
|
||||||
|
func (r *Row) calcLayout() {
|
||||||
|
r.assignWidth(r.Width)
|
||||||
|
r.Height = r.solveHeight()
|
||||||
|
r.assignX(r.X)
|
||||||
|
r.assignY(r.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tell if the node is leaf in the tree.
|
||||||
|
func (r *Row) isLeaf() bool {
|
||||||
|
return r.Cols == nil || len(r.Cols) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Row) isRenderableLeaf() bool {
|
||||||
|
return r.isLeaf() && r.Widget != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign widgets' (and their parent rows') width recursively.
|
||||||
|
func (r *Row) assignWidth(w int) {
|
||||||
|
r.SetWidth(w)
|
||||||
|
|
||||||
|
accW := 0 // acc span and offset
|
||||||
|
calcW := make([]int, len(r.Cols)) // calculated width
|
||||||
|
calcOftX := make([]int, len(r.Cols)) // computated start position of x
|
||||||
|
|
||||||
|
for i, c := range r.Cols {
|
||||||
|
accW += c.Span + c.Offset
|
||||||
|
cw := int(float64(c.Span*r.Width) / 12.0)
|
||||||
|
|
||||||
|
if i >= 1 {
|
||||||
|
calcOftX[i] = calcOftX[i-1] +
|
||||||
|
calcW[i-1] +
|
||||||
|
int(float64(r.Cols[i-1].Offset*r.Width)/12.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use up the space if it is the last col
|
||||||
|
if i == len(r.Cols)-1 && accW == 12 {
|
||||||
|
cw = r.Width - calcOftX[i]
|
||||||
|
}
|
||||||
|
calcW[i] = cw
|
||||||
|
r.Cols[i].assignWidth(cw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom up calc and set rows' (and their widgets') height,
|
||||||
|
// return r's total height.
|
||||||
|
func (r *Row) solveHeight() int {
|
||||||
|
if r.isRenderableLeaf() {
|
||||||
|
r.Height = r.Widget.GetHeight()
|
||||||
|
return r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
maxh := 0
|
||||||
|
if !r.isLeaf() {
|
||||||
|
for _, c := range r.Cols {
|
||||||
|
nh := c.solveHeight()
|
||||||
|
// when embed rows in Cols, row widgets stack up
|
||||||
|
if r.Widget != nil {
|
||||||
|
nh += r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
if nh > maxh {
|
||||||
|
maxh = nh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Height = maxh
|
||||||
|
return maxh
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively assign x position for r tree.
|
||||||
|
func (r *Row) assignX(x int) {
|
||||||
|
r.SetX(x)
|
||||||
|
|
||||||
|
if !r.isLeaf() {
|
||||||
|
acc := 0
|
||||||
|
for i, c := range r.Cols {
|
||||||
|
if c.Offset != 0 {
|
||||||
|
acc += int(float64(c.Offset*r.Width) / 12.0)
|
||||||
|
}
|
||||||
|
r.Cols[i].assignX(x + acc)
|
||||||
|
acc += c.Width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively assign y position to r.
|
||||||
|
func (r *Row) assignY(y int) {
|
||||||
|
r.SetY(y)
|
||||||
|
|
||||||
|
if r.isLeaf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range r.Cols {
|
||||||
|
acc := 0
|
||||||
|
if r.Widget != nil {
|
||||||
|
acc = r.Widget.GetHeight()
|
||||||
|
}
|
||||||
|
r.Cols[i].assignY(y + acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight implements GridBufferer interface.
|
||||||
|
func (r Row) GetHeight() int {
|
||||||
|
return r.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetX implements GridBufferer interface.
|
||||||
|
func (r *Row) SetX(x int) {
|
||||||
|
r.X = x
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetX(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetY implements GridBufferer interface.
|
||||||
|
func (r *Row) SetY(y int) {
|
||||||
|
r.Y = y
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetY(y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWidth implements GridBufferer interface.
|
||||||
|
func (r *Row) SetWidth(w int) {
|
||||||
|
r.Width = w
|
||||||
|
if r.Widget != nil {
|
||||||
|
r.Widget.SetWidth(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface,
|
||||||
|
// recursively merge all widgets buffer
|
||||||
|
func (r *Row) Buffer() Buffer {
|
||||||
|
merged := NewBuffer()
|
||||||
|
|
||||||
|
if r.isRenderableLeaf() {
|
||||||
|
return r.Widget.Buffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// for those are not leaves but have a renderable widget
|
||||||
|
if r.Widget != nil {
|
||||||
|
merged.Merge(r.Widget.Buffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect buffer from children
|
||||||
|
if !r.isLeaf() {
|
||||||
|
for _, c := range r.Cols {
|
||||||
|
merged.Merge(c.Buffer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid implements 12 columns system.
|
||||||
|
// A simple example:
|
||||||
|
/*
|
||||||
|
import ui "github.com/gizak/termui"
|
||||||
|
// init and create widgets...
|
||||||
|
|
||||||
|
// build
|
||||||
|
ui.Body.AddRows(
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(6, 0, widget0),
|
||||||
|
ui.NewCol(6, 0, widget1)),
|
||||||
|
ui.NewRow(
|
||||||
|
ui.NewCol(3, 0, widget2),
|
||||||
|
ui.NewCol(3, 0, widget30, widget31, widget32),
|
||||||
|
ui.NewCol(6, 0, widget4)))
|
||||||
|
|
||||||
|
// calculate layout
|
||||||
|
ui.Body.Align()
|
||||||
|
|
||||||
|
ui.Render(ui.Body)
|
||||||
|
*/
|
||||||
|
type Grid struct {
|
||||||
|
Rows []*Row
|
||||||
|
Width int
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
BgColor Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrid returns *Grid with given rows.
|
||||||
|
func NewGrid(rows ...*Row) *Grid {
|
||||||
|
return &Grid{Rows: rows}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRows appends given rows to Grid.
|
||||||
|
func (g *Grid) AddRows(rs ...*Row) {
|
||||||
|
g.Rows = append(g.Rows, rs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRow creates a new row out of given columns.
|
||||||
|
func NewRow(cols ...*Row) *Row {
|
||||||
|
rs := &Row{Span: 12, Cols: cols}
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow.
|
||||||
|
// Note that if multiple widgets are provided, they will stack up in the col.
|
||||||
|
func NewCol(span, offset int, widgets ...GridBufferer) *Row {
|
||||||
|
r := &Row{Span: span, Offset: offset}
|
||||||
|
|
||||||
|
if widgets != nil && len(widgets) == 1 {
|
||||||
|
wgt := widgets[0]
|
||||||
|
nw, isRow := wgt.(*Row)
|
||||||
|
if isRow {
|
||||||
|
r.Cols = nw.Cols
|
||||||
|
} else {
|
||||||
|
r.Widget = wgt
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Cols = []*Row{}
|
||||||
|
ir := r
|
||||||
|
for _, w := range widgets {
|
||||||
|
nr := &Row{Span: 12, Widget: w}
|
||||||
|
ir.Cols = []*Row{nr}
|
||||||
|
ir = nr
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align calculate each rows' layout.
|
||||||
|
func (g *Grid) Align() {
|
||||||
|
h := 0
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
r.SetWidth(g.Width)
|
||||||
|
r.SetX(g.X)
|
||||||
|
r.SetY(g.Y + h)
|
||||||
|
r.calcLayout()
|
||||||
|
h += r.GetHeight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implments Bufferer interface.
|
||||||
|
func (g Grid) Buffer() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
for _, r := range g.Rows {
|
||||||
|
buf.Merge(r.Buffer())
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
var Body *Grid
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
var r *Row
|
||||||
|
|
||||||
|
func TestRowWidth(t *testing.T) {
|
||||||
|
p0 := NewBlock()
|
||||||
|
p0.Height = 1
|
||||||
|
p1 := NewBlock()
|
||||||
|
p1.Height = 1
|
||||||
|
p2 := NewBlock()
|
||||||
|
p2.Height = 1
|
||||||
|
p3 := NewBlock()
|
||||||
|
p3.Height = 1
|
||||||
|
|
||||||
|
/* test against tree:
|
||||||
|
|
||||||
|
r
|
||||||
|
/ \
|
||||||
|
0:w 1
|
||||||
|
/ \
|
||||||
|
10:w 11
|
||||||
|
/
|
||||||
|
110:w
|
||||||
|
/
|
||||||
|
1100:w
|
||||||
|
*/
|
||||||
|
|
||||||
|
r = NewRow(
|
||||||
|
NewCol(6, 0, p0),
|
||||||
|
NewCol(6, 0,
|
||||||
|
NewRow(
|
||||||
|
NewCol(6, 0, p1),
|
||||||
|
NewCol(6, 0, p2, p3))))
|
||||||
|
|
||||||
|
r.assignWidth(100)
|
||||||
|
if r.Width != 100 ||
|
||||||
|
(r.Cols[0].Width) != 50 ||
|
||||||
|
(r.Cols[1].Width) != 50 ||
|
||||||
|
(r.Cols[1].Cols[0].Width) != 25 ||
|
||||||
|
(r.Cols[1].Cols[1].Width) != 25 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].Width) != 25 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].Cols[0].Width) != 25 {
|
||||||
|
t.Error("assignWidth fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRowHeight(t *testing.T) {
|
||||||
|
spew.Dump()
|
||||||
|
|
||||||
|
if (r.solveHeight()) != 2 ||
|
||||||
|
(r.Cols[1].Cols[1].Height) != 2 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].Height) != 2 ||
|
||||||
|
(r.Cols[1].Cols[0].Height) != 1 {
|
||||||
|
t.Error("solveHeight fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssignXY(t *testing.T) {
|
||||||
|
r.assignX(0)
|
||||||
|
r.assignY(0)
|
||||||
|
if (r.Cols[0].X) != 0 ||
|
||||||
|
(r.Cols[1].Cols[0].X) != 50 ||
|
||||||
|
(r.Cols[1].Cols[1].X) != 75 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].X) != 75 ||
|
||||||
|
(r.Cols[1].Cols[0].Y) != 0 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].Y) != 0 ||
|
||||||
|
(r.Cols[1].Cols[1].Cols[0].Cols[0].Y) != 1 {
|
||||||
|
t.Error("assignXY fails")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
import rw "github.com/mattn/go-runewidth"
|
||||||
|
|
||||||
|
/* ---------------Port from termbox-go --------------------- */
|
||||||
|
|
||||||
|
// Attribute is printable cell's color and style.
|
||||||
|
type Attribute uint16
|
||||||
|
|
||||||
|
// 8 basic clolrs
|
||||||
|
const (
|
||||||
|
ColorDefault Attribute = iota
|
||||||
|
ColorBlack
|
||||||
|
ColorRed
|
||||||
|
ColorGreen
|
||||||
|
ColorYellow
|
||||||
|
ColorBlue
|
||||||
|
ColorMagenta
|
||||||
|
ColorCyan
|
||||||
|
ColorWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
//Have a constant that defines number of colors
|
||||||
|
const NumberofColors = 8
|
||||||
|
|
||||||
|
// Text style
|
||||||
|
const (
|
||||||
|
AttrBold Attribute = 1 << (iota + 9)
|
||||||
|
AttrUnderline
|
||||||
|
AttrReverse
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dot = "…"
|
||||||
|
dotw = rw.StringWidth(dot)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ----------------------- End ----------------------------- */
|
||||||
|
|
||||||
|
func toTmAttr(x Attribute) tm.Attribute {
|
||||||
|
return tm.Attribute(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func str2runes(s string) []rune {
|
||||||
|
return []rune(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here for backwards-compatibility.
|
||||||
|
func trimStr2Runes(s string, w int) []rune {
|
||||||
|
return TrimStr2Runes(s, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStr2Runes trims string to w[-1 rune], appends …, and returns the runes
|
||||||
|
// of that string if string is grather then n. If string is small then w,
|
||||||
|
// return the runes.
|
||||||
|
func TrimStr2Runes(s string, w int) []rune {
|
||||||
|
if w <= 0 {
|
||||||
|
return []rune{}
|
||||||
|
}
|
||||||
|
|
||||||
|
sw := rw.StringWidth(s)
|
||||||
|
if sw > w {
|
||||||
|
return []rune(rw.Truncate(s, w, dot))
|
||||||
|
}
|
||||||
|
return str2runes(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimStrIfAppropriate trim string to "s[:-1] + …"
|
||||||
|
// if string > width otherwise return string
|
||||||
|
func TrimStrIfAppropriate(s string, w int) string {
|
||||||
|
if w <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sw := rw.StringWidth(s)
|
||||||
|
if sw > w {
|
||||||
|
return rw.Truncate(s, w, dot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func strWidth(s string) int {
|
||||||
|
return rw.StringWidth(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func charWidth(ch rune) int {
|
||||||
|
return rw.RuneWidth(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
var whiteSpaceRegex = regexp.MustCompile(`\s`)
|
||||||
|
|
||||||
|
// StringToAttribute converts text to a termui attribute. You may specifiy more
|
||||||
|
// then one attribute like that: "BLACK, BOLD, ...". All whitespaces
|
||||||
|
// are ignored.
|
||||||
|
func StringToAttribute(text string) Attribute {
|
||||||
|
text = whiteSpaceRegex.ReplaceAllString(strings.ToLower(text), "")
|
||||||
|
attributes := strings.Split(text, ",")
|
||||||
|
result := Attribute(0)
|
||||||
|
|
||||||
|
for _, theAttribute := range attributes {
|
||||||
|
var match Attribute
|
||||||
|
switch theAttribute {
|
||||||
|
case "reset", "default":
|
||||||
|
match = ColorDefault
|
||||||
|
|
||||||
|
case "black":
|
||||||
|
match = ColorBlack
|
||||||
|
|
||||||
|
case "red":
|
||||||
|
match = ColorRed
|
||||||
|
|
||||||
|
case "green":
|
||||||
|
match = ColorGreen
|
||||||
|
|
||||||
|
case "yellow":
|
||||||
|
match = ColorYellow
|
||||||
|
|
||||||
|
case "blue":
|
||||||
|
match = ColorBlue
|
||||||
|
|
||||||
|
case "magenta":
|
||||||
|
match = ColorMagenta
|
||||||
|
|
||||||
|
case "cyan":
|
||||||
|
match = ColorCyan
|
||||||
|
|
||||||
|
case "white":
|
||||||
|
match = ColorWhite
|
||||||
|
|
||||||
|
case "bold":
|
||||||
|
match = AttrBold
|
||||||
|
|
||||||
|
case "underline":
|
||||||
|
match = AttrUnderline
|
||||||
|
|
||||||
|
case "reverse":
|
||||||
|
match = AttrReverse
|
||||||
|
}
|
||||||
|
|
||||||
|
result |= match
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextCells returns a coloured text cells []Cell
|
||||||
|
func TextCells(s string, fg, bg Attribute) []Cell {
|
||||||
|
cs := make([]Cell, 0, len(s))
|
||||||
|
|
||||||
|
// sequence := MarkdownTextRendererFactory{}.TextRenderer(s).Render(fg, bg)
|
||||||
|
// runes := []rune(sequence.NormalizedText)
|
||||||
|
runes := str2runes(s)
|
||||||
|
|
||||||
|
for n := range runes {
|
||||||
|
// point, _ := sequence.PointAt(n, 0, 0)
|
||||||
|
// cs = append(cs, Cell{point.Ch, point.Fg, point.Bg})
|
||||||
|
cs = append(cs, Cell{runes[n], fg, bg})
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the actual screen space the cell takes (usually 1 or 2).
|
||||||
|
func (c Cell) Width() int {
|
||||||
|
return charWidth(c.Ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy return a copy of c
|
||||||
|
func (c Cell) Copy() Cell {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimTxCells trims the overflowed text cells sequence.
|
||||||
|
func TrimTxCells(cs []Cell, w int) []Cell {
|
||||||
|
if len(cs) <= w {
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
return cs[:w]
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTrimTxCls trims the overflowed text cells sequence and append dots at the end.
|
||||||
|
func DTrimTxCls(cs []Cell, w int) []Cell {
|
||||||
|
l := len(cs)
|
||||||
|
if l <= 0 {
|
||||||
|
return []Cell{}
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := make([]Cell, 0, w)
|
||||||
|
csw := 0
|
||||||
|
for i := 0; i < l && csw <= w; i++ {
|
||||||
|
c := cs[i]
|
||||||
|
cw := c.Width()
|
||||||
|
|
||||||
|
if cw+csw < w {
|
||||||
|
rt = append(rt, c)
|
||||||
|
csw += cw
|
||||||
|
} else {
|
||||||
|
rt = append(rt, Cell{'…', c.Fg, c.Bg})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
|
||||||
|
func CellsToStr(cs []Cell) string {
|
||||||
|
str := ""
|
||||||
|
for _, c := range cs {
|
||||||
|
str += string(c.Ch)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStr2Rune(t *testing.T) {
|
||||||
|
s := "你好,世界."
|
||||||
|
rs := str2runes(s)
|
||||||
|
if len(rs) != 6 {
|
||||||
|
t.Error(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWidth(t *testing.T) {
|
||||||
|
s0 := "つのだ☆HIRO"
|
||||||
|
s1 := "11111111111"
|
||||||
|
// above not align for setting East Asian Ambiguous to wide!!
|
||||||
|
|
||||||
|
if strWidth(s0) != strWidth(s1) {
|
||||||
|
t.Error("str len failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
len1 := []rune{'a', '2', '&', '「', 'オ', '。'} //will false: 'ᆵ', 'ᄚ', 'ᄒ'
|
||||||
|
for _, v := range len1 {
|
||||||
|
if charWidth(v) != 1 {
|
||||||
|
t.Error("len1 failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
len2 := []rune{'漢', '字', '한', '자', '你', '好', 'だ', '。', '%', 's', 'E', 'ョ', '、', 'ヲ'}
|
||||||
|
for _, v := range len2 {
|
||||||
|
if charWidth(v) != 2 {
|
||||||
|
t.Error("len2 failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrim(t *testing.T) {
|
||||||
|
s := "つのだ☆HIRO"
|
||||||
|
if string(trimStr2Runes(s, 10)) != "つのだ☆HI"+dot {
|
||||||
|
t.Error("trim failed")
|
||||||
|
}
|
||||||
|
if string(trimStr2Runes(s, 11)) != "つのだ☆HIRO" {
|
||||||
|
t.Error("avoid tail trim failed")
|
||||||
|
}
|
||||||
|
if string(trimStr2Runes(s, 15)) != "つのだ☆HIRO" {
|
||||||
|
t.Error("avoid trim failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimStrIfAppropriate_NoTrim(t *testing.T) {
|
||||||
|
assert.Equal(t, "hello", TrimStrIfAppropriate("hello", 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrimStrIfAppropriate(t *testing.T) {
|
||||||
|
assert.Equal(t, "hel…", TrimStrIfAppropriate("hello", 4))
|
||||||
|
assert.Equal(t, "h…", TrimStrIfAppropriate("hello", 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringToAttribute(t *testing.T) {
|
||||||
|
assert.Equal(t, ColorRed, StringToAttribute("ReD"))
|
||||||
|
assert.Equal(t, ColorRed|AttrBold, StringToAttribute("RED, bold"))
|
||||||
|
}
|
|
@ -0,0 +1,331 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// only 16 possible combinations, why bother
|
||||||
|
var braillePatterns = map[[2]int]rune{
|
||||||
|
[2]int{0, 0}: '⣀',
|
||||||
|
[2]int{0, 1}: '⡠',
|
||||||
|
[2]int{0, 2}: '⡐',
|
||||||
|
[2]int{0, 3}: '⡈',
|
||||||
|
|
||||||
|
[2]int{1, 0}: '⢄',
|
||||||
|
[2]int{1, 1}: '⠤',
|
||||||
|
[2]int{1, 2}: '⠔',
|
||||||
|
[2]int{1, 3}: '⠌',
|
||||||
|
|
||||||
|
[2]int{2, 0}: '⢂',
|
||||||
|
[2]int{2, 1}: '⠢',
|
||||||
|
[2]int{2, 2}: '⠒',
|
||||||
|
[2]int{2, 3}: '⠊',
|
||||||
|
|
||||||
|
[2]int{3, 0}: '⢁',
|
||||||
|
[2]int{3, 1}: '⠡',
|
||||||
|
[2]int{3, 2}: '⠑',
|
||||||
|
[2]int{3, 3}: '⠉',
|
||||||
|
}
|
||||||
|
|
||||||
|
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
||||||
|
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
||||||
|
|
||||||
|
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode,
|
||||||
|
// because one braille char can represent two data points.
|
||||||
|
/*
|
||||||
|
lc := termui.NewLineChart()
|
||||||
|
lc.BorderLabel = "braille-mode Line Chart"
|
||||||
|
lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
|
||||||
|
lc.Width = 50
|
||||||
|
lc.Height = 12
|
||||||
|
lc.AxesColor = termui.ColorWhite
|
||||||
|
lc.LineColor = termui.ColorGreen | termui.AttrBold
|
||||||
|
// termui.Render(lc)...
|
||||||
|
*/
|
||||||
|
type LineChart struct {
|
||||||
|
Block
|
||||||
|
Data []float64
|
||||||
|
DataLabels []string // if unset, the data indices will be used
|
||||||
|
Mode string // braille | dot
|
||||||
|
DotStyle rune
|
||||||
|
LineColor Attribute
|
||||||
|
scale float64 // data span per cell on y-axis
|
||||||
|
AxesColor Attribute
|
||||||
|
drawingX int
|
||||||
|
drawingY int
|
||||||
|
axisYHeight int
|
||||||
|
axisXWidth int
|
||||||
|
axisYLabelGap int
|
||||||
|
axisXLabelGap int
|
||||||
|
topValue float64
|
||||||
|
bottomValue float64
|
||||||
|
labelX [][]rune
|
||||||
|
labelY [][]rune
|
||||||
|
labelYSpace int
|
||||||
|
maxY float64
|
||||||
|
minY float64
|
||||||
|
autoLabels bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLineChart returns a new LineChart with current theme.
|
||||||
|
func NewLineChart() *LineChart {
|
||||||
|
lc := &LineChart{Block: *NewBlock()}
|
||||||
|
lc.AxesColor = ThemeAttr("linechart.axes.fg")
|
||||||
|
lc.LineColor = ThemeAttr("linechart.line.fg")
|
||||||
|
lc.Mode = "braille"
|
||||||
|
lc.DotStyle = '•'
|
||||||
|
lc.axisXLabelGap = 2
|
||||||
|
lc.axisYLabelGap = 1
|
||||||
|
lc.bottomValue = math.Inf(1)
|
||||||
|
lc.topValue = math.Inf(-1)
|
||||||
|
return lc
|
||||||
|
}
|
||||||
|
|
||||||
|
// one cell contains two data points
|
||||||
|
// so the capicity is 2x as dot-mode
|
||||||
|
func (lc *LineChart) renderBraille() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
// return: b -> which cell should the point be in
|
||||||
|
// m -> in the cell, divided into 4 equal height levels, which subcell?
|
||||||
|
getPos := func(d float64) (b, m int) {
|
||||||
|
cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
|
||||||
|
b = cnt4 / 4
|
||||||
|
m = cnt4 % 4
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// plot points
|
||||||
|
for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
|
||||||
|
b0, m0 := getPos(lc.Data[2*i])
|
||||||
|
b1, m1 := getPos(lc.Data[2*i+1])
|
||||||
|
|
||||||
|
if b0 == b1 {
|
||||||
|
c := Cell{
|
||||||
|
Ch: braillePatterns[[2]int{m0, m1}],
|
||||||
|
Bg: lc.Bg,
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
}
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
} else {
|
||||||
|
c0 := Cell{Ch: lSingleBraille[m0],
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg}
|
||||||
|
x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
buf.Set(x0, y0, c0)
|
||||||
|
|
||||||
|
c1 := Cell{Ch: rSingleBraille[m1],
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg}
|
||||||
|
x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
|
||||||
|
buf.Set(x1, y1, c1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) renderDot() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: lc.DotStyle,
|
||||||
|
Fg: lc.LineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLabelX() {
|
||||||
|
lc.labelX = [][]rune{}
|
||||||
|
|
||||||
|
for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
|
||||||
|
if lc.Mode == "dot" {
|
||||||
|
if l >= len(lc.DataLabels) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str2runes(lc.DataLabels[l])
|
||||||
|
w := strWidth(lc.DataLabels[l])
|
||||||
|
if l+w <= lc.axisXWidth {
|
||||||
|
lc.labelX = append(lc.labelX, s)
|
||||||
|
}
|
||||||
|
l += w + lc.axisXLabelGap
|
||||||
|
} else { // braille
|
||||||
|
if 2*l >= len(lc.DataLabels) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str2runes(lc.DataLabels[2*l])
|
||||||
|
w := strWidth(lc.DataLabels[2*l])
|
||||||
|
if l+w <= lc.axisXWidth {
|
||||||
|
lc.labelX = append(lc.labelX, s)
|
||||||
|
}
|
||||||
|
l += w + lc.axisXLabelGap
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortenFloatVal(x float64) string {
|
||||||
|
s := fmt.Sprintf("%.2f", x)
|
||||||
|
if len(s)-3 > 3 {
|
||||||
|
s = fmt.Sprintf("%.2e", x)
|
||||||
|
}
|
||||||
|
|
||||||
|
if x < 0 {
|
||||||
|
s = fmt.Sprintf("%.2f", x)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLabelY() {
|
||||||
|
span := lc.topValue - lc.bottomValue
|
||||||
|
lc.scale = span / float64(lc.axisYHeight)
|
||||||
|
|
||||||
|
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
|
||||||
|
lc.labelY = make([][]rune, n)
|
||||||
|
maxLen := 0
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n)))
|
||||||
|
if len(s) > maxLen {
|
||||||
|
maxLen = len(s)
|
||||||
|
}
|
||||||
|
lc.labelY[i] = s
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.labelYSpace = maxLen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) calcLayout() {
|
||||||
|
// set datalabels if it is not provided
|
||||||
|
if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
|
||||||
|
lc.autoLabels = true
|
||||||
|
lc.DataLabels = make([]string, len(lc.Data))
|
||||||
|
for i := range lc.Data {
|
||||||
|
lc.DataLabels[i] = fmt.Sprint(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy increase, to avoid y shaking frequently
|
||||||
|
// update bound Y when drawing is gonna overflow
|
||||||
|
lc.minY = lc.Data[0]
|
||||||
|
lc.maxY = lc.Data[0]
|
||||||
|
|
||||||
|
// valid visible range
|
||||||
|
vrange := lc.innerArea.Dx()
|
||||||
|
if lc.Mode == "braille" {
|
||||||
|
vrange = 2 * lc.innerArea.Dx()
|
||||||
|
}
|
||||||
|
if vrange > len(lc.Data) {
|
||||||
|
vrange = len(lc.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range lc.Data[:vrange] {
|
||||||
|
if v > lc.maxY {
|
||||||
|
lc.maxY = v
|
||||||
|
}
|
||||||
|
if v < lc.minY {
|
||||||
|
lc.minY = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span := lc.maxY - lc.minY
|
||||||
|
|
||||||
|
if lc.minY < lc.bottomValue {
|
||||||
|
lc.bottomValue = lc.minY - 0.2*span
|
||||||
|
}
|
||||||
|
|
||||||
|
if lc.maxY > lc.topValue {
|
||||||
|
lc.topValue = lc.maxY + 0.2*span
|
||||||
|
}
|
||||||
|
|
||||||
|
lc.axisYHeight = lc.innerArea.Dy() - 2
|
||||||
|
lc.calcLabelY()
|
||||||
|
|
||||||
|
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
|
||||||
|
lc.calcLabelX()
|
||||||
|
|
||||||
|
lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
|
||||||
|
lc.drawingY = lc.innerArea.Min.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lc *LineChart) plotAxes() Buffer {
|
||||||
|
buf := NewBuffer()
|
||||||
|
|
||||||
|
origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
|
||||||
|
origX := lc.innerArea.Min.X + lc.labelYSpace
|
||||||
|
|
||||||
|
buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
|
||||||
|
for x := origX + 1; x < origX+lc.axisXWidth; x++ {
|
||||||
|
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
|
||||||
|
for dy := 1; dy <= lc.axisYHeight; dy++ {
|
||||||
|
buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
|
||||||
|
// x label
|
||||||
|
oft := 0
|
||||||
|
for _, rs := range lc.labelX {
|
||||||
|
if oft+len(rs) > lc.axisXWidth {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for j, r := range rs {
|
||||||
|
c := Cell{
|
||||||
|
Ch: r,
|
||||||
|
Fg: lc.AxesColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x := origX + oft + j
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
oft += len(rs) + lc.axisXLabelGap
|
||||||
|
}
|
||||||
|
|
||||||
|
// y labels
|
||||||
|
for i, rs := range lc.labelY {
|
||||||
|
for j, r := range rs {
|
||||||
|
buf.Set(
|
||||||
|
lc.innerArea.Min.X+j,
|
||||||
|
origY-i*(lc.axisYLabelGap+1),
|
||||||
|
Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (lc *LineChart) Buffer() Buffer {
|
||||||
|
buf := lc.Block.Buffer()
|
||||||
|
|
||||||
|
if lc.Data == nil || len(lc.Data) == 0 {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
lc.calcLayout()
|
||||||
|
buf.Merge(lc.plotAxes())
|
||||||
|
|
||||||
|
if lc.Mode == "dot" {
|
||||||
|
buf.Merge(lc.renderDot())
|
||||||
|
} else {
|
||||||
|
buf.Merge(lc.renderBraille())
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const VDASH = '┊'
|
||||||
|
const HDASH = '┈'
|
||||||
|
const ORIGIN = '└'
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
const VDASH = '|'
|
||||||
|
const HDASH = '-'
|
||||||
|
const ORIGIN = '+'
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// List displays []string as its items,
|
||||||
|
// it has a Overflow option (default is "hidden"), when set to "hidden",
|
||||||
|
// the item exceeding List's width is truncated, but when set to "wrap",
|
||||||
|
// the overflowed text breaks into next line.
|
||||||
|
/*
|
||||||
|
strs := []string{
|
||||||
|
"[0] github.com/gizak/termui",
|
||||||
|
"[1] editbox.go",
|
||||||
|
"[2] iterrupt.go",
|
||||||
|
"[3] keyboard.go",
|
||||||
|
"[4] output.go",
|
||||||
|
"[5] random_out.go",
|
||||||
|
"[6] dashboard.go",
|
||||||
|
"[7] nsf/termbox-go"}
|
||||||
|
|
||||||
|
ls := termui.NewList()
|
||||||
|
ls.Items = strs
|
||||||
|
ls.ItemFgColor = termui.ColorYellow
|
||||||
|
ls.BorderLabel = "List"
|
||||||
|
ls.Height = 7
|
||||||
|
ls.Width = 25
|
||||||
|
ls.Y = 0
|
||||||
|
*/
|
||||||
|
type List struct {
|
||||||
|
Block
|
||||||
|
Items []string
|
||||||
|
Overflow string
|
||||||
|
ItemFgColor Attribute
|
||||||
|
ItemBgColor Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewList returns a new *List with current theme.
|
||||||
|
func NewList() *List {
|
||||||
|
l := &List{Block: *NewBlock()}
|
||||||
|
l.Overflow = "hidden"
|
||||||
|
l.ItemFgColor = ThemeAttr("list.item.fg")
|
||||||
|
l.ItemBgColor = ThemeAttr("list.item.bg")
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (l *List) Buffer() Buffer {
|
||||||
|
buf := l.Block.Buffer()
|
||||||
|
|
||||||
|
switch l.Overflow {
|
||||||
|
case "wrap":
|
||||||
|
cs := DefaultTxBuilder.Build(strings.Join(l.Items, "\n"), l.ItemFgColor, l.ItemBgColor)
|
||||||
|
i, j, k := 0, 0, 0
|
||||||
|
for i < l.innerArea.Dy() && k < len(cs) {
|
||||||
|
w := cs[k].Width()
|
||||||
|
if cs[k].Ch == '\n' || j+w > l.innerArea.Dx() {
|
||||||
|
i++
|
||||||
|
j = 0
|
||||||
|
if cs[k].Ch == '\n' {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, cs[k])
|
||||||
|
|
||||||
|
k++
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
|
||||||
|
case "hidden":
|
||||||
|
trimItems := l.Items
|
||||||
|
if len(trimItems) > l.innerArea.Dy() {
|
||||||
|
trimItems = trimItems[:l.innerArea.Dy()]
|
||||||
|
}
|
||||||
|
for i, v := range trimItems {
|
||||||
|
cs := DTrimTxCls(DefaultTxBuilder.Build(v, l.ItemFgColor, l.ItemBgColor), l.innerArea.Dx())
|
||||||
|
j := 0
|
||||||
|
for _, vv := range cs {
|
||||||
|
w := vv.Width()
|
||||||
|
buf.Set(l.innerArea.Min.X+j, l.innerArea.Min.Y+i, vv)
|
||||||
|
j += w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,242 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go
|
||||||
|
// Multi-Colored-BarChart creates multiple bars in a widget:
|
||||||
|
/*
|
||||||
|
bc := termui.NewMBarChart()
|
||||||
|
data := make([][]int, 2)
|
||||||
|
data[0] := []int{3, 2, 5, 7, 9, 4}
|
||||||
|
data[1] := []int{7, 8, 5, 3, 1, 6}
|
||||||
|
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"}
|
||||||
|
bc.BorderLabel = "Bar Chart"
|
||||||
|
bc.Data = data
|
||||||
|
bc.Width = 26
|
||||||
|
bc.Height = 10
|
||||||
|
bc.DataLabels = bclabels
|
||||||
|
bc.TextColor = termui.ColorGreen
|
||||||
|
bc.BarColor = termui.ColorRed
|
||||||
|
bc.NumColor = termui.ColorYellow
|
||||||
|
*/
|
||||||
|
type MBarChart struct {
|
||||||
|
Block
|
||||||
|
BarColor [NumberofColors]Attribute
|
||||||
|
TextColor Attribute
|
||||||
|
NumColor [NumberofColors]Attribute
|
||||||
|
Data [NumberofColors][]int
|
||||||
|
DataLabels []string
|
||||||
|
BarWidth int
|
||||||
|
BarGap int
|
||||||
|
labels [][]rune
|
||||||
|
dataNum [NumberofColors][][]rune
|
||||||
|
numBar int
|
||||||
|
scale float64
|
||||||
|
max int
|
||||||
|
minDataLen int
|
||||||
|
numStack int
|
||||||
|
ShowScale bool
|
||||||
|
maxScale []rune
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBarChart returns a new *BarChart with current theme.
|
||||||
|
func NewMBarChart() *MBarChart {
|
||||||
|
bc := &MBarChart{Block: *NewBlock()}
|
||||||
|
bc.BarColor[0] = ThemeAttr("mbarchart.bar.bg")
|
||||||
|
bc.NumColor[0] = ThemeAttr("mbarchart.num.fg")
|
||||||
|
bc.TextColor = ThemeAttr("mbarchart.text.fg")
|
||||||
|
bc.BarGap = 1
|
||||||
|
bc.BarWidth = 3
|
||||||
|
return bc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *MBarChart) layout() {
|
||||||
|
bc.numBar = bc.innerArea.Dx() / (bc.BarGap + bc.BarWidth)
|
||||||
|
bc.labels = make([][]rune, bc.numBar)
|
||||||
|
DataLen := 0
|
||||||
|
LabelLen := len(bc.DataLabels)
|
||||||
|
bc.minDataLen = 9999 //Set this to some very hight value so that we find the minimum one We want to know which array among data[][] has got the least length
|
||||||
|
|
||||||
|
// We need to know how many stack/data array data[0] , data[1] are there
|
||||||
|
for i := 0; i < len(bc.Data); i++ {
|
||||||
|
if bc.Data[i] == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
DataLen++
|
||||||
|
}
|
||||||
|
bc.numStack = DataLen
|
||||||
|
|
||||||
|
//We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs
|
||||||
|
|
||||||
|
for i := 0; i < DataLen; i++ {
|
||||||
|
if bc.minDataLen > len(bc.Data[i]) {
|
||||||
|
bc.minDataLen = len(bc.Data[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if LabelLen > bc.minDataLen {
|
||||||
|
LabelLen = bc.minDataLen
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < LabelLen && i < bc.numBar; i++ {
|
||||||
|
bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < bc.numStack; i++ {
|
||||||
|
bc.dataNum[i] = make([][]rune, len(bc.Data[i]))
|
||||||
|
//For each stack of bar calcualte the rune
|
||||||
|
for j := 0; j < LabelLen && i < bc.numBar; j++ {
|
||||||
|
n := bc.Data[i][j]
|
||||||
|
s := fmt.Sprint(n)
|
||||||
|
bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth)
|
||||||
|
}
|
||||||
|
//If color is not defined by default then populate a color that is different from the prevous bar
|
||||||
|
if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault {
|
||||||
|
if i == 0 {
|
||||||
|
bc.BarColor[i] = ColorBlack
|
||||||
|
} else {
|
||||||
|
bc.BarColor[i] = bc.BarColor[i-1] + 1
|
||||||
|
if bc.BarColor[i] > NumberofColors {
|
||||||
|
bc.BarColor[i] = ColorBlack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//If Max value is not set then we have to populate, this time the max value will be max(sum(d1[0],d2[0],d3[0]) .... sum(d1[n], d2[n], d3[n]))
|
||||||
|
|
||||||
|
if bc.max == 0 {
|
||||||
|
bc.max = -1
|
||||||
|
}
|
||||||
|
for i := 0; i < bc.minDataLen && i < LabelLen; i++ {
|
||||||
|
var dsum int
|
||||||
|
for j := 0; j < bc.numStack; j++ {
|
||||||
|
dsum += bc.Data[j][i]
|
||||||
|
}
|
||||||
|
if dsum > bc.max {
|
||||||
|
bc.max = dsum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Finally Calculate max sale
|
||||||
|
if bc.ShowScale {
|
||||||
|
s := fmt.Sprintf("%d", bc.max)
|
||||||
|
bc.maxScale = trimStr2Runes(s, len(s))
|
||||||
|
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-2)
|
||||||
|
} else {
|
||||||
|
bc.scale = float64(bc.max) / float64(bc.innerArea.Dy()-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *MBarChart) SetMax(max int) {
|
||||||
|
|
||||||
|
if max > 0 {
|
||||||
|
bc.max = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (bc *MBarChart) Buffer() Buffer {
|
||||||
|
buf := bc.Block.Buffer()
|
||||||
|
bc.layout()
|
||||||
|
var oftX int
|
||||||
|
|
||||||
|
for i := 0; i < bc.numBar && i < bc.minDataLen && i < len(bc.DataLabels); i++ {
|
||||||
|
ph := 0 //Previous Height to stack up
|
||||||
|
oftX = i * (bc.BarWidth + bc.BarGap)
|
||||||
|
for i1 := 0; i1 < bc.numStack; i1++ {
|
||||||
|
h := int(float64(bc.Data[i1][i]) / bc.scale)
|
||||||
|
// plot bars
|
||||||
|
for j := 0; j < bc.BarWidth; j++ {
|
||||||
|
for k := 0; k < h; k++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: ' ',
|
||||||
|
Bg: bc.BarColor[i1],
|
||||||
|
}
|
||||||
|
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
|
||||||
|
c.Bg |= AttrReverse
|
||||||
|
}
|
||||||
|
x := bc.innerArea.Min.X + i*(bc.BarWidth+bc.BarGap) + j
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - k - ph
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ph += h
|
||||||
|
}
|
||||||
|
// plot text
|
||||||
|
for j, k := 0, 0; j < len(bc.labels[i]); j++ {
|
||||||
|
w := charWidth(bc.labels[i][j])
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.labels[i][j],
|
||||||
|
Bg: bc.Bg,
|
||||||
|
Fg: bc.TextColor,
|
||||||
|
}
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 1
|
||||||
|
x := bc.innerArea.Max.X + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
k += w
|
||||||
|
}
|
||||||
|
// plot num
|
||||||
|
ph = 0 //re-initialize previous height
|
||||||
|
for i1 := 0; i1 < bc.numStack; i1++ {
|
||||||
|
h := int(float64(bc.Data[i1][i]) / bc.scale)
|
||||||
|
for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.dataNum[i1][i][j],
|
||||||
|
Fg: bc.NumColor[i1],
|
||||||
|
Bg: bc.BarColor[i1],
|
||||||
|
}
|
||||||
|
if bc.BarColor[i1] == ColorDefault { // the same as above
|
||||||
|
c.Bg |= AttrReverse
|
||||||
|
}
|
||||||
|
if h == 0 {
|
||||||
|
c.Bg = bc.Bg
|
||||||
|
}
|
||||||
|
x := bc.innerArea.Min.X + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2 - ph
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
ph += h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.ShowScale {
|
||||||
|
//Currently bar graph only supprts data range from 0 to MAX
|
||||||
|
//Plot 0
|
||||||
|
c := Cell{
|
||||||
|
Ch: '0',
|
||||||
|
Bg: bc.Bg,
|
||||||
|
Fg: bc.TextColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
y := bc.innerArea.Min.Y + bc.innerArea.Dy() - 2
|
||||||
|
x := bc.X
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
|
||||||
|
//Plot the maximum sacle value
|
||||||
|
for i := 0; i < len(bc.maxScale); i++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: bc.maxScale[i],
|
||||||
|
Bg: bc.Bg,
|
||||||
|
Fg: bc.TextColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
y := bc.innerArea.Min.Y
|
||||||
|
x := bc.X + i
|
||||||
|
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
pages:
|
||||||
|
- Home: 'index.md'
|
||||||
|
- Quickstart: 'quickstart.md'
|
||||||
|
- Recipes: 'recipes.md'
|
||||||
|
- References:
|
||||||
|
- Layouts: 'layouts.md'
|
||||||
|
- Components: 'components.md'
|
||||||
|
- Events: 'events.md'
|
||||||
|
- Themes: 'themes.md'
|
||||||
|
- Versions: 'versions.md'
|
||||||
|
- About: 'about.md'
|
||||||
|
|
||||||
|
site_name: termui
|
||||||
|
repo_url: https://github.com/gizak/termui/
|
||||||
|
site_description: 'termui user guide'
|
||||||
|
site_author: gizak
|
||||||
|
|
||||||
|
docs_dir: '_docs'
|
||||||
|
|
||||||
|
theme: readthedocs
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- smarty
|
||||||
|
- admonition
|
||||||
|
- toc
|
||||||
|
|
||||||
|
extra:
|
||||||
|
version: 1.0
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
// Par displays a paragraph.
|
||||||
|
/*
|
||||||
|
par := termui.NewPar("Simple Text")
|
||||||
|
par.Height = 3
|
||||||
|
par.Width = 17
|
||||||
|
par.BorderLabel = "Label"
|
||||||
|
*/
|
||||||
|
type Par struct {
|
||||||
|
Block
|
||||||
|
Text string
|
||||||
|
TextFgColor Attribute
|
||||||
|
TextBgColor Attribute
|
||||||
|
WrapLength int // words wrap limit. Note it may not work properly with multi-width char
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPar returns a new *Par with given text as its content.
|
||||||
|
func NewPar(s string) *Par {
|
||||||
|
return &Par{
|
||||||
|
Block: *NewBlock(),
|
||||||
|
Text: s,
|
||||||
|
TextFgColor: ThemeAttr("par.text.fg"),
|
||||||
|
TextBgColor: ThemeAttr("par.text.bg"),
|
||||||
|
WrapLength: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (p *Par) Buffer() Buffer {
|
||||||
|
buf := p.Block.Buffer()
|
||||||
|
|
||||||
|
fg, bg := p.TextFgColor, p.TextBgColor
|
||||||
|
cs := DefaultTxBuilder.Build(p.Text, fg, bg)
|
||||||
|
|
||||||
|
// wrap if WrapLength set
|
||||||
|
if p.WrapLength < 0 {
|
||||||
|
cs = wrapTx(cs, p.Width-2)
|
||||||
|
} else if p.WrapLength > 0 {
|
||||||
|
cs = wrapTx(cs, p.WrapLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
y, x, n := 0, 0, 0
|
||||||
|
for y < p.innerArea.Dy() && n < len(cs) {
|
||||||
|
w := cs[n].Width()
|
||||||
|
if cs[n].Ch == '\n' || x+w > p.innerArea.Dx() {
|
||||||
|
y++
|
||||||
|
x = 0 // set x = 0
|
||||||
|
if cs[n].Ch == '\n' {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
|
||||||
|
if y >= p.innerArea.Dy() {
|
||||||
|
buf.Set(p.innerArea.Min.X+p.innerArea.Dx()-1,
|
||||||
|
p.innerArea.Min.Y+p.innerArea.Dy()-1,
|
||||||
|
Cell{Ch: '…', Fg: p.TextFgColor, Bg: p.TextBgColor})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Set(p.innerArea.Min.X+x, p.innerArea.Min.Y+y, cs[n])
|
||||||
|
|
||||||
|
n++
|
||||||
|
x += w
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPar_NoBorderBackground(t *testing.T) {
|
||||||
|
par := NewPar("a")
|
||||||
|
par.Border = false
|
||||||
|
par.Bg = ColorBlue
|
||||||
|
par.TextBgColor = ColorBlue
|
||||||
|
par.Width = 2
|
||||||
|
par.Height = 2
|
||||||
|
|
||||||
|
pts := par.Buffer()
|
||||||
|
for _, p := range pts.CellMap {
|
||||||
|
t.Log(p)
|
||||||
|
if p.Bg != par.Bg {
|
||||||
|
t.Errorf("expected color to be %v but got %v", par.Bg, p.Bg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
|
||||||
|
// Align is the position of the gauge's label.
|
||||||
|
type Align uint
|
||||||
|
|
||||||
|
// All supported positions.
|
||||||
|
const (
|
||||||
|
AlignNone Align = 0
|
||||||
|
AlignLeft Align = 1 << iota
|
||||||
|
AlignRight
|
||||||
|
AlignBottom
|
||||||
|
AlignTop
|
||||||
|
AlignCenterVertical
|
||||||
|
AlignCenterHorizontal
|
||||||
|
AlignCenter = AlignCenterVertical | AlignCenterHorizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
func AlignArea(parent, child image.Rectangle, a Align) image.Rectangle {
|
||||||
|
w, h := child.Dx(), child.Dy()
|
||||||
|
|
||||||
|
// parent center
|
||||||
|
pcx, pcy := parent.Min.X+parent.Dx()/2, parent.Min.Y+parent.Dy()/2
|
||||||
|
// child center
|
||||||
|
ccx, ccy := child.Min.X+child.Dx()/2, child.Min.Y+child.Dy()/2
|
||||||
|
|
||||||
|
if a&AlignLeft == AlignLeft {
|
||||||
|
child.Min.X = parent.Min.X
|
||||||
|
child.Max.X = child.Min.X + w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignRight == AlignRight {
|
||||||
|
child.Max.X = parent.Max.X
|
||||||
|
child.Min.X = child.Max.X - w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignBottom == AlignBottom {
|
||||||
|
child.Max.Y = parent.Max.Y
|
||||||
|
child.Min.Y = child.Max.Y - h
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignTop == AlignRight {
|
||||||
|
child.Min.Y = parent.Min.Y
|
||||||
|
child.Max.Y = child.Min.Y + h
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignCenterHorizontal == AlignCenterHorizontal {
|
||||||
|
child.Min.X += pcx - ccx
|
||||||
|
child.Max.X = child.Min.X + w
|
||||||
|
}
|
||||||
|
|
||||||
|
if a&AlignCenterVertical == AlignCenterVertical {
|
||||||
|
child.Min.Y += pcy - ccy
|
||||||
|
child.Max.Y = child.Min.Y + h
|
||||||
|
}
|
||||||
|
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveArea(a image.Rectangle, dx, dy int) image.Rectangle {
|
||||||
|
a.Min.X += dx
|
||||||
|
a.Max.X += dx
|
||||||
|
a.Min.Y += dy
|
||||||
|
a.Max.Y += dy
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
var termWidth int
|
||||||
|
var termHeight int
|
||||||
|
|
||||||
|
func TermRect() image.Rectangle {
|
||||||
|
return image.Rect(0, 0, termWidth, termHeight)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlignArea(t *testing.T) {
|
||||||
|
p := image.Rect(0, 0, 100, 100)
|
||||||
|
c := image.Rect(10, 10, 20, 20)
|
||||||
|
|
||||||
|
nc := AlignArea(p, c, AlignLeft)
|
||||||
|
if nc.Min.X != 0 || nc.Max.Y != 20 {
|
||||||
|
t.Errorf("AlignLeft failed:\n%+v", nc)
|
||||||
|
}
|
||||||
|
|
||||||
|
nc = AlignArea(p, c, AlignCenter)
|
||||||
|
if nc.Min.X != 45 || nc.Max.Y != 55 {
|
||||||
|
t.Error("AlignCenter failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
nc = AlignArea(p, c, AlignBottom|AlignRight)
|
||||||
|
if nc.Min.X != 90 || nc.Max.Y != 100 {
|
||||||
|
t.Errorf("AlignBottom|AlignRight failed\n%+v", nc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMoveArea(t *testing.T) {
|
||||||
|
a := image.Rect(10, 10, 20, 20)
|
||||||
|
a = MoveArea(a, 5, 10)
|
||||||
|
if a.Min.X != 15 || a.Min.Y != 20 || a.Max.X != 25 || a.Max.Y != 30 {
|
||||||
|
t.Error("MoveArea failed")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/maruel/panicparse/stack"
|
||||||
|
tm "github.com/nsf/termbox-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bufferer should be implemented by all renderable components.
|
||||||
|
type Bufferer interface {
|
||||||
|
Buffer() Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes termui library. This function should be called before any others.
|
||||||
|
// After initialization, the library must be finalized by 'Close' function.
|
||||||
|
func Init() error {
|
||||||
|
if err := tm.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sysEvtChs = make([]chan Event, 0)
|
||||||
|
go hookTermboxEvt()
|
||||||
|
|
||||||
|
renderJobs = make(chan []Bufferer)
|
||||||
|
//renderLock = new(sync.RWMutex)
|
||||||
|
|
||||||
|
Body = NewGrid()
|
||||||
|
Body.X = 0
|
||||||
|
Body.Y = 0
|
||||||
|
Body.BgColor = ThemeAttr("bg")
|
||||||
|
Body.Width = TermWidth()
|
||||||
|
|
||||||
|
DefaultEvtStream.Init()
|
||||||
|
DefaultEvtStream.Merge("termbox", NewSysEvtCh())
|
||||||
|
DefaultEvtStream.Merge("timer", NewTimerCh(time.Second))
|
||||||
|
DefaultEvtStream.Merge("custom", usrEvtCh)
|
||||||
|
|
||||||
|
DefaultEvtStream.Handle("/", DefualtHandler)
|
||||||
|
DefaultEvtStream.Handle("/sys/wnd/resize", func(e Event) {
|
||||||
|
w := e.Data.(EvtWnd)
|
||||||
|
Body.Width = w.Width
|
||||||
|
})
|
||||||
|
|
||||||
|
DefaultWgtMgr = NewWgtMgr()
|
||||||
|
DefaultEvtStream.Hook(DefaultWgtMgr.WgtHandlersHook())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for bs := range renderJobs {
|
||||||
|
render(bs...)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close finalizes termui library,
|
||||||
|
// should be called after successful initialization when termui's functionality isn't required anymore.
|
||||||
|
func Close() {
|
||||||
|
tm.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderLock sync.Mutex
|
||||||
|
|
||||||
|
func termSync() {
|
||||||
|
renderLock.Lock()
|
||||||
|
tm.Sync()
|
||||||
|
termWidth, termHeight = tm.Size()
|
||||||
|
renderLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermWidth returns the current terminal's width.
|
||||||
|
func TermWidth() int {
|
||||||
|
termSync()
|
||||||
|
return termWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermHeight returns the current terminal's height.
|
||||||
|
func TermHeight() int {
|
||||||
|
termSync()
|
||||||
|
return termHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders all Bufferer in the given order from left to right,
|
||||||
|
// right could overlap on left ones.
|
||||||
|
func render(bs ...Bufferer) {
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
Close()
|
||||||
|
fmt.Fprintf(os.Stderr, "Captured a panic(value=%v) when rendering Bufferer. Exit termui and clean terminal...\nPrint stack trace:\n\n", e)
|
||||||
|
//debug.PrintStack()
|
||||||
|
gs, err := stack.ParseDump(bytes.NewReader(debug.Stack()), os.Stderr)
|
||||||
|
if err != nil {
|
||||||
|
debug.PrintStack()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
p := &stack.Palette{}
|
||||||
|
buckets := stack.SortBuckets(stack.Bucketize(gs, stack.AnyValue))
|
||||||
|
srcLen, pkgLen := stack.CalcLengths(buckets, false)
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
io.WriteString(os.Stdout, p.BucketHeader(&bucket, false, len(buckets) > 1))
|
||||||
|
io.WriteString(os.Stdout, p.StackLines(&bucket.Signature, srcLen, pkgLen, false))
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for _, b := range bs {
|
||||||
|
|
||||||
|
buf := b.Buffer()
|
||||||
|
// set cels in buf
|
||||||
|
for p, c := range buf.CellMap {
|
||||||
|
if p.In(buf.Area) {
|
||||||
|
|
||||||
|
tm.SetCell(p.X, p.Y, c.Ch, toTmAttr(c.Fg), toTmAttr(c.Bg))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLock.Lock()
|
||||||
|
// render
|
||||||
|
tm.Flush()
|
||||||
|
renderLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clear() {
|
||||||
|
tm.Clear(tm.ColorDefault, toTmAttr(ThemeAttr("bg")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearArea(r image.Rectangle, bg Attribute) {
|
||||||
|
for i := r.Min.X; i < r.Max.X; i++ {
|
||||||
|
for j := r.Min.Y; j < r.Max.Y; j++ {
|
||||||
|
tm.SetCell(i, j, ' ', tm.ColorDefault, toTmAttr(bg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearArea(r image.Rectangle, bg Attribute) {
|
||||||
|
clearArea(r, bg)
|
||||||
|
tm.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
var renderJobs chan []Bufferer
|
||||||
|
|
||||||
|
func Render(bs ...Bufferer) {
|
||||||
|
//go func() { renderJobs <- bs }()
|
||||||
|
renderJobs <- bs
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃. The data points should be non-negative integers.
|
||||||
|
/*
|
||||||
|
data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1}
|
||||||
|
spl := termui.NewSparkline()
|
||||||
|
spl.Data = data
|
||||||
|
spl.Title = "Sparkline 0"
|
||||||
|
spl.LineColor = termui.ColorGreen
|
||||||
|
*/
|
||||||
|
type Sparkline struct {
|
||||||
|
Data []int
|
||||||
|
Height int
|
||||||
|
Title string
|
||||||
|
TitleColor Attribute
|
||||||
|
LineColor Attribute
|
||||||
|
displayHeight int
|
||||||
|
scale float32
|
||||||
|
max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparklines is a renderable widget which groups together the given sparklines.
|
||||||
|
/*
|
||||||
|
spls := termui.NewSparklines(spl0,spl1,spl2) //...
|
||||||
|
spls.Height = 2
|
||||||
|
spls.Width = 20
|
||||||
|
*/
|
||||||
|
type Sparklines struct {
|
||||||
|
Block
|
||||||
|
Lines []Sparkline
|
||||||
|
displayLines int
|
||||||
|
displayWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
|
// Add appends a given Sparkline to s *Sparklines.
|
||||||
|
func (s *Sparklines) Add(sl Sparkline) {
|
||||||
|
s.Lines = append(s.Lines, sl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines.
|
||||||
|
func NewSparkline() Sparkline {
|
||||||
|
return Sparkline{
|
||||||
|
Height: 1,
|
||||||
|
TitleColor: ThemeAttr("sparkline.title.fg"),
|
||||||
|
LineColor: ThemeAttr("sparkline.line.fg")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later.
|
||||||
|
func NewSparklines(ss ...Sparkline) *Sparklines {
|
||||||
|
s := &Sparklines{Block: *NewBlock(), Lines: ss}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sl *Sparklines) update() {
|
||||||
|
for i, v := range sl.Lines {
|
||||||
|
if v.Title == "" {
|
||||||
|
sl.Lines[i].displayHeight = v.Height
|
||||||
|
} else {
|
||||||
|
sl.Lines[i].displayHeight = v.Height + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sl.displayWidth = sl.innerArea.Dx()
|
||||||
|
|
||||||
|
// get how many lines gotta display
|
||||||
|
h := 0
|
||||||
|
sl.displayLines = 0
|
||||||
|
for _, v := range sl.Lines {
|
||||||
|
if h+v.displayHeight <= sl.innerArea.Dy() {
|
||||||
|
sl.displayLines++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
h += v.displayHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < sl.displayLines; i++ {
|
||||||
|
data := sl.Lines[i].Data
|
||||||
|
|
||||||
|
max := 0
|
||||||
|
for _, v := range data {
|
||||||
|
if max < v {
|
||||||
|
max = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sl.Lines[i].max = max
|
||||||
|
if max != 0 {
|
||||||
|
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max)
|
||||||
|
} else { // when all negative
|
||||||
|
sl.Lines[i].scale = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer implements Bufferer interface.
|
||||||
|
func (sl *Sparklines) Buffer() Buffer {
|
||||||
|
buf := sl.Block.Buffer()
|
||||||
|
sl.update()
|
||||||
|
|
||||||
|
oftY := 0
|
||||||
|
for i := 0; i < sl.displayLines; i++ {
|
||||||
|
l := sl.Lines[i]
|
||||||
|
data := l.Data
|
||||||
|
|
||||||
|
if len(data) > sl.innerArea.Dx() {
|
||||||
|
data = data[len(data)-sl.innerArea.Dx():]
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.Title != "" {
|
||||||
|
rs := trimStr2Runes(l.Title, sl.innerArea.Dx())
|
||||||
|
oftX := 0
|
||||||
|
for _, v := range rs {
|
||||||
|
w := charWidth(v)
|
||||||
|
c := Cell{
|
||||||
|
Ch: v,
|
||||||
|
Fg: l.TitleColor,
|
||||||
|
Bg: sl.Bg,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + oftX
|
||||||
|
y := sl.innerArea.Min.Y + oftY
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
oftX += w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, v := range data {
|
||||||
|
// display height of the data point, zero when data is negative
|
||||||
|
h := int(float32(v)*l.scale + 0.5)
|
||||||
|
if v < 0 {
|
||||||
|
h = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
barCnt := h / 8
|
||||||
|
barMod := h % 8
|
||||||
|
for jj := 0; jj < barCnt; jj++ {
|
||||||
|
c := Cell{
|
||||||
|
Ch: ' ', // => sparks[7]
|
||||||
|
Bg: l.LineColor,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + j
|
||||||
|
y := sl.innerArea.Min.Y + oftY + l.Height - jj
|
||||||
|
|
||||||
|
//p.Bg = sl.BgColor
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
if barMod != 0 {
|
||||||
|
c := Cell{
|
||||||
|
Ch: sparks[barMod-1],
|
||||||
|
Fg: l.LineColor,
|
||||||
|
Bg: sl.Bg,
|
||||||
|
}
|
||||||
|
x := sl.innerArea.Min.X + j
|
||||||
|
y := sl.innerArea.Min.Y + oftY + l.Height - barCnt
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oftY += l.displayHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
/* Table is like:
|
||||||
|
|
||||||
|
┌Awesome Table ────────────────────────────────────────────────┐
|
||||||
|
│ Col0 | Col1 | Col2 | Col3 | Col4 | Col5 | Col6 |
|
||||||
|
│──────────────────────────────────────────────────────────────│
|
||||||
|
│ Some Item #1 | AAA | 123 | CCCCC | EEEEE | GGGGG | IIIII |
|
||||||
|
│──────────────────────────────────────────────────────────────│
|
||||||
|
│ Some Item #2 | BBB | 456 | DDDDD | FFFFF | HHHHH | JJJJJ |
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
Datapoints are a two dimensional array of strings: [][]string
|
||||||
|
|
||||||
|
Example:
|
||||||
|
data := [][]string{
|
||||||
|
{"Col0", "Col1", "Col3", "Col4", "Col5", "Col6"},
|
||||||
|
{"Some Item #1", "AAA", "123", "CCCCC", "EEEEE", "GGGGG", "IIIII"},
|
||||||
|
{"Some Item #2", "BBB", "456", "DDDDD", "FFFFF", "HHHHH", "JJJJJ"},
|
||||||
|
}
|
||||||
|
|
||||||
|
table := termui.NewTable()
|
||||||
|
table.Rows = data // type [][]string
|
||||||
|
table.FgColor = termui.ColorWhite
|
||||||
|
table.BgColor = termui.ColorDefault
|
||||||
|
table.Height = 7
|
||||||
|
table.Width = 62
|
||||||
|
table.Y = 0
|
||||||
|
table.X = 0
|
||||||
|
table.Border = true
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Table tracks all the attributes of a Table instance
|
||||||
|
type Table struct {
|
||||||
|
Block
|
||||||
|
Rows [][]string
|
||||||
|
CellWidth []int
|
||||||
|
FgColor Attribute
|
||||||
|
BgColor Attribute
|
||||||
|
FgColors []Attribute
|
||||||
|
BgColors []Attribute
|
||||||
|
Separator bool
|
||||||
|
TextAlign Align
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTable returns a new Table instance
|
||||||
|
func NewTable() *Table {
|
||||||
|
table := &Table{Block: *NewBlock()}
|
||||||
|
table.FgColor = ColorWhite
|
||||||
|
table.BgColor = ColorDefault
|
||||||
|
table.Separator = true
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// CellsWidth calculates the width of a cell array and returns an int
|
||||||
|
func cellsWidth(cells []Cell) int {
|
||||||
|
width := 0
|
||||||
|
for _, c := range cells {
|
||||||
|
width += c.Width()
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analysis generates and returns an array of []Cell that represent all columns in the Table
|
||||||
|
func (table *Table) Analysis() [][]Cell {
|
||||||
|
var rowCells [][]Cell
|
||||||
|
length := len(table.Rows)
|
||||||
|
if length < 1 {
|
||||||
|
return rowCells
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(table.FgColors) == 0 {
|
||||||
|
table.FgColors = make([]Attribute, len(table.Rows))
|
||||||
|
}
|
||||||
|
if len(table.BgColors) == 0 {
|
||||||
|
table.BgColors = make([]Attribute, len(table.Rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
cellWidths := make([]int, len(table.Rows[0]))
|
||||||
|
|
||||||
|
for y, row := range table.Rows {
|
||||||
|
if table.FgColors[y] == 0 {
|
||||||
|
table.FgColors[y] = table.FgColor
|
||||||
|
}
|
||||||
|
if table.BgColors[y] == 0 {
|
||||||
|
table.BgColors[y] = table.BgColor
|
||||||
|
}
|
||||||
|
for x, str := range row {
|
||||||
|
cells := DefaultTxBuilder.Build(str, table.FgColors[y], table.BgColors[y])
|
||||||
|
cw := cellsWidth(cells)
|
||||||
|
if cellWidths[x] < cw {
|
||||||
|
cellWidths[x] = cw
|
||||||
|
}
|
||||||
|
rowCells = append(rowCells, cells)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table.CellWidth = cellWidths
|
||||||
|
return rowCells
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize calculates the table size and sets the internal value
|
||||||
|
func (table *Table) SetSize() {
|
||||||
|
length := len(table.Rows)
|
||||||
|
if table.Separator {
|
||||||
|
table.Height = length*2 + 1
|
||||||
|
} else {
|
||||||
|
table.Height = length + 2
|
||||||
|
}
|
||||||
|
table.Width = 2
|
||||||
|
if length != 0 {
|
||||||
|
for _, cellWidth := range table.CellWidth {
|
||||||
|
table.Width += cellWidth + 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculatePosition ...
|
||||||
|
func (table *Table) CalculatePosition(x int, y int, coordinateX *int, coordinateY *int, cellStart *int) {
|
||||||
|
if table.Separator {
|
||||||
|
*coordinateY = table.innerArea.Min.Y + y*2
|
||||||
|
} else {
|
||||||
|
*coordinateY = table.innerArea.Min.Y + y
|
||||||
|
}
|
||||||
|
if x == 0 {
|
||||||
|
*cellStart = table.innerArea.Min.X
|
||||||
|
} else {
|
||||||
|
*cellStart += table.CellWidth[x-1] + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
switch table.TextAlign {
|
||||||
|
case AlignRight:
|
||||||
|
*coordinateX = *cellStart + (table.CellWidth[x] - len(table.Rows[y][x])) + 2
|
||||||
|
case AlignCenter:
|
||||||
|
*coordinateX = *cellStart + (table.CellWidth[x]-len(table.Rows[y][x]))/2 + 2
|
||||||
|
default:
|
||||||
|
*coordinateX = *cellStart + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer ...
|
||||||
|
func (table *Table) Buffer() Buffer {
|
||||||
|
buffer := table.Block.Buffer()
|
||||||
|
rowCells := table.Analysis()
|
||||||
|
pointerX := table.innerArea.Min.X + 2
|
||||||
|
pointerY := table.innerArea.Min.Y
|
||||||
|
borderPointerX := table.innerArea.Min.X
|
||||||
|
for y, row := range table.Rows {
|
||||||
|
for x := range row {
|
||||||
|
table.CalculatePosition(x, y, &pointerX, &pointerY, &borderPointerX)
|
||||||
|
background := DefaultTxBuilder.Build(strings.Repeat(" ", table.CellWidth[x]+3), table.BgColors[y], table.BgColors[y])
|
||||||
|
cells := rowCells[y*len(row)+x]
|
||||||
|
for i, back := range background {
|
||||||
|
buffer.Set(borderPointerX+i, pointerY, back)
|
||||||
|
}
|
||||||
|
|
||||||
|
coordinateX := pointerX
|
||||||
|
for _, printer := range cells {
|
||||||
|
buffer.Set(coordinateX, pointerY, printer)
|
||||||
|
coordinateX += printer.Width()
|
||||||
|
}
|
||||||
|
|
||||||
|
if x != 0 {
|
||||||
|
dividors := DefaultTxBuilder.Build("|", table.FgColors[y], table.BgColors[y])
|
||||||
|
for _, dividor := range dividors {
|
||||||
|
buffer.Set(borderPointerX, pointerY, dividor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.Separator {
|
||||||
|
border := DefaultTxBuilder.Build(strings.Repeat("─", table.Width-2), table.FgColor, table.BgColor)
|
||||||
|
for i, cell := range border {
|
||||||
|
buffer.Set(i+1, pointerY+1, cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
|
@ -0,0 +1,278 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-wordwrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextBuilder is a minimal interface to produce text []Cell using specific syntax (markdown).
|
||||||
|
type TextBuilder interface {
|
||||||
|
Build(s string, fg, bg Attribute) []Cell
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTxBuilder is set to be MarkdownTxBuilder.
|
||||||
|
var DefaultTxBuilder = NewMarkdownTxBuilder()
|
||||||
|
|
||||||
|
// MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
|
||||||
|
type MarkdownTxBuilder struct {
|
||||||
|
baseFg Attribute
|
||||||
|
baseBg Attribute
|
||||||
|
plainTx []rune
|
||||||
|
markers []marker
|
||||||
|
}
|
||||||
|
|
||||||
|
type marker struct {
|
||||||
|
st int
|
||||||
|
ed int
|
||||||
|
fg Attribute
|
||||||
|
bg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorMap = map[string]Attribute{
|
||||||
|
"red": ColorRed,
|
||||||
|
"blue": ColorBlue,
|
||||||
|
"black": ColorBlack,
|
||||||
|
"cyan": ColorCyan,
|
||||||
|
"yellow": ColorYellow,
|
||||||
|
"white": ColorWhite,
|
||||||
|
"default": ColorDefault,
|
||||||
|
"green": ColorGreen,
|
||||||
|
"magenta": ColorMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
var attrMap = map[string]Attribute{
|
||||||
|
"bold": AttrBold,
|
||||||
|
"underline": AttrUnderline,
|
||||||
|
"reverse": AttrReverse,
|
||||||
|
}
|
||||||
|
|
||||||
|
func rmSpc(s string) string {
|
||||||
|
reg := regexp.MustCompile(`\s+`)
|
||||||
|
return reg.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
|
||||||
|
func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
|
||||||
|
fg := mtb.baseFg
|
||||||
|
bg := mtb.baseBg
|
||||||
|
|
||||||
|
updateAttr := func(a Attribute, attrs []string) Attribute {
|
||||||
|
for _, s := range attrs {
|
||||||
|
// replace the color
|
||||||
|
if c, ok := colorMap[s]; ok {
|
||||||
|
a &= 0xFF00 // erase clr 0 ~ 8 bits
|
||||||
|
a |= c // set clr
|
||||||
|
}
|
||||||
|
// add attrs
|
||||||
|
if c, ok := attrMap[s]; ok {
|
||||||
|
a |= c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := strings.Split(s, ",")
|
||||||
|
fgs := []string{}
|
||||||
|
bgs := []string{}
|
||||||
|
for _, v := range ss {
|
||||||
|
subs := strings.Split(v, "-")
|
||||||
|
if len(subs) > 1 {
|
||||||
|
if subs[0] == "fg" {
|
||||||
|
fgs = append(fgs, subs[1])
|
||||||
|
}
|
||||||
|
if subs[0] == "bg" {
|
||||||
|
bgs = append(bgs, subs[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fg = updateAttr(fg, fgs)
|
||||||
|
bg = updateAttr(bg, bgs)
|
||||||
|
return fg, bg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mtb *MarkdownTxBuilder) reset() {
|
||||||
|
mtb.plainTx = []rune{}
|
||||||
|
mtb.markers = []marker{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse streams and parses text into normalized text and render sequence.
|
||||||
|
func (mtb *MarkdownTxBuilder) parse(str string) {
|
||||||
|
rs := str2runes(str)
|
||||||
|
normTx := []rune{}
|
||||||
|
square := []rune{}
|
||||||
|
brackt := []rune{}
|
||||||
|
accSquare := false
|
||||||
|
accBrackt := false
|
||||||
|
cntSquare := 0
|
||||||
|
|
||||||
|
reset := func() {
|
||||||
|
square = []rune{}
|
||||||
|
brackt = []rune{}
|
||||||
|
accSquare = false
|
||||||
|
accBrackt = false
|
||||||
|
cntSquare = 0
|
||||||
|
}
|
||||||
|
// pipe stacks into normTx and clear
|
||||||
|
rollback := func() {
|
||||||
|
normTx = append(normTx, square...)
|
||||||
|
normTx = append(normTx, brackt...)
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
// chop first and last
|
||||||
|
chop := func(s []rune) []rune {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range rs {
|
||||||
|
switch {
|
||||||
|
// stacking brackt
|
||||||
|
case accBrackt:
|
||||||
|
brackt = append(brackt, r)
|
||||||
|
if ')' == r {
|
||||||
|
fg, bg := mtb.readAttr(string(chop(brackt)))
|
||||||
|
st := len(normTx)
|
||||||
|
ed := len(normTx) + len(square) - 2
|
||||||
|
mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
|
||||||
|
normTx = append(normTx, chop(square)...)
|
||||||
|
reset()
|
||||||
|
} else if i+1 == len(rs) {
|
||||||
|
rollback()
|
||||||
|
}
|
||||||
|
// stacking square
|
||||||
|
case accSquare:
|
||||||
|
switch {
|
||||||
|
// squares closed and followed by a '('
|
||||||
|
case cntSquare == 0 && '(' == r:
|
||||||
|
accBrackt = true
|
||||||
|
brackt = append(brackt, '(')
|
||||||
|
// squares closed but not followed by a '('
|
||||||
|
case cntSquare == 0:
|
||||||
|
rollback()
|
||||||
|
if '[' == r {
|
||||||
|
accSquare = true
|
||||||
|
cntSquare = 1
|
||||||
|
brackt = append(brackt, '[')
|
||||||
|
} else {
|
||||||
|
normTx = append(normTx, r)
|
||||||
|
}
|
||||||
|
// hit the end
|
||||||
|
case i+1 == len(rs):
|
||||||
|
square = append(square, r)
|
||||||
|
rollback()
|
||||||
|
case '[' == r:
|
||||||
|
cntSquare++
|
||||||
|
square = append(square, '[')
|
||||||
|
case ']' == r:
|
||||||
|
cntSquare--
|
||||||
|
square = append(square, ']')
|
||||||
|
// normal char
|
||||||
|
default:
|
||||||
|
square = append(square, r)
|
||||||
|
}
|
||||||
|
// stacking normTx
|
||||||
|
default:
|
||||||
|
if '[' == r {
|
||||||
|
accSquare = true
|
||||||
|
cntSquare = 1
|
||||||
|
square = append(square, '[')
|
||||||
|
} else {
|
||||||
|
normTx = append(normTx, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mtb.plainTx = normTx
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapTx(cs []Cell, wl int) []Cell {
|
||||||
|
tmpCell := make([]Cell, len(cs))
|
||||||
|
copy(tmpCell, cs)
|
||||||
|
|
||||||
|
// get the plaintext
|
||||||
|
plain := CellsToStr(cs)
|
||||||
|
|
||||||
|
// wrap
|
||||||
|
plainWrapped := wordwrap.WrapString(plain, uint(wl))
|
||||||
|
|
||||||
|
// find differences and insert
|
||||||
|
finalCell := tmpCell // finalcell will get the inserts and is what is returned
|
||||||
|
|
||||||
|
plainRune := []rune(plain)
|
||||||
|
plainWrappedRune := []rune(plainWrapped)
|
||||||
|
trigger := "go"
|
||||||
|
plainRuneNew := plainRune
|
||||||
|
|
||||||
|
for trigger != "stop" {
|
||||||
|
plainRune = plainRuneNew
|
||||||
|
for i := range plainRune {
|
||||||
|
if plainRune[i] == plainWrappedRune[i] {
|
||||||
|
trigger = "stop"
|
||||||
|
} else if plainRune[i] != plainWrappedRune[i] && plainWrappedRune[i] == 10 {
|
||||||
|
trigger = "go"
|
||||||
|
cell := Cell{10, 0, 0}
|
||||||
|
j := i - 0
|
||||||
|
|
||||||
|
// insert a cell into the []Cell in correct position
|
||||||
|
tmpCell[i] = cell
|
||||||
|
|
||||||
|
// insert the newline into plain so we avoid indexing errors
|
||||||
|
plainRuneNew = append(plainRune, 10)
|
||||||
|
copy(plainRuneNew[j+1:], plainRuneNew[j:])
|
||||||
|
plainRuneNew[j] = plainWrappedRune[j]
|
||||||
|
|
||||||
|
// restart the inner for loop until plain and plain wrapped are
|
||||||
|
// the same; yeah, it's inefficient, but the text amounts
|
||||||
|
// should be small
|
||||||
|
break
|
||||||
|
|
||||||
|
} else if plainRune[i] != plainWrappedRune[i] &&
|
||||||
|
plainWrappedRune[i-1] == 10 && // if the prior rune is a newline
|
||||||
|
plainRune[i] == 32 { // and this rune is a space
|
||||||
|
trigger = "go"
|
||||||
|
// need to delete plainRune[i] because it gets rid of an extra
|
||||||
|
// space
|
||||||
|
plainRuneNew = append(plainRune[:i], plainRune[i+1:]...)
|
||||||
|
break
|
||||||
|
|
||||||
|
} else {
|
||||||
|
trigger = "stop" // stops the outer for loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalCell = tmpCell
|
||||||
|
|
||||||
|
return finalCell
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build implements TextBuilder interface.
|
||||||
|
func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
|
||||||
|
mtb.baseFg = fg
|
||||||
|
mtb.baseBg = bg
|
||||||
|
mtb.reset()
|
||||||
|
mtb.parse(s)
|
||||||
|
cs := make([]Cell, len(mtb.plainTx))
|
||||||
|
for i := range cs {
|
||||||
|
cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
|
||||||
|
}
|
||||||
|
for _, mrk := range mtb.markers {
|
||||||
|
for i := mrk.st; i < mrk.ed; i++ {
|
||||||
|
cs[i].Fg = mrk.fg
|
||||||
|
cs[i].Bg = mrk.bg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
|
||||||
|
func NewMarkdownTxBuilder() TextBuilder {
|
||||||
|
return MarkdownTxBuilder{}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestReadAttr(t *testing.T) {
|
||||||
|
m := MarkdownTxBuilder{}
|
||||||
|
m.baseFg = ColorCyan | AttrUnderline
|
||||||
|
m.baseBg = ColorBlue | AttrBold
|
||||||
|
fg, bg := m.readAttr("fg-red,bg-reverse")
|
||||||
|
if fg != ColorRed|AttrUnderline || bg != ColorBlue|AttrBold|AttrReverse {
|
||||||
|
t.Error("readAttr failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMTBParse(t *testing.T) {
|
||||||
|
/*
|
||||||
|
str := func(cs []Cell) string {
|
||||||
|
rs := make([]rune, len(cs))
|
||||||
|
for i := range cs {
|
||||||
|
rs[i] = cs[i].Ch
|
||||||
|
}
|
||||||
|
return string(rs)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
tbls := [][]string{
|
||||||
|
{"hello world", "hello world"},
|
||||||
|
{"[hello](fg-red) world", "hello world"},
|
||||||
|
{"[[hello]](bg-red) world", "[hello] world"},
|
||||||
|
{"[1] hello world", "[1] hello world"},
|
||||||
|
{"[[1]](bg-white) [hello] world", "[1] [hello] world"},
|
||||||
|
{"[hello world]", "[hello world]"},
|
||||||
|
{"", ""},
|
||||||
|
{"[hello world)", "[hello world)"},
|
||||||
|
{"[0] [hello](bg-red)[ world](fg-blue)!", "[0] hello world!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := MarkdownTxBuilder{}
|
||||||
|
m.baseFg = ColorWhite
|
||||||
|
m.baseBg = ColorDefault
|
||||||
|
for _, s := range tbls {
|
||||||
|
m.reset()
|
||||||
|
m.parse(s[0])
|
||||||
|
res := string(m.plainTx)
|
||||||
|
if s[1] != res {
|
||||||
|
t.Errorf("\ninput :%s\nshould:%s\noutput:%s", s[0], s[1], res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.reset()
|
||||||
|
m.parse("[0] [hello](bg-red)[ world](fg-blue)")
|
||||||
|
if len(m.markers) != 2 &&
|
||||||
|
m.markers[0].st == 4 &&
|
||||||
|
m.markers[0].ed == 11 &&
|
||||||
|
m.markers[0].fg == ColorWhite &&
|
||||||
|
m.markers[0].bg == ColorRed {
|
||||||
|
t.Error("markers dismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
m2 := NewMarkdownTxBuilder()
|
||||||
|
cs := m2.Build("[0] [hellob-e) wrd]fgblue)!", ColorWhite, ColorBlack)
|
||||||
|
cs = m2.Build("[0] [hello](bg-red) [world](fg-blue)!", ColorWhite, ColorBlack)
|
||||||
|
if cs[4].Ch != 'h' && cs[4].Bg != ColorRed && cs[4].Fg != ColorWhite {
|
||||||
|
t.Error("dismatch in Build")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
/*
|
||||||
|
// A ColorScheme represents the current look-and-feel of the dashboard.
|
||||||
|
type ColorScheme struct {
|
||||||
|
BodyBg Attribute
|
||||||
|
BlockBg Attribute
|
||||||
|
HasBorder bool
|
||||||
|
BorderFg Attribute
|
||||||
|
BorderBg Attribute
|
||||||
|
BorderLabelTextFg Attribute
|
||||||
|
BorderLabelTextBg Attribute
|
||||||
|
ParTextFg Attribute
|
||||||
|
ParTextBg Attribute
|
||||||
|
SparklineLine Attribute
|
||||||
|
SparklineTitle Attribute
|
||||||
|
GaugeBar Attribute
|
||||||
|
GaugePercent Attribute
|
||||||
|
LineChartLine Attribute
|
||||||
|
LineChartAxes Attribute
|
||||||
|
ListItemFg Attribute
|
||||||
|
ListItemBg Attribute
|
||||||
|
BarChartBar Attribute
|
||||||
|
BarChartText Attribute
|
||||||
|
BarChartNum Attribute
|
||||||
|
MBarChartBar Attribute
|
||||||
|
MBarChartText Attribute
|
||||||
|
MBarChartNum Attribute
|
||||||
|
TabActiveBg Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// default color scheme depends on the user's terminal setting.
|
||||||
|
var themeDefault = ColorScheme{HasBorder: true}
|
||||||
|
|
||||||
|
var themeHelloWorld = ColorScheme{
|
||||||
|
BodyBg: ColorBlack,
|
||||||
|
BlockBg: ColorBlack,
|
||||||
|
HasBorder: true,
|
||||||
|
BorderFg: ColorWhite,
|
||||||
|
BorderBg: ColorBlack,
|
||||||
|
BorderLabelTextBg: ColorBlack,
|
||||||
|
BorderLabelTextFg: ColorGreen,
|
||||||
|
ParTextBg: ColorBlack,
|
||||||
|
ParTextFg: ColorWhite,
|
||||||
|
SparklineLine: ColorMagenta,
|
||||||
|
SparklineTitle: ColorWhite,
|
||||||
|
GaugeBar: ColorRed,
|
||||||
|
GaugePercent: ColorWhite,
|
||||||
|
LineChartLine: ColorYellow | AttrBold,
|
||||||
|
LineChartAxes: ColorWhite,
|
||||||
|
ListItemBg: ColorBlack,
|
||||||
|
ListItemFg: ColorYellow,
|
||||||
|
BarChartBar: ColorRed,
|
||||||
|
BarChartNum: ColorWhite,
|
||||||
|
BarChartText: ColorCyan,
|
||||||
|
MBarChartBar: ColorRed,
|
||||||
|
MBarChartNum: ColorWhite,
|
||||||
|
MBarChartText: ColorCyan,
|
||||||
|
TabActiveBg: ColorMagenta,
|
||||||
|
}
|
||||||
|
|
||||||
|
var theme = themeDefault // global dep
|
||||||
|
|
||||||
|
// Theme returns the currently used theme.
|
||||||
|
func Theme() ColorScheme {
|
||||||
|
return theme
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets a new, custom theme.
|
||||||
|
func SetTheme(newTheme ColorScheme) {
|
||||||
|
theme = newTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseTheme sets a predefined scheme. Currently available: "hello-world" and
|
||||||
|
// "black-and-white".
|
||||||
|
func UseTheme(th string) {
|
||||||
|
switch th {
|
||||||
|
case "helloworld":
|
||||||
|
theme = themeHelloWorld
|
||||||
|
default:
|
||||||
|
theme = themeDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
var ColorMap = map[string]Attribute{
|
||||||
|
"fg": ColorWhite,
|
||||||
|
"bg": ColorDefault,
|
||||||
|
"border.fg": ColorWhite,
|
||||||
|
"label.fg": ColorGreen,
|
||||||
|
"par.fg": ColorYellow,
|
||||||
|
"par.label.bg": ColorWhite,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ThemeAttr(name string) Attribute {
|
||||||
|
return lookUpAttr(ColorMap, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookUpAttr(clrmap map[string]Attribute, name string) Attribute {
|
||||||
|
|
||||||
|
a, ok := clrmap[name]
|
||||||
|
if ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := strings.Split(name, ".")
|
||||||
|
for i := range ns {
|
||||||
|
nn := strings.Join(ns[i:len(ns)], ".")
|
||||||
|
a, ok = ColorMap[nn]
|
||||||
|
if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0<=r,g,b <= 5
|
||||||
|
func ColorRGB(r, g, b int) Attribute {
|
||||||
|
within := func(n int) int {
|
||||||
|
if n < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 5 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
r, b, g = within(r), within(b), within(g)
|
||||||
|
return Attribute(0x0f + 36*r + 6*g + b)
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var cmap = map[string]Attribute{
|
||||||
|
"fg": ColorWhite,
|
||||||
|
"bg": ColorDefault,
|
||||||
|
"border.fg": ColorWhite,
|
||||||
|
"label.fg": ColorGreen,
|
||||||
|
"par.fg": ColorYellow,
|
||||||
|
"par.label.bg": ColorWhite,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoopUpAttr(t *testing.T) {
|
||||||
|
tbl := []struct {
|
||||||
|
name string
|
||||||
|
should Attribute
|
||||||
|
}{
|
||||||
|
{"par.label.bg", ColorWhite},
|
||||||
|
{"par.label.fg", ColorGreen},
|
||||||
|
{"par.bg", ColorDefault},
|
||||||
|
{"bar.border.fg", ColorWhite},
|
||||||
|
{"bar.label.bg", ColorDefault},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range tbl {
|
||||||
|
if lookUpAttr(cmap, v.name) != v.should {
|
||||||
|
t.Error(v.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT license that can
|
||||||
|
// be found in the LICENSE file.
|
||||||
|
|
||||||
|
package termui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// event mixins
|
||||||
|
type WgtMgr map[string]WgtInfo
|
||||||
|
|
||||||
|
type WgtInfo struct {
|
||||||
|
Handlers map[string]func(Event)
|
||||||
|
WgtRef Widget
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Widget interface {
|
||||||
|
Id() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWgtInfo(wgt Widget) WgtInfo {
|
||||||
|
return WgtInfo{
|
||||||
|
Handlers: make(map[string]func(Event)),
|
||||||
|
WgtRef: wgt,
|
||||||
|
Id: wgt.Id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWgtMgr() WgtMgr {
|
||||||
|
wm := WgtMgr(make(map[string]WgtInfo))
|
||||||
|
return wm
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) AddWgt(wgt Widget) {
|
||||||
|
wm[wgt.Id()] = NewWgtInfo(wgt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgt(wgt Widget) {
|
||||||
|
wm.RmWgtById(wgt.Id())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgtById(id string) {
|
||||||
|
delete(wm, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) AddWgtHandler(id, path string, h func(Event)) {
|
||||||
|
if w, ok := wm[id]; ok {
|
||||||
|
w.Handlers[path] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) RmWgtHandler(id, path string) {
|
||||||
|
if w, ok := wm[id]; ok {
|
||||||
|
delete(w.Handlers, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter struct {
|
||||||
|
sync.RWMutex
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenId() string {
|
||||||
|
counter.Lock()
|
||||||
|
defer counter.Unlock()
|
||||||
|
|
||||||
|
counter.count += 1
|
||||||
|
return fmt.Sprintf("%d", counter.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wm WgtMgr) WgtHandlersHook() func(Event) {
|
||||||
|
return func(e Event) {
|
||||||
|
for _, v := range wm {
|
||||||
|
if k := findMatch(v.Handlers, e.Path); k != "" {
|
||||||
|
v.Handlers[k](e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultWgtMgr WgtMgr
|
||||||
|
|
||||||
|
func (b *Block) Handle(path string, handler func(Event)) {
|
||||||
|
if _, ok := DefaultWgtMgr[b.Id()]; !ok {
|
||||||
|
DefaultWgtMgr.AddWgt(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultWgtMgr.AddWgtHandler(b.Id(), path, handler)
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
language: go
|
|
||||||
sudo: false
|
|
||||||
install:
|
|
||||||
- go get ./...
|
|
||||||
go:
|
|
||||||
- 1.4
|
|
||||||
- tip
|
|
|
@ -1,10 +0,0 @@
|
||||||
MIT License
|
|
||||||
===========
|
|
||||||
|
|
||||||
Copyright (c) 2015, Greg Osuri
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,31 +0,0 @@
|
||||||
# uilive [![GoDoc](https://godoc.org/github.com/gosuri/uilive?status.svg)](https://godoc.org/github.com/gosuri/uilive) [![Build Status](https://travis-ci.org/gosuri/uilive.svg?branch=master)](https://travis-ci.org/gosuri/uilive)
|
|
||||||
|
|
||||||
uilive is a go library for updating terminal output in realtime. It provides a buffered [io.Writer](https://golang.org/pkg/io/#Writer) that is flushed at a timed interval. uilive powers [uiprogress](https://github.com/gosuri/uiprogress).
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
Calling `uilive.New()` will create a new writer. To start rendering, simply call `writer.Start()` and update the ui by writing to the `writer`. Full source for the below example is in [example/main.go](example/main.go).
|
|
||||||
|
|
||||||
```go
|
|
||||||
writer := uilive.New()
|
|
||||||
// start listening for updates and render
|
|
||||||
writer.Start()
|
|
||||||
|
|
||||||
for i := 0; i <= 100; i++ {
|
|
||||||
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
|
||||||
time.Sleep(time.Millisecond * 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
|
||||||
writer.Stop() // flush and stop rendering
|
|
||||||
```
|
|
||||||
|
|
||||||
The above will render
|
|
||||||
|
|
||||||
![example](doc/example.gif)
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ go get -v github.com/gosuri/uilive
|
|
||||||
```
|
|
|
@ -1,2 +0,0 @@
|
||||||
// Package uilive provides a writer that live updates the terminal. It provides a buffered io.Writer that is flushed at a timed interval.
|
|
||||||
package uilive
|
|
Binary file not shown.
Before Width: | Height: | Size: 156 KiB |
|
@ -1,26 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gosuri/uilive"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
writer := uilive.New()
|
|
||||||
|
|
||||||
// start listening for updates and render
|
|
||||||
writer.Start()
|
|
||||||
|
|
||||||
for _, f := range []string{"Foo.zip", "Bar.iso"} {
|
|
||||||
for i := 0; i <= 50; i++ {
|
|
||||||
fmt.Fprintf(writer, "Downloading %s.. (%d/%d) GB\n", f, i, 50)
|
|
||||||
time.Sleep(time.Millisecond * 25)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(writer.Bypass(), "Downloaded %s\n", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
|
||||||
writer.Stop() // flush and stop rendering
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package uilive_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gosuri/uilive"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Example() {
|
|
||||||
writer := uilive.New()
|
|
||||||
|
|
||||||
// start listening to updates and render
|
|
||||||
writer.Start()
|
|
||||||
|
|
||||||
for i := 0; i <= 100; i++ {
|
|
||||||
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
|
||||||
time.Sleep(time.Millisecond * 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
|
||||||
writer.Stop() // flush and stop rendering
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
package uilive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ESC is the ASCII code for escape character
|
|
||||||
const ESC = 27
|
|
||||||
|
|
||||||
// RefreshInterval is the default refresh interval to update the ui
|
|
||||||
var RefreshInterval = time.Millisecond
|
|
||||||
|
|
||||||
// Out is the default output writer for the Writer
|
|
||||||
var Out = os.Stdout
|
|
||||||
|
|
||||||
// ErrClosedPipe is the error returned when trying to writer is not listening
|
|
||||||
var ErrClosedPipe = errors.New("uilive: read/write on closed pipe")
|
|
||||||
|
|
||||||
// FdWriter is a writer with a file descriptor.
|
|
||||||
type FdWriter interface {
|
|
||||||
io.Writer
|
|
||||||
Fd() uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer is a buffered the writer that updates the terminal. The contents of writer will be flushed on a timed interval or when Flush is called.
|
|
||||||
type Writer struct {
|
|
||||||
// Out is the writer to write to
|
|
||||||
Out io.Writer
|
|
||||||
|
|
||||||
// RefreshInterval is the time the UI sould refresh
|
|
||||||
RefreshInterval time.Duration
|
|
||||||
|
|
||||||
ticker *time.Ticker
|
|
||||||
tdone chan bool
|
|
||||||
|
|
||||||
buf bytes.Buffer
|
|
||||||
mtx *sync.Mutex
|
|
||||||
lineCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
type bypass struct {
|
|
||||||
writer *Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new Writer with defaults
|
|
||||||
func New() *Writer {
|
|
||||||
return &Writer{
|
|
||||||
Out: Out,
|
|
||||||
RefreshInterval: RefreshInterval,
|
|
||||||
|
|
||||||
mtx: &sync.Mutex{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush writes to the out and resets the buffer. It should be called after the last call to Write to ensure that any data buffered in the Writer is written to output.
|
|
||||||
// Any incomplete escape sequence at the end is considered complete for formatting purposes.
|
|
||||||
// An error is returned if the contents of the buffer cannot be written to the underlying output stream
|
|
||||||
func (w *Writer) Flush() error {
|
|
||||||
w.mtx.Lock()
|
|
||||||
defer w.mtx.Unlock()
|
|
||||||
|
|
||||||
// do nothing is buffer is empty
|
|
||||||
if len(w.buf.Bytes()) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w.clearLines()
|
|
||||||
|
|
||||||
lines := 0
|
|
||||||
for _, b := range w.buf.Bytes() {
|
|
||||||
if b == '\n' {
|
|
||||||
lines++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.lineCount = lines
|
|
||||||
_, err := w.Out.Write(w.buf.Bytes())
|
|
||||||
w.buf.Reset()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the listener in a non-blocking manner
|
|
||||||
func (w *Writer) Start() {
|
|
||||||
if w.ticker == nil {
|
|
||||||
w.ticker = time.NewTicker(w.RefreshInterval)
|
|
||||||
w.tdone = make(chan bool, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
go w.Listen()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop stops the listener that updates the terminal
|
|
||||||
func (w *Writer) Stop() {
|
|
||||||
w.Flush()
|
|
||||||
close(w.tdone)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen listens for updates to the writer's buffer and flushes to the out provided. It blocks the runtime.
|
|
||||||
func (w *Writer) Listen() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-w.ticker.C:
|
|
||||||
if w.ticker != nil {
|
|
||||||
w.Flush()
|
|
||||||
}
|
|
||||||
case <-w.tdone:
|
|
||||||
w.mtx.Lock()
|
|
||||||
w.ticker.Stop()
|
|
||||||
w.ticker = nil
|
|
||||||
w.mtx.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write save the contents of b to its buffers. The only errors returned are ones encountered while writing to the underlying buffer.
|
|
||||||
func (w *Writer) Write(b []byte) (n int, err error) {
|
|
||||||
w.mtx.Lock()
|
|
||||||
defer w.mtx.Unlock()
|
|
||||||
return w.buf.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bypass creates an io.Writer which allows non-buffered output to be written to the underlying output
|
|
||||||
func (w *Writer) Bypass() io.Writer {
|
|
||||||
return &bypass{writer: w}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bypass) Write(p []byte) (n int, err error) {
|
|
||||||
b.writer.mtx.Lock()
|
|
||||||
defer b.writer.mtx.Unlock()
|
|
||||||
|
|
||||||
b.writer.clearLines()
|
|
||||||
b.writer.lineCount = 0
|
|
||||||
return b.writer.Out.Write(p)
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package uilive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (w *Writer) clearLines() {
|
|
||||||
for i := 0; i < w.lineCount; i++ {
|
|
||||||
fmt.Fprintf(w.Out, "%c[2K", ESC) // clear the line
|
|
||||||
fmt.Fprintf(w.Out, "%c[%dA", ESC, 1) // move the cursor up
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package uilive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWriter(t *testing.T) {
|
|
||||||
w := New()
|
|
||||||
b := &bytes.Buffer{}
|
|
||||||
w.Out = b
|
|
||||||
w.Start()
|
|
||||||
for i := 0; i < 2; i++ {
|
|
||||||
fmt.Fprintln(w, "foo")
|
|
||||||
}
|
|
||||||
w.Stop()
|
|
||||||
fmt.Fprintln(b, "bar")
|
|
||||||
|
|
||||||
want := "foo\nfoo\nbar\n"
|
|
||||||
if b.String() != want {
|
|
||||||
t.Fatalf("want %q, got %q", want, b.String())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package uilive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/mattn/go-isatty"
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
||||||
|
|
||||||
var (
|
|
||||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
|
||||||
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
|
||||||
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
|
||||||
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
|
|
||||||
)
|
|
||||||
|
|
||||||
type short int16
|
|
||||||
type dword uint32
|
|
||||||
type word uint16
|
|
||||||
|
|
||||||
type coord struct {
|
|
||||||
x short
|
|
||||||
y short
|
|
||||||
}
|
|
||||||
|
|
||||||
type smallRect struct {
|
|
||||||
left short
|
|
||||||
top short
|
|
||||||
right short
|
|
||||||
bottom short
|
|
||||||
}
|
|
||||||
|
|
||||||
type consoleScreenBufferInfo struct {
|
|
||||||
size coord
|
|
||||||
cursorPosition coord
|
|
||||||
attributes word
|
|
||||||
window smallRect
|
|
||||||
maximumWindowSize coord
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) clearLines() {
|
|
||||||
f, ok := w.Out.(FdWriter)
|
|
||||||
if ok && !isatty.IsTerminal(f.Fd()) {
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
for i := 0; i < w.lineCount; i++ {
|
|
||||||
fmt.Fprintf(w.Out, "%c[%dA", ESC, 0) // move the cursor up
|
|
||||||
fmt.Fprintf(w.Out, "%c[2K\r", ESC) // clear the line
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fd := f.Fd()
|
|
||||||
var csbi consoleScreenBufferInfo
|
|
||||||
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi)))
|
|
||||||
|
|
||||||
for i := 0; i < w.lineCount; i++ {
|
|
||||||
// move the cursor up
|
|
||||||
csbi.cursorPosition.y--
|
|
||||||
procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&csbi.cursorPosition))))
|
|
||||||
// clear the line
|
|
||||||
cursor := coord{
|
|
||||||
x: csbi.window.left,
|
|
||||||
y: csbi.window.top + csbi.cursorPosition.y,
|
|
||||||
}
|
|
||||||
var count, w dword
|
|
||||||
count = dword(csbi.size.x)
|
|
||||||
procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
|
|
||||||
Usage: app [-bdsuikqs] BOOL1 [STR1] INT3... COMMAND [arg...]
|
|
||||||
|
|
||||||
App Desc
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
BOOL1 Bool Argument 1 (env $BOOL1)
|
|
||||||
BOOL2 Bool Argument 2 (default true)
|
|
||||||
BOOL3 Bool Argument 3 (env $BOOL3)
|
|
||||||
STR1 String Argument 1 (env $STR1)
|
|
||||||
STR2 String Argument 2 (env $STR2) (default "a value")
|
|
||||||
STR3 String Argument 3 (env $STR3)
|
|
||||||
INT1 Int Argument 1 (env $INT1) (default 0)
|
|
||||||
INT2 Int Argument 2 (env $INT2) (default 1)
|
|
||||||
INT3 Int Argument 3 (env $INT3)
|
|
||||||
STRS1 Strings Argument 1 (env $STRS1)
|
|
||||||
STRS2 (env $STRS2) (default ["value1", "value2"])
|
|
||||||
STRS3 Strings Argument 3 (env $STRS3)
|
|
||||||
INTS1 Ints Argument 1 (env $INTS1)
|
|
||||||
INTS2 Ints Argument 2 (env $INTS2) (default [1, 2, 3])
|
|
||||||
INTS3 Ints Argument 3 (env $INTS3)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-b, --bool1 Bool Option 1 (env $BOOL1)
|
|
||||||
--bool2 Bool Option 2 (default true)
|
|
||||||
-d Bool Option 3 (env $BOOL3)
|
|
||||||
-s, --str1 String Option 1 (env $STR1)
|
|
||||||
--str2 String Option 2 (default "a value")
|
|
||||||
-u String Option 3 (env $STR3)
|
|
||||||
-i, --int1 (env $INT1, $ALIAS_INT1) (default 0)
|
|
||||||
--int2 Int Option 2 (env $INT2) (default 1)
|
|
||||||
-k Int Option 3 (env $INT3)
|
|
||||||
-x, --strs1 Strings Option 1 (env $STRS1)
|
|
||||||
--strs2 Strings Option 2 (env $STRS2) (default ["value1", "value2"])
|
|
||||||
-z Strings Option 3 (env $STRS3)
|
|
||||||
-q, --ints1 Ints Option 1 (env $INTS1)
|
|
||||||
--ints2 Ints Option 2 (env $INTS2) (default [1, 2, 3])
|
|
||||||
-s Ints Option 3 (env $INTS3)
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
command1 command1 description
|
|
||||||
command2 command2 description
|
|
||||||
command3 command3 description
|
|
||||||
|
|
||||||
Run 'app COMMAND --help' for more information on a command.
|
|
|
@ -1,10 +0,0 @@
|
||||||
|
|
||||||
Usage: app [-o] ARG
|
|
||||||
|
|
||||||
Longer App Desc
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
ARG Argument
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-o, --opt Option
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Copyright 2014 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
# Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
# that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.8.x
|
||||||
|
- 1.x
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get github.com/maruel/pre-commit-go/cmd/pcg
|
||||||
|
|
||||||
|
script:
|
||||||
|
- pcg
|
|
@ -0,0 +1,57 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kr/pretty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/kr/text"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "7cafcd837844e784b526369c9bce262804aebc60"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/maruel/ut"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "a9c9f15ccfa6f8b90182a53df32f4745586fbae3"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||||
|
version = "v0.0.9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "fc9e8d8ef48496124e79ae0df75490096eccf6fe"
|
||||||
|
version = "v0.0.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mgutz/ansi"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/pmezard/go-difflib"
|
||||||
|
packages = ["difflib"]
|
||||||
|
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "e42485b6e20ae7d2304ec72e535b103ed350cc02"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "64e1c923b988d687243b43f8168fc7a83ceb603bf1ce4126022d34625cada8d9"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
|
@ -0,0 +1,38 @@
|
||||||
|
|
||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/maruel/ut"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
version = "0.0.9"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
version = "0.0.2"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/mgutz/ansi"
|
|
@ -0,0 +1,201 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor 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 the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2015 Marc-Antoine Ruel
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,139 @@
|
||||||
|
panicparse
|
||||||
|
==========
|
||||||
|
|
||||||
|
Parses panic stack traces, densifies and deduplicates goroutines with similar
|
||||||
|
stack traces. Helps debugging crashes and deadlocks in heavily parallelized
|
||||||
|
process.
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/maruel/panicparse.svg?branch=master)](https://travis-ci.org/maruel/panicparse)
|
||||||
|
|
||||||
|
panicparse helps make sense of Go crash dumps:
|
||||||
|
|
||||||
|
![Screencast](https://raw.githubusercontent.com/wiki/maruel/panicparse/parse.gif "Screencast")
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* >50% more compact output than original stack dump yet more readable.
|
||||||
|
* Exported symbols are bold, private symbols are darker.
|
||||||
|
* Stdlib is green, main is yellow, rest is red.
|
||||||
|
* Deduplicates redundant goroutine stacks. Useful for large server crashes.
|
||||||
|
* Arguments as pointer IDs instead of raw pointer values.
|
||||||
|
* Pushes stdlib-only stacks at the bottom to help focus on important code.
|
||||||
|
* Usable as a library!
|
||||||
|
[![GoDoc](https://godoc.org/github.com/maruel/panicparse/stack?status.svg)](https://godoc.org/github.com/maruel/panicparse/stack)
|
||||||
|
* Warning: please pin the version (e.g. vendor it). Breaking changes are
|
||||||
|
not planned but may happen.
|
||||||
|
* Parses the source files if available to augment the output.
|
||||||
|
* Works on Windows.
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
go get github.com/maruel/panicparse/cmd/pp
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Piping a stack trace from another process
|
||||||
|
|
||||||
|
#### TL;DR
|
||||||
|
|
||||||
|
* Ubuntu (bash v4 or zsh): `|&`
|
||||||
|
* OSX, [install bash 4+](README.md#updating-bash-on-osx), then: `|&`
|
||||||
|
* Windows _or_ OSX with stock bash v3: `2>&1 |`
|
||||||
|
* [Fish](http://fishshell.com/) shell: `^|`
|
||||||
|
|
||||||
|
|
||||||
|
#### Longer version
|
||||||
|
|
||||||
|
`pp` streams its stdin to stdout as long as it doesn't detect any panic.
|
||||||
|
`panic()` and Go's native deadlock detector [print to
|
||||||
|
stderr](https://golang.org/src/runtime/panic1.go) via the native [`print()`
|
||||||
|
function](https://golang.org/pkg/builtin/#print).
|
||||||
|
|
||||||
|
|
||||||
|
**Bash v4** or **zsh**: `|&` tells the shell to redirect stderr to stdout,
|
||||||
|
it's an alias for `2>&1 |` ([bash
|
||||||
|
v4](https://www.gnu.org/software/bash/manual/bash.html#Pipelines),
|
||||||
|
[zsh](http://zsh.sourceforge.net/Doc/Release/Shell-Grammar.html#Simple-Commands-_0026-Pipelines)):
|
||||||
|
|
||||||
|
go test -v |&pp
|
||||||
|
|
||||||
|
|
||||||
|
**Windows or OSX native bash** [(which is
|
||||||
|
3.2.57)](http://meta.ath0.com/2012/02/05/apples-great-gpl-purge/): They don't
|
||||||
|
have this shortcut, so use the long form:
|
||||||
|
|
||||||
|
go test -v 2>&1 | pp
|
||||||
|
|
||||||
|
|
||||||
|
**Fish**: It uses [^ for stderr
|
||||||
|
redirection](http://fishshell.com/docs/current/tutorial.html#tut_pipes_and_redirections)
|
||||||
|
so the shortcut is `^|`:
|
||||||
|
|
||||||
|
go test -v ^|pp
|
||||||
|
|
||||||
|
|
||||||
|
**PowerShell**: [It has broken `2>&1` redirection](https://connect.microsoft.com/PowerShell/feedback/details/765551/in-powershell-v3-you-cant-redirect-stderr-to-stdout-without-generating-error-records). The workaround is to shell out to cmd.exe. :(
|
||||||
|
|
||||||
|
|
||||||
|
### Investigate deadlock
|
||||||
|
|
||||||
|
On POSIX, use `Ctrl-\` to send SIGQUIT to your process, `pp` will ignore
|
||||||
|
the signal and will parse the stack trace.
|
||||||
|
|
||||||
|
|
||||||
|
### Parsing from a file
|
||||||
|
|
||||||
|
To dump to a file then parse, pass the file path of a stack trace
|
||||||
|
|
||||||
|
go test 2> stack.txt
|
||||||
|
pp stack.txt
|
||||||
|
|
||||||
|
|
||||||
|
Tips
|
||||||
|
----
|
||||||
|
|
||||||
|
### GOTRACEBACK
|
||||||
|
|
||||||
|
Starting with Go 1.6, [`GOTRACEBACK`](https://golang.org/pkg/runtime/) defaults
|
||||||
|
to `single` instead of `all` / `1` that was used in 1.5 and before. To get all
|
||||||
|
goroutines trace and not just the crashing one, set the environment variable:
|
||||||
|
|
||||||
|
export GOTRACEBACK=all
|
||||||
|
|
||||||
|
or `set GOTRACEBACK=all` on Windows. Probably worth to put it in your `.bashrc`.
|
||||||
|
|
||||||
|
|
||||||
|
### Updating bash on OSX
|
||||||
|
|
||||||
|
Install bash v4+ on OSX via [homebrew](http://brew.sh) or
|
||||||
|
[macports](https://www.macports.org/). Your future self will appreciate having
|
||||||
|
done that.
|
||||||
|
|
||||||
|
|
||||||
|
### If you have `/usr/bin/pp` installed
|
||||||
|
|
||||||
|
If you try `pp` for the first time and you get:
|
||||||
|
|
||||||
|
Creating tables and indexes...
|
||||||
|
Done.
|
||||||
|
|
||||||
|
and/or
|
||||||
|
|
||||||
|
/usr/bin/pp5.18: No input files specified
|
||||||
|
|
||||||
|
you may be running the _Perl PAR Packager_ instead of panicparse.
|
||||||
|
|
||||||
|
You have two choices, either you put `$GOPATH/bin` at the begining of `$PATH` or
|
||||||
|
use long name `panicparse` with:
|
||||||
|
|
||||||
|
go get github.com/maruel/panicparse
|
||||||
|
|
||||||
|
then using `panicparse` instead of `pp`:
|
||||||
|
|
||||||
|
go test 2> panicparse
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// panicparse: analyzes stack dump of Go processes and simplifies it.
|
||||||
|
//
|
||||||
|
// It is mostly useful on servers will large number of identical goroutines,
|
||||||
|
// making the crash dump harder to read than strictly necesary.
|
||||||
|
//
|
||||||
|
// Colors:
|
||||||
|
// - Magenta: first goroutine to be listed.
|
||||||
|
// - Yellow: main package.
|
||||||
|
// - Green: standard library.
|
||||||
|
// - Red: other packages.
|
||||||
|
//
|
||||||
|
// Bright colors are used for exported symbols.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/maruel/panicparse/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := internal.Main(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// This file contains the code to process sources, to be able to deduct the
|
||||||
|
// original types.
|
||||||
|
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cache is a cache of sources on the file system.
|
||||||
|
type cache struct {
|
||||||
|
files map[string][]byte
|
||||||
|
parsed map[string]*parsedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Augment processes source files to improve calls to be more descriptive.
|
||||||
|
//
|
||||||
|
// It modifies goroutines in place.
|
||||||
|
func Augment(goroutines []Goroutine) {
|
||||||
|
c := &cache{}
|
||||||
|
for i := range goroutines {
|
||||||
|
c.augmentGoroutine(&goroutines[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// augmentGoroutine processes source files to improve call to be more
|
||||||
|
// descriptive.
|
||||||
|
//
|
||||||
|
// It modifies the routine.
|
||||||
|
func (c *cache) augmentGoroutine(goroutine *Goroutine) {
|
||||||
|
if c.files == nil {
|
||||||
|
c.files = map[string][]byte{}
|
||||||
|
}
|
||||||
|
if c.parsed == nil {
|
||||||
|
c.parsed = map[string]*parsedFile{}
|
||||||
|
}
|
||||||
|
// For each call site, look at the next call and populate it. Then we can
|
||||||
|
// walk back and reformat things.
|
||||||
|
for i := range goroutine.Stack.Calls {
|
||||||
|
c.load(goroutine.Stack.Calls[i].SourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once all loaded, we can look at the next call when available.
|
||||||
|
for i := 0; i < len(goroutine.Stack.Calls)-1; i++ {
|
||||||
|
// Get the AST from the previous call and process the call line with it.
|
||||||
|
if f := c.getFuncAST(&goroutine.Stack.Calls[i]); f != nil {
|
||||||
|
processCall(&goroutine.Stack.Calls[i], f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private stuff.
|
||||||
|
|
||||||
|
// load loads a source file and parses the AST tree. Failures are ignored.
|
||||||
|
func (c *cache) load(fileName string) {
|
||||||
|
if _, ok := c.parsed[fileName]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.parsed[fileName] = nil
|
||||||
|
if !strings.HasSuffix(fileName, ".go") {
|
||||||
|
// Ignore C and assembly.
|
||||||
|
c.files[fileName] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("load(%s)", fileName)
|
||||||
|
if _, ok := c.files[fileName]; !ok {
|
||||||
|
var err error
|
||||||
|
if c.files[fileName], err = ioutil.ReadFile(fileName); err != nil {
|
||||||
|
log.Printf("Failed to read %s: %s", fileName, err)
|
||||||
|
c.files[fileName] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
src := c.files[fileName]
|
||||||
|
parsed, err := parser.ParseFile(fset, fileName, src, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to parse %s: %s", fileName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Convert the line number into raw file offset.
|
||||||
|
offsets := []int{0, 0}
|
||||||
|
start := 0
|
||||||
|
for l := 1; start < len(src); l++ {
|
||||||
|
start += bytes.IndexByte(src[start:], '\n') + 1
|
||||||
|
offsets = append(offsets, start)
|
||||||
|
}
|
||||||
|
c.parsed[fileName] = &parsedFile{offsets, parsed}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cache) getFuncAST(call *Call) *ast.FuncDecl {
|
||||||
|
if p := c.parsed[call.SourcePath]; p != nil {
|
||||||
|
return p.getFuncAST(call.Func.Name(), call.Line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type parsedFile struct {
|
||||||
|
lineToByteOffset []int
|
||||||
|
parsed *ast.File
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFuncAST gets the callee site function AST representation for the code
|
||||||
|
// inside the function f at line l.
|
||||||
|
func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl) {
|
||||||
|
if len(p.lineToByteOffset) <= l {
|
||||||
|
// The line number in the stack trace line does not exist in the file. That
|
||||||
|
// can only mean that the sources on disk do not match the sources used to
|
||||||
|
// build the binary.
|
||||||
|
// TODO(maruel): This should be surfaced, so that source parsing is
|
||||||
|
// completely ignored.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the AST to find the lineToByteOffset that fits the line number.
|
||||||
|
var lastFunc *ast.FuncDecl
|
||||||
|
var found ast.Node
|
||||||
|
// Inspect() goes depth first. This means for example that a function like:
|
||||||
|
// func a() {
|
||||||
|
// b := func() {}
|
||||||
|
// c()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Were we are looking at the c() call can return confused values. It is
|
||||||
|
// important to look at the actual ast.Node hierarchy.
|
||||||
|
ast.Inspect(p.parsed, func(n ast.Node) bool {
|
||||||
|
if d != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if n == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if found != nil {
|
||||||
|
// We are walking up.
|
||||||
|
}
|
||||||
|
if int(n.Pos()) >= p.lineToByteOffset[l] {
|
||||||
|
// We are expecting a ast.CallExpr node. It can be harder to figure out
|
||||||
|
// when there are multiple calls on a single line, as the stack trace
|
||||||
|
// doesn't have file byte offset information, only line based.
|
||||||
|
// gofmt will always format to one function call per line but there can
|
||||||
|
// be edge cases, like:
|
||||||
|
// a = A{Foo(), Bar()}
|
||||||
|
d = lastFunc
|
||||||
|
//p.processNode(call, n)
|
||||||
|
return false
|
||||||
|
} else if f, ok := n.(*ast.FuncDecl); ok {
|
||||||
|
lastFunc = f
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func name(n ast.Node) string {
|
||||||
|
switch t := n.(type) {
|
||||||
|
case *ast.InterfaceType:
|
||||||
|
return "interface{}"
|
||||||
|
case *ast.Ident:
|
||||||
|
return t.Name
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
return t.Sel.Name
|
||||||
|
case *ast.StarExpr:
|
||||||
|
return "*" + name(t.X)
|
||||||
|
default:
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldToType returns the type name and whether if it's an ellipsis.
|
||||||
|
func fieldToType(f *ast.Field) (string, bool) {
|
||||||
|
switch arg := f.Type.(type) {
|
||||||
|
case *ast.ArrayType:
|
||||||
|
return "[]" + name(arg.Elt), false
|
||||||
|
case *ast.Ellipsis:
|
||||||
|
return name(arg.Elt), true
|
||||||
|
case *ast.FuncType:
|
||||||
|
// Do not print the function signature to not overload the trace.
|
||||||
|
return "func", false
|
||||||
|
case *ast.Ident:
|
||||||
|
return arg.Name, false
|
||||||
|
case *ast.InterfaceType:
|
||||||
|
return "interface{}", false
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
return arg.Sel.Name, false
|
||||||
|
case *ast.StarExpr:
|
||||||
|
return "*" + name(arg.X), false
|
||||||
|
case *ast.MapType:
|
||||||
|
return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false
|
||||||
|
case *ast.ChanType:
|
||||||
|
return fmt.Sprintf("chan %s", name(arg.Value)), false
|
||||||
|
default:
|
||||||
|
// TODO(maruel): Implement anything missing.
|
||||||
|
return "<unknown>", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractArgumentsType returns the name of the type of each input argument.
|
||||||
|
func extractArgumentsType(f *ast.FuncDecl) ([]string, bool) {
|
||||||
|
var fields []*ast.Field
|
||||||
|
if f.Recv != nil {
|
||||||
|
if len(f.Recv.List) != 1 {
|
||||||
|
panic("Expect only one receiver; please fix panicparse's code")
|
||||||
|
}
|
||||||
|
// If it is an object receiver (vs a pointer receiver), its address is not
|
||||||
|
// printed in the stack trace so it needs to be ignored.
|
||||||
|
if _, ok := f.Recv.List[0].Type.(*ast.StarExpr); ok {
|
||||||
|
fields = append(fields, f.Recv.List[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var types []string
|
||||||
|
extra := false
|
||||||
|
for _, arg := range append(fields, f.Type.Params.List...) {
|
||||||
|
// Assert that extra is only set on the last item of fields?
|
||||||
|
var t string
|
||||||
|
t, extra = fieldToType(arg)
|
||||||
|
mult := len(arg.Names)
|
||||||
|
if mult == 0 {
|
||||||
|
mult = 1
|
||||||
|
}
|
||||||
|
for i := 0; i < mult; i++ {
|
||||||
|
types = append(types, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types, extra
|
||||||
|
}
|
||||||
|
|
||||||
|
// processCall walks the function and populate call accordingly.
|
||||||
|
func processCall(call *Call, f *ast.FuncDecl) {
|
||||||
|
values := make([]uint64, len(call.Args.Values))
|
||||||
|
for i := range call.Args.Values {
|
||||||
|
values[i] = call.Args.Values[i].Value
|
||||||
|
}
|
||||||
|
index := 0
|
||||||
|
pop := func() uint64 {
|
||||||
|
if len(values) != 0 {
|
||||||
|
x := values[0]
|
||||||
|
values = values[1:]
|
||||||
|
index++
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
popName := func() string {
|
||||||
|
n := call.Args.Values[index].Name
|
||||||
|
v := pop()
|
||||||
|
if len(n) == 0 {
|
||||||
|
return fmt.Sprintf("0x%x", v)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
types, extra := extractArgumentsType(f)
|
||||||
|
for i := 0; len(values) != 0; i++ {
|
||||||
|
var t string
|
||||||
|
if i >= len(types) {
|
||||||
|
if !extra {
|
||||||
|
// These are unexpected value! Print them as hex.
|
||||||
|
call.Args.Processed = append(call.Args.Processed, popName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t = types[len(types)-1]
|
||||||
|
} else {
|
||||||
|
t = types[i]
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case "float32":
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%g", math.Float32frombits(uint32(pop()))))
|
||||||
|
case "float64":
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%g", math.Float64frombits(pop())))
|
||||||
|
case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64":
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%d", pop()))
|
||||||
|
case "string":
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s, len=%d)", t, popName(), pop()))
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(t, "*") {
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s)", t, popName()))
|
||||||
|
} else if strings.HasPrefix(t, "[]") {
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s len=%d cap=%d)", t, popName(), pop(), pop()))
|
||||||
|
} else {
|
||||||
|
// Assumes it's an interface. For now, discard the object value, which
|
||||||
|
// is probably not a good idea.
|
||||||
|
call.Args.Processed = append(call.Args.Processed, fmt.Sprintf("%s(%s)", t, popName()))
|
||||||
|
pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(values) == 0 && call.Args.Elided {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,563 @@
|
||||||
|
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/maruel/ut"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAugment(t *testing.T) {
|
||||||
|
data := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected Stack
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Local function doesn't interfere",
|
||||||
|
`package main
|
||||||
|
func f(s string) {
|
||||||
|
a := func(i int) int {
|
||||||
|
return 1 + i
|
||||||
|
}
|
||||||
|
_ = a(3)
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f("yo")
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 7, Func: Function{"main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer, Name: ""}, {Value: 0x2}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 10, Func: Function{"main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"func",
|
||||||
|
`package main
|
||||||
|
func f(a func() string) {
|
||||||
|
panic(a())
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(func() string { return "ooh" })
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{Values: []Arg{{Value: pointer}}},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"func ellipsis",
|
||||||
|
`package main
|
||||||
|
func f(a ...func() string) {
|
||||||
|
panic(a[0]())
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(func() string { return "ooh" })
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: 0x1}, {Value: 0x1}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"interface{}",
|
||||||
|
`package main
|
||||||
|
func f(a []interface{}) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(make([]interface{}, 5, 7))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: 0x5}, {Value: 0x7}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"[]int",
|
||||||
|
`package main
|
||||||
|
func f(a []int) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(make([]int, 5, 7))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: 5}, {Value: 7}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"[]interface{}",
|
||||||
|
`package main
|
||||||
|
func f(a []interface{}) {
|
||||||
|
panic(a[0].(string))
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f([]interface{}{"ooh"})
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: 1}, {Value: 1}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map[int]int",
|
||||||
|
`package main
|
||||||
|
func f(a map[int]int) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(map[int]int{1: 2})
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"map[interface{}]interface{}",
|
||||||
|
`package main
|
||||||
|
func f(a map[interface{}]interface{}) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(make(map[interface{}]interface{}))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chan int",
|
||||||
|
`package main
|
||||||
|
func f(a chan int) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(make(chan int))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chan interface{}",
|
||||||
|
`package main
|
||||||
|
func f(a chan interface{}) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(make(chan interface{}))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"non-pointer method",
|
||||||
|
`package main
|
||||||
|
type S struct {
|
||||||
|
}
|
||||||
|
func (s S) f() {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
var s S
|
||||||
|
s.f()
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{SourcePath: "main.go", Line: 5, Func: Function{Raw: "main.S.f"}},
|
||||||
|
{SourcePath: "main.go", Line: 9, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pointer method",
|
||||||
|
`package main
|
||||||
|
type S struct {
|
||||||
|
}
|
||||||
|
func (s *S) f() {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
var s S
|
||||||
|
s.f()
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 5, Func: Function{Raw: "main.(*S).f"},
|
||||||
|
Args: Args{Values: []Arg{{Value: pointer}}},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 9, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"string",
|
||||||
|
`package main
|
||||||
|
func f(s string) {
|
||||||
|
panic(s)
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f("ooh")
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{Values: []Arg{{Value: pointer}, {Value: 0x3}}},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"string and int",
|
||||||
|
`package main
|
||||||
|
func f(s string, i int) {
|
||||||
|
panic(s)
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f("ooh", 42)
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{Values: []Arg{{Value: pointer}, {Value: 0x3}, {Value: 42}}},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"values are elided",
|
||||||
|
`package main
|
||||||
|
func f(s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12 int, s13 interface{}) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(0, 0, 0, 0, 0, 0, 0, 0, 42, 43, 44, 45, nil)
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{}, {}, {}, {}, {}, {}, {}, {}, {Value: 42}, {Value: 43}},
|
||||||
|
Elided: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error",
|
||||||
|
`package main
|
||||||
|
import "errors"
|
||||||
|
func f(err error) {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(errors.New("ooh"))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 4, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 7, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error unnamed",
|
||||||
|
`package main
|
||||||
|
import "errors"
|
||||||
|
func f(error) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(errors.New("ooh"))
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 4, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{{Value: pointer}, {Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 7, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"float32",
|
||||||
|
`package main
|
||||||
|
func f(v float32) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(0.5)
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
// The value is NOT a pointer but floating point encoding is not
|
||||||
|
// deterministic.
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"float64",
|
||||||
|
`package main
|
||||||
|
func f(v float64) {
|
||||||
|
panic("ooh")
|
||||||
|
}
|
||||||
|
func main() {
|
||||||
|
f(0.5)
|
||||||
|
}`,
|
||||||
|
Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: "main.go", Line: 3, Func: Function{Raw: "main.f"},
|
||||||
|
Args: Args{
|
||||||
|
// The value is NOT a pointer but floating point encoding is not
|
||||||
|
// deterministic.
|
||||||
|
Values: []Arg{{Value: pointer}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{SourcePath: "main.go", Line: 6, Func: Function{Raw: "main.main"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, line := range data {
|
||||||
|
extra := bytes.Buffer{}
|
||||||
|
_, content := getCrash(t, line.input)
|
||||||
|
goroutines, err := ParseDump(bytes.NewBuffer(content), &extra)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse input for test %s: %v", line.name, err)
|
||||||
|
}
|
||||||
|
// On go1.4, there's one less space.
|
||||||
|
actual := extra.String()
|
||||||
|
if actual != "panic: ooh\n\nexit status 2\n" && actual != "panic: ooh\nexit status 2\n" {
|
||||||
|
t.Fatalf("Unexpected panic output:\n%#v", actual)
|
||||||
|
}
|
||||||
|
s := goroutines[0].Signature.Stack
|
||||||
|
t.Logf("Test: %v", line.name)
|
||||||
|
zapPointers(t, line.name, &line.expected, &s)
|
||||||
|
zapPaths(&s)
|
||||||
|
ut.AssertEqualIndex(t, i, line.expected, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAugmentDummy(t *testing.T) {
|
||||||
|
goroutines := []Goroutine{
|
||||||
|
{
|
||||||
|
Signature: Signature{
|
||||||
|
Stack: Stack{
|
||||||
|
Calls: []Call{{SourcePath: "missing.go"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Augment(goroutines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
c := &cache{
|
||||||
|
files: map[string][]byte{"bad.go": []byte("bad content")},
|
||||||
|
parsed: map[string]*parsedFile{},
|
||||||
|
}
|
||||||
|
c.load("foo.asm")
|
||||||
|
c.load("bad.go")
|
||||||
|
c.load("doesnt_exist.go")
|
||||||
|
if l := len(c.parsed); l != 3 {
|
||||||
|
t.Fatalf("expected 3, got %d", l)
|
||||||
|
}
|
||||||
|
if c.parsed["foo.asm"] != nil {
|
||||||
|
t.Fatalf("foo.asm is not present; should not have been loaded")
|
||||||
|
}
|
||||||
|
if c.parsed["bad.go"] != nil {
|
||||||
|
t.Fatalf("bad.go is not valid code; should not have been loaded")
|
||||||
|
}
|
||||||
|
if c.parsed["doesnt_exist.go"] != nil {
|
||||||
|
t.Fatalf("doesnt_exist.go is not present; should not have been loaded")
|
||||||
|
}
|
||||||
|
if c.getFuncAST(&Call{SourcePath: "other"}) != nil {
|
||||||
|
t.Fatalf("there's no 'other'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
const pointer = uint64(0xfffffffff)
|
||||||
|
const pointerStr = "0xfffffffff"
|
||||||
|
|
||||||
|
func overrideEnv(env []string, key, value string) []string {
|
||||||
|
prefix := key + "="
|
||||||
|
for i, e := range env {
|
||||||
|
if strings.HasPrefix(e, prefix) {
|
||||||
|
env[i] = prefix + value
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(env, prefix+value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCrash(t *testing.T, content string) (string, []byte) {
|
||||||
|
name, err := ioutil.TempDir("", "panicparse")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temporary directory: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := os.RemoveAll(name); err != nil {
|
||||||
|
t.Fatalf("failed to remove temporary directory %q: %v", name, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
main := filepath.Join(name, "main.go")
|
||||||
|
if err := ioutil.WriteFile(main, []byte(content), 0500); err != nil {
|
||||||
|
t.Fatalf("failed to write %q: %v", main, err)
|
||||||
|
}
|
||||||
|
cmd := exec.Command("go", "run", main)
|
||||||
|
// Use the Go 1.4 compatible format.
|
||||||
|
cmd.Env = overrideEnv(os.Environ(), "GOTRACEBACK", "1")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error since this is supposed to crash")
|
||||||
|
}
|
||||||
|
return main, out
|
||||||
|
}
|
||||||
|
|
||||||
|
// zapPointers zaps out pointers.
|
||||||
|
func zapPointers(t *testing.T, name string, expected, s *Stack) {
|
||||||
|
for i := range s.Calls {
|
||||||
|
if i >= len(expected.Calls) {
|
||||||
|
// When using GOTRACEBACK=2, it'll include runtime.main() and
|
||||||
|
// runtime.goexit(). Ignore these since they could be changed in a future
|
||||||
|
// version.
|
||||||
|
s.Calls = s.Calls[:len(expected.Calls)]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for j := range s.Calls[i].Args.Values {
|
||||||
|
if j >= len(expected.Calls[i].Args.Values) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if expected.Calls[i].Args.Values[j].Value == pointer {
|
||||||
|
// Replace the pointer value.
|
||||||
|
if s.Calls[i].Args.Values[j].Value == 0 {
|
||||||
|
t.Fatalf("%s: Call %d, value %d, expected pointer, got 0", name, i, j)
|
||||||
|
}
|
||||||
|
old := fmt.Sprintf("0x%x", s.Calls[i].Args.Values[j].Value)
|
||||||
|
s.Calls[i].Args.Values[j].Value = pointer
|
||||||
|
for k := range s.Calls[i].Args.Processed {
|
||||||
|
s.Calls[i].Args.Processed[k] = strings.Replace(s.Calls[i].Args.Processed[k], old, pointerStr, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// zapPaths removes the directory part and only keep the base file name.
|
||||||
|
func zapPaths(s *Stack) {
|
||||||
|
for j := range s.Calls {
|
||||||
|
s.Calls[j].SourcePath = filepath.Base(s.Calls[j].SourcePath)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,832 @@
|
||||||
|
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package stack analyzes stack dump of Go processes and simplifies it.
|
||||||
|
//
|
||||||
|
// It is mostly useful on servers will large number of identical goroutines,
|
||||||
|
// making the crash dump harder to read than strictly necesary.
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const lockedToThread = "locked to thread"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TODO(maruel): Handle corrupted stack cases:
|
||||||
|
// - missed stack barrier
|
||||||
|
// - found next stack barrier at 0x123; expected
|
||||||
|
// - runtime: unexpected return pc for FUNC_NAME called from 0x123
|
||||||
|
|
||||||
|
reRoutineHeader = regexp.MustCompile("^goroutine (\\d+) \\[([^\\]]+)\\]\\:\r?\n$")
|
||||||
|
reMinutes = regexp.MustCompile("^(\\d+) minutes$")
|
||||||
|
reUnavail = regexp.MustCompile("^(?:\t| +)goroutine running on other thread; stack unavailable")
|
||||||
|
// See gentraceback() in src/runtime/traceback.go for more information.
|
||||||
|
// - Sometimes the source file comes up as "<autogenerated>". It is the
|
||||||
|
// compiler than generated these, not the runtime.
|
||||||
|
// - The tab may be replaced with spaces when a user copy-paste it, handle
|
||||||
|
// this transparently.
|
||||||
|
// - "runtime.gopanic" is explicitly replaced with "panic" by gentraceback().
|
||||||
|
// - The +0x123 byte offset is printed when frame.pc > _func.entry. _func is
|
||||||
|
// generated by the linker.
|
||||||
|
// - The +0x123 byte offset is not included with generated code, e.g. unnamed
|
||||||
|
// functions "func·006()" which is generally go func() { ... }()
|
||||||
|
// statements. Since the _func is generated at runtime, it's probably why
|
||||||
|
// _func.entry is not set.
|
||||||
|
// - C calls may have fp=0x123 sp=0x123 appended. I think it normally happens
|
||||||
|
// when a signal is not correctly handled. It is printed with m.throwing>0.
|
||||||
|
// These are discarded.
|
||||||
|
// - For cgo, the source file may be "??".
|
||||||
|
reFile = regexp.MustCompile("^(?:\t| +)(\\?\\?|\\<autogenerated\\>|.+\\.(?:c|go|s))\\:(\\d+)(?:| \\+0x[0-9a-f]+)(?:| fp=0x[0-9a-f]+ sp=0x[0-9a-f]+)\r?\n$")
|
||||||
|
// Sadly, it doesn't note the goroutine number so we could cascade them per
|
||||||
|
// parenthood.
|
||||||
|
reCreated = regexp.MustCompile("^created by (.+)\r?\n$")
|
||||||
|
reFunc = regexp.MustCompile("^(.+)\\((.*)\\)\r?\n$")
|
||||||
|
reElided = regexp.MustCompile("^\\.\\.\\.additional frames elided\\.\\.\\.\r?\n$")
|
||||||
|
// Include frequent GOROOT value on Windows, distro provided and user
|
||||||
|
// installed path. This simplifies the user's life when processing a trace
|
||||||
|
// generated on another VM.
|
||||||
|
// TODO(maruel): Guess the path automatically via traces containing the
|
||||||
|
// 'runtime' package, which is very frequent. This would be "less bad" than
|
||||||
|
// throwing up random values at the parser.
|
||||||
|
goroots = []string{runtime.GOROOT(), "c:/go", "/usr/lib/go", "/usr/local/go"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Similarity is the level at which two call lines arguments must match to be
|
||||||
|
// considered similar enough to coalesce them.
|
||||||
|
type Similarity int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ExactFlags requires same bits (e.g. Locked).
|
||||||
|
ExactFlags Similarity = iota
|
||||||
|
// ExactLines requests the exact same arguments on the call line.
|
||||||
|
ExactLines
|
||||||
|
// AnyPointer considers different pointers a similar call line.
|
||||||
|
AnyPointer
|
||||||
|
// AnyValue accepts any value as similar call line.
|
||||||
|
AnyValue
|
||||||
|
)
|
||||||
|
|
||||||
|
// Function is a function call.
|
||||||
|
//
|
||||||
|
// Go stack traces print a mangled function call, this wrapper unmangle the
|
||||||
|
// string before printing and adds other filtering methods.
|
||||||
|
type Function struct {
|
||||||
|
Raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is the fully qualified function name.
|
||||||
|
//
|
||||||
|
// Sadly Go is a bit confused when the package name doesn't match the directory
|
||||||
|
// containing the source file and will use the directory name instead of the
|
||||||
|
// real package name.
|
||||||
|
func (f Function) String() string {
|
||||||
|
s, _ := url.QueryUnescape(f.Raw)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name is the naked function name.
|
||||||
|
func (f Function) Name() string {
|
||||||
|
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgName is the package name for this function reference.
|
||||||
|
func (f Function) PkgName() string {
|
||||||
|
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s, _ := url.QueryUnescape(parts[0])
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgDotName returns "<package>.<func>" format.
|
||||||
|
func (f Function) PkgDotName() string {
|
||||||
|
parts := strings.SplitN(filepath.Base(f.Raw), ".", 2)
|
||||||
|
s, _ := url.QueryUnescape(parts[0])
|
||||||
|
if len(parts) == 1 {
|
||||||
|
return parts[0]
|
||||||
|
}
|
||||||
|
if s != "" || parts[1] != "" {
|
||||||
|
return s + "." + parts[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExported returns true if the function is exported.
|
||||||
|
func (f Function) IsExported() bool {
|
||||||
|
name := f.Name()
|
||||||
|
parts := strings.Split(name, ".")
|
||||||
|
r, _ := utf8.DecodeRuneInString(parts[len(parts)-1])
|
||||||
|
if unicode.ToUpper(r) == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return f.PkgName() == "main" && name == "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arg is an argument on a Call.
|
||||||
|
type Arg struct {
|
||||||
|
Value uint64 // Value is the raw value as found in the stack trace
|
||||||
|
Name string // Name is a pseudo name given to the argument
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPtr returns true if we guess it's a pointer. It's only a guess, it can be
|
||||||
|
// easily be confused by a bitmask.
|
||||||
|
func (a *Arg) IsPtr() bool {
|
||||||
|
// Assumes all pointers are above 16Mb and positive.
|
||||||
|
return a.Value > 16*1024*1024 && a.Value < math.MaxInt64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Arg) String() string {
|
||||||
|
if a.Name != "" {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
if a.Value == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("0x%x", a.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args is a series of function call arguments.
|
||||||
|
type Args struct {
|
||||||
|
Values []Arg // Values is the arguments as shown on the stack trace. They are mangled via simplification.
|
||||||
|
Processed []string // Processed is the arguments generated from processing the source files. It can have a length lower than Values.
|
||||||
|
Elided bool // If set, it means there was a trailing ", ..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Args) String() string {
|
||||||
|
var v []string
|
||||||
|
if len(a.Processed) != 0 {
|
||||||
|
v = make([]string, 0, len(a.Processed))
|
||||||
|
for _, item := range a.Processed {
|
||||||
|
v = append(v, item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v = make([]string, 0, len(a.Values))
|
||||||
|
for _, item := range a.Values {
|
||||||
|
v = append(v, item.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.Elided {
|
||||||
|
v = append(v, "...")
|
||||||
|
}
|
||||||
|
return strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true only if both arguments are exactly equal.
|
||||||
|
func (a *Args) Equal(r *Args) bool {
|
||||||
|
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, l := range a.Values {
|
||||||
|
if l != r.Values[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar returns true if the two Args are equal or almost but not quite
|
||||||
|
// equal.
|
||||||
|
func (a *Args) Similar(r *Args, similar Similarity) bool {
|
||||||
|
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if similar == AnyValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, l := range a.Values {
|
||||||
|
switch similar {
|
||||||
|
case ExactFlags, ExactLines:
|
||||||
|
if l != r.Values[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if l.IsPtr() != r.Values[i].IsPtr() || (!l.IsPtr() && l != r.Values[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges two similar Args, zapping out differences.
|
||||||
|
func (a *Args) Merge(r *Args) Args {
|
||||||
|
out := Args{
|
||||||
|
Values: make([]Arg, len(a.Values)),
|
||||||
|
Elided: a.Elided,
|
||||||
|
}
|
||||||
|
for i, l := range a.Values {
|
||||||
|
if l != r.Values[i] {
|
||||||
|
out.Values[i].Name = "*"
|
||||||
|
out.Values[i].Value = l.Value
|
||||||
|
} else {
|
||||||
|
out.Values[i] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call is an item in the stack trace.
|
||||||
|
type Call struct {
|
||||||
|
SourcePath string // Full path name of the source file
|
||||||
|
Line int // Line number
|
||||||
|
Func Function // Fully qualified function name (encoded).
|
||||||
|
Args Args // Call arguments
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true only if both calls are exactly equal.
|
||||||
|
func (c *Call) Equal(r *Call) bool {
|
||||||
|
return c.SourcePath == r.SourcePath && c.Line == r.Line && c.Func == r.Func && c.Args.Equal(&r.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar returns true if the two Call are equal or almost but not quite
|
||||||
|
// equal.
|
||||||
|
func (c *Call) Similar(r *Call, similar Similarity) bool {
|
||||||
|
return c.SourcePath == r.SourcePath && c.Line == r.Line && c.Func == r.Func && c.Args.Similar(&r.Args, similar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges two similar Call, zapping out differences.
|
||||||
|
func (c *Call) Merge(r *Call) Call {
|
||||||
|
return Call{
|
||||||
|
SourcePath: c.SourcePath,
|
||||||
|
Line: c.Line,
|
||||||
|
Func: c.Func,
|
||||||
|
Args: c.Args.Merge(&r.Args),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceName returns the base file name of the source file.
|
||||||
|
func (c *Call) SourceName() string {
|
||||||
|
return filepath.Base(c.SourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceLine returns "source.go:line", including only the base file name.
|
||||||
|
func (c *Call) SourceLine() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.SourceName(), c.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullSourceLine returns "/path/to/source.go:line".
|
||||||
|
func (c *Call) FullSourceLine() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.SourcePath, c.Line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgSource is one directory plus the file name of the source file.
|
||||||
|
func (c *Call) PkgSource() string {
|
||||||
|
return filepath.Join(filepath.Base(filepath.Dir(c.SourcePath)), c.SourceName())
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMainSource = "_test" + string(os.PathSeparator) + "_testmain.go"
|
||||||
|
|
||||||
|
// IsStdlib returns true if it is a Go standard library function. This includes
|
||||||
|
// the 'go test' generated main executable.
|
||||||
|
func (c *Call) IsStdlib() bool {
|
||||||
|
for _, goroot := range goroots {
|
||||||
|
if strings.HasPrefix(c.SourcePath, goroot) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Consider _test/_testmain.go as stdlib since it's injected by "go test".
|
||||||
|
return c.PkgSource() == testMainSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPkgMain returns true if it is in the main package.
|
||||||
|
func (c *Call) IsPkgMain() bool {
|
||||||
|
return c.Func.PkgName() == "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack is a call stack.
|
||||||
|
type Stack struct {
|
||||||
|
Calls []Call // Call stack. First is original function, last is leaf function.
|
||||||
|
Elided bool // Happens when there's >100 items in Stack, currently hardcoded in package runtime.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true on if both call stacks are exactly equal.
|
||||||
|
func (s *Stack) Equal(r *Stack) bool {
|
||||||
|
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range s.Calls {
|
||||||
|
if !s.Calls[i].Equal(&r.Calls[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar returns true if the two Stack are equal or almost but not quite
|
||||||
|
// equal.
|
||||||
|
func (s *Stack) Similar(r *Stack, similar Similarity) bool {
|
||||||
|
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range s.Calls {
|
||||||
|
if !s.Calls[i].Similar(&r.Calls[i], similar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges two similar Stack, zapping out differences.
|
||||||
|
func (s *Stack) Merge(r *Stack) *Stack {
|
||||||
|
// Assumes similar stacks have the same length.
|
||||||
|
out := &Stack{
|
||||||
|
Calls: make([]Call, len(s.Calls)),
|
||||||
|
Elided: s.Elided,
|
||||||
|
}
|
||||||
|
for i := range s.Calls {
|
||||||
|
out.Calls[i] = s.Calls[i].Merge(&r.Calls[i])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less compares two Stack, where the ones that are less are more
|
||||||
|
// important, so they come up front. A Stack with more private functions is
|
||||||
|
// 'less' so it is at the top. Inversely, a Stack with only public
|
||||||
|
// functions is 'more' so it is at the bottom.
|
||||||
|
func (s *Stack) Less(r *Stack) bool {
|
||||||
|
lStdlib := 0
|
||||||
|
lPrivate := 0
|
||||||
|
for _, c := range s.Calls {
|
||||||
|
if c.IsStdlib() {
|
||||||
|
lStdlib++
|
||||||
|
} else {
|
||||||
|
lPrivate++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rStdlib := 0
|
||||||
|
rPrivate := 0
|
||||||
|
for _, s := range r.Calls {
|
||||||
|
if s.IsStdlib() {
|
||||||
|
rStdlib++
|
||||||
|
} else {
|
||||||
|
rPrivate++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lPrivate > rPrivate {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if lPrivate < rPrivate {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lStdlib > rStdlib {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lStdlib < rStdlib {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack lengths are the same.
|
||||||
|
for x := range s.Calls {
|
||||||
|
if s.Calls[x].Func.Raw < r.Calls[x].Func.Raw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.Calls[x].Func.Raw > r.Calls[x].Func.Raw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.Calls[x].PkgSource() < r.Calls[x].PkgSource() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.Calls[x].PkgSource() > r.Calls[x].PkgSource() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.Calls[x].Line < r.Calls[x].Line {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.Calls[x].Line > r.Calls[x].Line {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature represents the signature of one or multiple goroutines.
|
||||||
|
//
|
||||||
|
// It is effectively the stack trace plus the goroutine internal bits, like
|
||||||
|
// it's state, if it is thread locked, which call site created this goroutine,
|
||||||
|
// etc.
|
||||||
|
type Signature struct {
|
||||||
|
// Use git grep 'gopark(|unlock)\(' to find them all plus everything listed
|
||||||
|
// in runtime/traceback.go. Valid values includes:
|
||||||
|
// - chan send, chan receive, select
|
||||||
|
// - finalizer wait, mark wait (idle),
|
||||||
|
// - Concurrent GC wait, GC sweep wait, force gc (idle)
|
||||||
|
// - IO wait, panicwait
|
||||||
|
// - semacquire, semarelease
|
||||||
|
// - sleep, timer goroutine (idle)
|
||||||
|
// - trace reader (blocked)
|
||||||
|
// Stuck cases:
|
||||||
|
// - chan send (nil chan), chan receive (nil chan), select (no cases)
|
||||||
|
// Runnable states:
|
||||||
|
// - idle, runnable, running, syscall, waiting, dead, enqueue, copystack,
|
||||||
|
// Scan states:
|
||||||
|
// - scan, scanrunnable, scanrunning, scansyscall, scanwaiting, scandead,
|
||||||
|
// scanenqueue
|
||||||
|
State string
|
||||||
|
CreatedBy Call // Which other goroutine which created this one.
|
||||||
|
SleepMin int // Wait time in minutes, if applicable.
|
||||||
|
SleepMax int // Wait time in minutes, if applicable.
|
||||||
|
Stack Stack
|
||||||
|
Locked bool // Locked to an OS thread.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal returns true only if both signatures are exactly equal.
|
||||||
|
func (s *Signature) Equal(r *Signature) bool {
|
||||||
|
if s.State != r.State || !s.CreatedBy.Equal(&r.CreatedBy) || s.Locked != r.Locked || s.SleepMin != r.SleepMin || s.SleepMax != r.SleepMax {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.Stack.Equal(&r.Stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar returns true if the two Signature are equal or almost but not quite
|
||||||
|
// equal.
|
||||||
|
func (s *Signature) Similar(r *Signature, similar Similarity) bool {
|
||||||
|
if s.State != r.State || !s.CreatedBy.Similar(&r.CreatedBy, similar) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if similar == ExactFlags && s.Locked != r.Locked {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.Stack.Similar(&r.Stack, similar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges two similar Signature, zapping out differences.
|
||||||
|
func (s *Signature) Merge(r *Signature) *Signature {
|
||||||
|
min := s.SleepMin
|
||||||
|
if r.SleepMin < min {
|
||||||
|
min = r.SleepMin
|
||||||
|
}
|
||||||
|
max := s.SleepMax
|
||||||
|
if r.SleepMax > max {
|
||||||
|
max = r.SleepMax
|
||||||
|
}
|
||||||
|
return &Signature{
|
||||||
|
State: s.State, // Drop right side.
|
||||||
|
CreatedBy: s.CreatedBy, // Drop right side.
|
||||||
|
SleepMin: min,
|
||||||
|
SleepMax: max,
|
||||||
|
Stack: *s.Stack.Merge(&r.Stack),
|
||||||
|
Locked: s.Locked || r.Locked, // TODO(maruel): This is weirdo.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less compares two Signature, where the ones that are less are more
|
||||||
|
// important, so they come up front. A Signature with more private functions is
|
||||||
|
// 'less' so it is at the top. Inversely, a Signature with only public
|
||||||
|
// functions is 'more' so it is at the bottom.
|
||||||
|
func (s *Signature) Less(r *Signature) bool {
|
||||||
|
if s.Stack.Less(&r.Stack) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if r.Stack.Less(&s.Stack) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.Locked && !r.Locked {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if r.Locked && !s.Locked {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.State < r.State {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if s.State > r.State {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goroutine represents the state of one goroutine, including the stack trace.
|
||||||
|
type Goroutine struct {
|
||||||
|
Signature // It's stack trace, internal bits, state, which call site created it, etc.
|
||||||
|
ID int // Goroutine ID.
|
||||||
|
First bool // First is the goroutine first printed, normally the one that crashed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucketize returns the number of similar goroutines.
|
||||||
|
func Bucketize(goroutines []Goroutine, similar Similarity) map[*Signature][]Goroutine {
|
||||||
|
out := map[*Signature][]Goroutine{}
|
||||||
|
// O(n²). Fix eventually.
|
||||||
|
for _, routine := range goroutines {
|
||||||
|
found := false
|
||||||
|
for key := range out {
|
||||||
|
// When a match is found, this effectively drops the other goroutine ID.
|
||||||
|
if key.Similar(&routine.Signature, similar) {
|
||||||
|
found = true
|
||||||
|
if !key.Equal(&routine.Signature) {
|
||||||
|
// Almost but not quite equal. There's different pointers passed
|
||||||
|
// around but the same values. Zap out the different values.
|
||||||
|
newKey := key.Merge(&routine.Signature)
|
||||||
|
out[newKey] = append(out[key], routine)
|
||||||
|
delete(out, key)
|
||||||
|
} else {
|
||||||
|
out[key] = append(out[key], routine)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
key := &Signature{}
|
||||||
|
*key = routine.Signature
|
||||||
|
out[key] = []Goroutine{routine}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket is a stack trace signature and the list of goroutines that fits this
|
||||||
|
// signature.
|
||||||
|
type Bucket struct {
|
||||||
|
Signature
|
||||||
|
Routines []Goroutine
|
||||||
|
}
|
||||||
|
|
||||||
|
// First returns true if it contains the first goroutine, e.g. the ones that
|
||||||
|
// likely generated the panic() call, if any.
|
||||||
|
func (b *Bucket) First() bool {
|
||||||
|
for _, r := range b.Routines {
|
||||||
|
if r.First {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less does reverse sort.
|
||||||
|
func (b *Bucket) Less(r *Bucket) bool {
|
||||||
|
if b.First() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if r.First() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return b.Signature.Less(&r.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buckets is a list of Bucket sorted by repeation count.
|
||||||
|
type Buckets []Bucket
|
||||||
|
|
||||||
|
func (b Buckets) Len() int {
|
||||||
|
return len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Buckets) Less(i, j int) bool {
|
||||||
|
return b[i].Less(&b[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Buckets) Swap(i, j int) {
|
||||||
|
b[j], b[i] = b[i], b[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortBuckets creates a list of Bucket from each goroutine stack trace count.
|
||||||
|
func SortBuckets(buckets map[*Signature][]Goroutine) Buckets {
|
||||||
|
out := make(Buckets, 0, len(buckets))
|
||||||
|
for signature, count := range buckets {
|
||||||
|
out = append(out, Bucket{*signature, count})
|
||||||
|
}
|
||||||
|
sort.Sort(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanLines is similar to bufio.ScanLines except that it:
|
||||||
|
// - doesn't drop '\n'
|
||||||
|
// - doesn't strip '\r'
|
||||||
|
// - returns when the data is bufio.MaxScanTokenSize bytes
|
||||||
|
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
if atEOF && len(data) == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||||
|
return i + 1, data[0 : i+1], nil
|
||||||
|
}
|
||||||
|
if atEOF {
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
if len(data) >= bufio.MaxScanTokenSize {
|
||||||
|
// Returns the line even if it is not at EOF nor has a '\n', otherwise the
|
||||||
|
// scanner will return bufio.ErrTooLong which is definitely not what we
|
||||||
|
// want.
|
||||||
|
return len(data), data, nil
|
||||||
|
}
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDump processes the output from runtime.Stack().
|
||||||
|
//
|
||||||
|
// It supports piping from another command and assumes there is junk before the
|
||||||
|
// actual stack trace. The junk is streamed to out.
|
||||||
|
func ParseDump(r io.Reader, out io.Writer) ([]Goroutine, error) {
|
||||||
|
goroutines := make([]Goroutine, 0, 16)
|
||||||
|
var goroutine *Goroutine
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
scanner.Split(scanLines)
|
||||||
|
// TODO(maruel): Use a formal state machine. Patterns follows:
|
||||||
|
// - reRoutineHeader
|
||||||
|
// Either:
|
||||||
|
// - reUnavail
|
||||||
|
// - reFunc + reFile in a loop
|
||||||
|
// - reElided
|
||||||
|
// Optionally ends with:
|
||||||
|
// - reCreated + reFile
|
||||||
|
// Between each goroutine stack dump: an empty line
|
||||||
|
created := false
|
||||||
|
// firstLine is the first line after the reRoutineHeader header line.
|
||||||
|
firstLine := false
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if line == "\n" || line == "\r\n" {
|
||||||
|
if goroutine != nil {
|
||||||
|
goroutine = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if line[len(line)-1] == '\n' {
|
||||||
|
if goroutine == nil {
|
||||||
|
if match := reRoutineHeader.FindStringSubmatch(line); match != nil {
|
||||||
|
if id, err := strconv.Atoi(match[1]); err == nil {
|
||||||
|
// See runtime/traceback.go.
|
||||||
|
// "<state>, \d+ minutes, locked to thread"
|
||||||
|
items := strings.Split(match[2], ", ")
|
||||||
|
sleep := 0
|
||||||
|
locked := false
|
||||||
|
for i := 1; i < len(items); i++ {
|
||||||
|
if items[i] == lockedToThread {
|
||||||
|
locked = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Look for duration, if any.
|
||||||
|
if match2 := reMinutes.FindStringSubmatch(items[i]); match2 != nil {
|
||||||
|
sleep, _ = strconv.Atoi(match2[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goroutines = append(goroutines, Goroutine{
|
||||||
|
Signature: Signature{
|
||||||
|
State: items[0],
|
||||||
|
SleepMin: sleep,
|
||||||
|
SleepMax: sleep,
|
||||||
|
Locked: locked,
|
||||||
|
},
|
||||||
|
ID: id,
|
||||||
|
First: len(goroutines) == 0,
|
||||||
|
})
|
||||||
|
goroutine = &goroutines[len(goroutines)-1]
|
||||||
|
firstLine = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if firstLine {
|
||||||
|
firstLine = false
|
||||||
|
if match := reUnavail.FindStringSubmatch(line); match != nil {
|
||||||
|
// Generate a fake stack entry.
|
||||||
|
goroutine.Stack.Calls = []Call{{SourcePath: "<unavailable>"}}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := reFile.FindStringSubmatch(line); match != nil {
|
||||||
|
// Triggers after a reFunc or a reCreated.
|
||||||
|
num, err := strconv.Atoi(match[2])
|
||||||
|
if err != nil {
|
||||||
|
return goroutines, fmt.Errorf("failed to parse int on line: \"%s\"", line)
|
||||||
|
}
|
||||||
|
if created {
|
||||||
|
created = false
|
||||||
|
goroutine.CreatedBy.SourcePath = match[1]
|
||||||
|
goroutine.CreatedBy.Line = num
|
||||||
|
} else {
|
||||||
|
i := len(goroutine.Stack.Calls) - 1
|
||||||
|
if i < 0 {
|
||||||
|
return goroutines, errors.New("unexpected order")
|
||||||
|
}
|
||||||
|
goroutine.Stack.Calls[i].SourcePath = match[1]
|
||||||
|
goroutine.Stack.Calls[i].Line = num
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := reCreated.FindStringSubmatch(line); match != nil {
|
||||||
|
created = true
|
||||||
|
goroutine.CreatedBy.Func.Raw = match[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := reFunc.FindStringSubmatch(line); match != nil {
|
||||||
|
args := Args{}
|
||||||
|
for _, a := range strings.Split(match[2], ", ") {
|
||||||
|
if a == "..." {
|
||||||
|
args.Elided = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a == "" {
|
||||||
|
// Remaining values were dropped.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseUint(a, 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
return goroutines, fmt.Errorf("failed to parse int on line: \"%s\"", line)
|
||||||
|
}
|
||||||
|
args.Values = append(args.Values, Arg{Value: v})
|
||||||
|
}
|
||||||
|
goroutine.Stack.Calls = append(goroutine.Stack.Calls, Call{Func: Function{match[1]}, Args: args})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := reElided.FindStringSubmatch(line); match != nil {
|
||||||
|
goroutine.Stack.Elided = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, _ = io.WriteString(out, line)
|
||||||
|
goroutine = nil
|
||||||
|
}
|
||||||
|
nameArguments(goroutines)
|
||||||
|
return goroutines, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private stuff.
|
||||||
|
|
||||||
|
func nameArguments(goroutines []Goroutine) {
|
||||||
|
// Set a name for any pointer occuring more than once.
|
||||||
|
type object struct {
|
||||||
|
args []*Arg
|
||||||
|
inPrimary bool
|
||||||
|
id int
|
||||||
|
}
|
||||||
|
objects := map[uint64]object{}
|
||||||
|
// Enumerate all the arguments.
|
||||||
|
for i := range goroutines {
|
||||||
|
for j := range goroutines[i].Stack.Calls {
|
||||||
|
for k := range goroutines[i].Stack.Calls[j].Args.Values {
|
||||||
|
arg := goroutines[i].Stack.Calls[j].Args.Values[k]
|
||||||
|
if arg.IsPtr() {
|
||||||
|
objects[arg.Value] = object{
|
||||||
|
args: append(objects[arg.Value].args, &goroutines[i].Stack.Calls[j].Args.Values[k]),
|
||||||
|
inPrimary: objects[arg.Value].inPrimary || i == 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CreatedBy.Args is never set.
|
||||||
|
}
|
||||||
|
order := uint64Slice{}
|
||||||
|
for k, obj := range objects {
|
||||||
|
if len(obj.args) > 1 && obj.inPrimary {
|
||||||
|
order = append(order, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(order)
|
||||||
|
nextID := 1
|
||||||
|
for _, k := range order {
|
||||||
|
for _, arg := range objects[k].args {
|
||||||
|
arg.Name = fmt.Sprintf("#%d", nextID)
|
||||||
|
}
|
||||||
|
nextID++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now do the rest. This is done so the output is deterministic.
|
||||||
|
order = uint64Slice{}
|
||||||
|
for k := range objects {
|
||||||
|
order = append(order, k)
|
||||||
|
}
|
||||||
|
sort.Sort(order)
|
||||||
|
for _, k := range order {
|
||||||
|
// Process the remaining pointers, they were not referenced by primary
|
||||||
|
// thread so will have higher IDs.
|
||||||
|
if objects[k].inPrimary {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, arg := range objects[k].args {
|
||||||
|
arg.Name = fmt.Sprintf("#%d", nextID)
|
||||||
|
}
|
||||||
|
nextID++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type uint64Slice []uint64
|
||||||
|
|
||||||
|
func (a uint64Slice) Len() int { return len(a) }
|
||||||
|
func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] }
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2016 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Palette defines the color used.
|
||||||
|
//
|
||||||
|
// An empty object Palette{} can be used to disable coloring.
|
||||||
|
type Palette struct {
|
||||||
|
EOLReset string
|
||||||
|
|
||||||
|
// Routine header.
|
||||||
|
RoutineFirst string // The first routine printed.
|
||||||
|
Routine string // Following routines.
|
||||||
|
CreatedBy string
|
||||||
|
|
||||||
|
// Call line.
|
||||||
|
Package string
|
||||||
|
SourceFile string
|
||||||
|
FunctionStdLib string
|
||||||
|
FunctionStdLibExported string
|
||||||
|
FunctionMain string
|
||||||
|
FunctionOther string
|
||||||
|
FunctionOtherExported string
|
||||||
|
Arguments string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalcLengths returns the maximum length of the source lines and package names.
|
||||||
|
func CalcLengths(buckets Buckets, fullPath bool) (int, int) {
|
||||||
|
srcLen := 0
|
||||||
|
pkgLen := 0
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
for _, line := range bucket.Signature.Stack.Calls {
|
||||||
|
l := 0
|
||||||
|
if fullPath {
|
||||||
|
l = len(line.FullSourceLine())
|
||||||
|
} else {
|
||||||
|
l = len(line.SourceLine())
|
||||||
|
}
|
||||||
|
if l > srcLen {
|
||||||
|
srcLen = l
|
||||||
|
}
|
||||||
|
l = len(line.Func.PkgName())
|
||||||
|
if l > pkgLen {
|
||||||
|
pkgLen = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return srcLen, pkgLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// functionColor returns the color to be used for the function name based on
|
||||||
|
// the type of package the function is in.
|
||||||
|
func (p *Palette) functionColor(line *Call) string {
|
||||||
|
if line.IsStdlib() {
|
||||||
|
if line.Func.IsExported() {
|
||||||
|
return p.FunctionStdLibExported
|
||||||
|
}
|
||||||
|
return p.FunctionStdLib
|
||||||
|
} else if line.IsPkgMain() {
|
||||||
|
return p.FunctionMain
|
||||||
|
} else if line.Func.IsExported() {
|
||||||
|
return p.FunctionOtherExported
|
||||||
|
}
|
||||||
|
return p.FunctionOther
|
||||||
|
}
|
||||||
|
|
||||||
|
// routineColor returns the color for the header of the goroutines bucket.
|
||||||
|
func (p *Palette) routineColor(bucket *Bucket, multipleBuckets bool) string {
|
||||||
|
if bucket.First() && multipleBuckets {
|
||||||
|
return p.RoutineFirst
|
||||||
|
}
|
||||||
|
return p.Routine
|
||||||
|
}
|
||||||
|
|
||||||
|
// BucketHeader prints the header of a goroutine signature.
|
||||||
|
func (p *Palette) BucketHeader(bucket *Bucket, fullPath, multipleBuckets bool) string {
|
||||||
|
extra := ""
|
||||||
|
if bucket.SleepMax != 0 {
|
||||||
|
if bucket.SleepMin != bucket.SleepMax {
|
||||||
|
extra += fmt.Sprintf(" [%d~%d minutes]", bucket.SleepMin, bucket.SleepMax)
|
||||||
|
} else {
|
||||||
|
extra += fmt.Sprintf(" [%d minutes]", bucket.SleepMax)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bucket.Locked {
|
||||||
|
extra += " [locked]"
|
||||||
|
}
|
||||||
|
created := bucket.CreatedBy.Func.PkgDotName()
|
||||||
|
if created != "" {
|
||||||
|
created += " @ "
|
||||||
|
if fullPath {
|
||||||
|
created += bucket.CreatedBy.FullSourceLine()
|
||||||
|
} else {
|
||||||
|
created += bucket.CreatedBy.SourceLine()
|
||||||
|
}
|
||||||
|
extra += p.CreatedBy + " [Created by " + created + "]"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s%d: %s%s%s\n",
|
||||||
|
p.routineColor(bucket, multipleBuckets), len(bucket.Routines),
|
||||||
|
bucket.State, extra,
|
||||||
|
p.EOLReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// callLine prints one stack line.
|
||||||
|
func (p *Palette) callLine(line *Call, srcLen, pkgLen int, fullPath bool) string {
|
||||||
|
src := ""
|
||||||
|
if fullPath {
|
||||||
|
src = line.FullSourceLine()
|
||||||
|
} else {
|
||||||
|
src = line.SourceLine()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(
|
||||||
|
" %s%-*s %s%-*s %s%s%s(%s)%s",
|
||||||
|
p.Package, pkgLen, line.Func.PkgName(),
|
||||||
|
p.SourceFile, srcLen, src,
|
||||||
|
p.functionColor(line), line.Func.Name(),
|
||||||
|
p.Arguments, line.Args,
|
||||||
|
p.EOLReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackLines prints one complete stack trace, without the header.
|
||||||
|
func (p *Palette) StackLines(signature *Signature, srcLen, pkgLen int, fullPath bool) string {
|
||||||
|
out := make([]string, len(signature.Stack.Calls))
|
||||||
|
for i := range signature.Stack.Calls {
|
||||||
|
out[i] = p.callLine(&signature.Stack.Calls[i], srcLen, pkgLen, fullPath)
|
||||||
|
}
|
||||||
|
if signature.Stack.Elided {
|
||||||
|
out = append(out, " (...)")
|
||||||
|
}
|
||||||
|
return strings.Join(out, "\n") + "\n"
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright 2016 Marc-Antoine Ruel. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package stack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/maruel/ut"
|
||||||
|
)
|
||||||
|
|
||||||
|
var p = &Palette{
|
||||||
|
EOLReset: "A",
|
||||||
|
RoutineFirst: "B",
|
||||||
|
Routine: "C",
|
||||||
|
CreatedBy: "D",
|
||||||
|
Package: "E",
|
||||||
|
SourceFile: "F",
|
||||||
|
FunctionStdLib: "G",
|
||||||
|
FunctionStdLibExported: "H",
|
||||||
|
FunctionMain: "I",
|
||||||
|
FunctionOther: "J",
|
||||||
|
FunctionOtherExported: "K",
|
||||||
|
Arguments: "L",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcLengths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
b := Buckets{
|
||||||
|
{
|
||||||
|
Signature{Stack: Stack{Calls: []Call{{SourcePath: "/gopath/baz.go", Func: Function{"main.func·001"}}}}},
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
srcLen, pkgLen := CalcLengths(b, true)
|
||||||
|
ut.AssertEqual(t, 16, srcLen)
|
||||||
|
ut.AssertEqual(t, 4, pkgLen)
|
||||||
|
srcLen, pkgLen = CalcLengths(b, false)
|
||||||
|
ut.AssertEqual(t, 8, srcLen)
|
||||||
|
ut.AssertEqual(t, 4, pkgLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBucketHeader(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
b := &Bucket{
|
||||||
|
Signature{
|
||||||
|
State: "chan receive",
|
||||||
|
CreatedBy: Call{
|
||||||
|
SourcePath: "/gopath/src/github.com/foo/bar/baz.go",
|
||||||
|
Line: 74,
|
||||||
|
Func: Function{"main.mainImpl"},
|
||||||
|
},
|
||||||
|
SleepMax: 6,
|
||||||
|
SleepMin: 2,
|
||||||
|
},
|
||||||
|
[]Goroutine{
|
||||||
|
{
|
||||||
|
First: true,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ut.AssertEqual(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ /gopath/src/github.com/foo/bar/baz.go:74]A\n", p.BucketHeader(b, true, true))
|
||||||
|
ut.AssertEqual(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ /gopath/src/github.com/foo/bar/baz.go:74]A\n", p.BucketHeader(b, true, false))
|
||||||
|
ut.AssertEqual(t, "B2: chan receive [2~6 minutes]D [Created by main.mainImpl @ baz.go:74]A\n", p.BucketHeader(b, false, true))
|
||||||
|
ut.AssertEqual(t, "C2: chan receive [2~6 minutes]D [Created by main.mainImpl @ baz.go:74]A\n", p.BucketHeader(b, false, false))
|
||||||
|
|
||||||
|
b = &Bucket{
|
||||||
|
Signature{
|
||||||
|
State: "b0rked",
|
||||||
|
SleepMax: 6,
|
||||||
|
SleepMin: 6,
|
||||||
|
Locked: true,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
ut.AssertEqual(t, "C0: b0rked [6 minutes] [locked]A\n", p.BucketHeader(b, false, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStackLines(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := &Signature{
|
||||||
|
State: "idle",
|
||||||
|
Stack: Stack{
|
||||||
|
Calls: []Call{
|
||||||
|
{
|
||||||
|
SourcePath: goroot + "/src/runtime/sys_linux_amd64.s",
|
||||||
|
Line: 400,
|
||||||
|
Func: Function{"runtime.Epollwait"},
|
||||||
|
Args: Args{
|
||||||
|
Values: []Arg{
|
||||||
|
{Value: 0x4},
|
||||||
|
{Value: 0x7fff671c7118},
|
||||||
|
{Value: 0xffffffff00000080},
|
||||||
|
{},
|
||||||
|
{Value: 0xffffffff0028c1be},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
Elided: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SourcePath: goroot + "/src/runtime/netpoll_epoll.go",
|
||||||
|
Line: 68,
|
||||||
|
Func: Function{"runtime.netpoll"},
|
||||||
|
Args: Args{Values: []Arg{{Value: 0x901b01}, {}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SourcePath: "/src/main.go",
|
||||||
|
Line: 1472,
|
||||||
|
Func: Function{"main.Main"},
|
||||||
|
Args: Args{Values: []Arg{{Value: 0xc208012000}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SourcePath: "/src/foo/bar.go",
|
||||||
|
Line: 1575,
|
||||||
|
Func: Function{"foo.OtherExported"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SourcePath: "/src/foo/bar.go",
|
||||||
|
Line: 10,
|
||||||
|
Func: Function{"foo.otherPrivate"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Elided: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expected := "" +
|
||||||
|
" Eruntime F" + goroot + "/src/runtime/sys_linux_amd64.s:400 HEpollwaitL(0x4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, 0, 0, 0, 0, 0, ...)A\n" +
|
||||||
|
" Eruntime F" + goroot + "/src/runtime/netpoll_epoll.go:68 GnetpollL(0x901b01, 0)A\n" +
|
||||||
|
" Emain F/src/main.go:1472 IMainL(0xc208012000)A\n" +
|
||||||
|
" Efoo F/src/foo/bar.go:1575 KOtherExportedL()A\n" +
|
||||||
|
" Efoo F/src/foo/bar.go:10 JotherPrivateL()A\n" +
|
||||||
|
" (...)\n"
|
||||||
|
ut.AssertEqual(t, expected, p.StackLines(s, 10, 10, true))
|
||||||
|
expected = "" +
|
||||||
|
" Eruntime Fsys_linux_amd64.s:400 HEpollwaitL(0x4, 0x7fff671c7118, 0xffffffff00000080, 0, 0xffffffff0028c1be, 0, 0, 0, 0, 0, ...)A\n" +
|
||||||
|
" Eruntime Fnetpoll_epoll.go:68 GnetpollL(0x901b01, 0)A\n" +
|
||||||
|
" Emain Fmain.go:1472 IMainL(0xc208012000)A\n" +
|
||||||
|
" Efoo Fbar.go:1575 KOtherExportedL()A\n" +
|
||||||
|
" Efoo Fbar.go:10 JotherPrivateL()A\n" +
|
||||||
|
" (...)\n"
|
||||||
|
ut.AssertEqual(t, expected, p.StackLines(s, 10, 10, false))
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mattn/go-colorable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
stdOut := bufio.NewWriter(colorable.NewColorableStdout())
|
|
||||||
|
|
||||||
fmt.Fprint(stdOut, "\x1B[3GMove to 3rd Column\n")
|
|
||||||
fmt.Fprint(stdOut, "\x1B[1;2HMove to 2nd Column on 1st Line\n")
|
|
||||||
stdOut.Flush()
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/mattn/go-colorable"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
|
||||||
logrus.SetOutput(colorable.NewColorableStdout())
|
|
||||||
|
|
||||||
logrus.Info("succeeded")
|
|
||||||
logrus.Warn("not correct")
|
|
||||||
logrus.Error("something error")
|
|
||||||
logrus.Fatal("panic")
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
. "github.com/mattn/go-colorable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
out := NewColorableStdout()
|
|
||||||
fmt.Fprint(out, "\x1B]0;TITLE Changed\007(See title and hit any key)")
|
|
||||||
var c [1]byte
|
|
||||||
os.Stdin.Read(c[:])
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
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,27 @@
|
||||||
|
go-runewidth
|
||||||
|
============
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/mattn/go-runewidth/badge.png?branch=HEAD)](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth)
|
||||||
|
|
||||||
|
Provides functions to get fixed width of the character or string.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
```go
|
||||||
|
runewidth.StringWidth("つのだ☆HIRO") == 12
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
under the MIT License: http://mattn.mit-license.org/2013
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
// +build js
|
||||||
|
|
||||||
|
package runewidth
|
||||||
|
|
||||||
|
func IsEastAsian() bool {
|
||||||
|
// TODO: Implement this for the web. Detect east asian in a compatible way, and return true.
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
// +build !windows,!js
|
||||||
|
|
||||||
|
package runewidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
|
||||||
|
|
||||||
|
var mblenTable = map[string]int{
|
||||||
|
"utf-8": 6,
|
||||||
|
"utf8": 6,
|
||||||
|
"jis": 8,
|
||||||
|
"eucjp": 3,
|
||||||
|
"euckr": 2,
|
||||||
|
"euccn": 2,
|
||||||
|
"sjis": 2,
|
||||||
|
"cp932": 2,
|
||||||
|
"cp51932": 2,
|
||||||
|
"cp936": 2,
|
||||||
|
"cp949": 2,
|
||||||
|
"cp950": 2,
|
||||||
|
"big5": 2,
|
||||||
|
"gbk": 2,
|
||||||
|
"gb2312": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEastAsian(locale string) bool {
|
||||||
|
charset := strings.ToLower(locale)
|
||||||
|
r := reLoc.FindStringSubmatch(locale)
|
||||||
|
if len(r) == 2 {
|
||||||
|
charset = strings.ToLower(r[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(charset, "@cjk_narrow") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for pos, b := range []byte(charset) {
|
||||||
|
if b == '@' {
|
||||||
|
charset = charset[:pos]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max := 1
|
||||||
|
if m, ok := mblenTable[charset]; ok {
|
||||||
|
max = m
|
||||||
|
}
|
||||||
|
if max > 1 && (charset[0] != 'u' ||
|
||||||
|
strings.HasPrefix(locale, "ja") ||
|
||||||
|
strings.HasPrefix(locale, "ko") ||
|
||||||
|
strings.HasPrefix(locale, "zh")) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEastAsian return true if the current locale is CJK
|
||||||
|
func IsEastAsian() bool {
|
||||||
|
locale := os.Getenv("LC_CTYPE")
|
||||||
|
if locale == "" {
|
||||||
|
locale = os.Getenv("LANG")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore C locale
|
||||||
|
if locale == "POSIX" || locale == "C" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEastAsian(locale)
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
package runewidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sort.Interface = (*table)(nil)
|
||||||
|
|
||||||
|
func (t table) Len() int {
|
||||||
|
return len(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t table) Less(i, j int) bool {
|
||||||
|
return t[i].first < t[j].first
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *table) Swap(i, j int) {
|
||||||
|
(*t)[i], (*t)[j] = (*t)[j], (*t)[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
var tables = []table{
|
||||||
|
private,
|
||||||
|
nonprint,
|
||||||
|
combining,
|
||||||
|
doublewidth,
|
||||||
|
ambiguous,
|
||||||
|
emoji,
|
||||||
|
notassigned,
|
||||||
|
neutral,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSorted(t *testing.T) {
|
||||||
|
for _, tbl := range tables {
|
||||||
|
if !sort.IsSorted(&tbl) {
|
||||||
|
t.Errorf("not sorted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runewidthtests = []struct {
|
||||||
|
in rune
|
||||||
|
out int
|
||||||
|
eaout int
|
||||||
|
}{
|
||||||
|
{'世', 2, 2},
|
||||||
|
{'界', 2, 2},
|
||||||
|
{'セ', 1, 1},
|
||||||
|
{'カ', 1, 1},
|
||||||
|
{'イ', 1, 1},
|
||||||
|
{'☆', 1, 2}, // double width in ambiguous
|
||||||
|
{'\x00', 0, 0},
|
||||||
|
{'\x01', 0, 0},
|
||||||
|
{'\u0300', 0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneWidth(t *testing.T) {
|
||||||
|
c := NewCondition()
|
||||||
|
for _, tt := range runewidthtests {
|
||||||
|
if out := c.RuneWidth(tt.in); out != tt.out {
|
||||||
|
t.Errorf("RuneWidth(%q) = %d, want %d", tt.in, out, tt.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.EastAsianWidth = true
|
||||||
|
for _, tt := range runewidthtests {
|
||||||
|
if out := c.RuneWidth(tt.in); out != tt.eaout {
|
||||||
|
t.Errorf("RuneWidth(%q) = %d, want %d", tt.in, out, tt.eaout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isambiguouswidthtests = []struct {
|
||||||
|
in rune
|
||||||
|
out bool
|
||||||
|
}{
|
||||||
|
{'世', false},
|
||||||
|
{'■', true},
|
||||||
|
{'界', false},
|
||||||
|
{'○', true},
|
||||||
|
{'㈱', false},
|
||||||
|
{'①', true},
|
||||||
|
{'②', true},
|
||||||
|
{'③', true},
|
||||||
|
{'④', true},
|
||||||
|
{'⑤', true},
|
||||||
|
{'⑥', true},
|
||||||
|
{'⑦', true},
|
||||||
|
{'⑧', true},
|
||||||
|
{'⑨', true},
|
||||||
|
{'⑩', true},
|
||||||
|
{'⑪', true},
|
||||||
|
{'⑫', true},
|
||||||
|
{'⑬', true},
|
||||||
|
{'⑭', true},
|
||||||
|
{'⑮', true},
|
||||||
|
{'⑯', true},
|
||||||
|
{'⑰', true},
|
||||||
|
{'⑱', true},
|
||||||
|
{'⑲', true},
|
||||||
|
{'⑳', true},
|
||||||
|
{'☆', true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAmbiguousWidth(t *testing.T) {
|
||||||
|
for _, tt := range isambiguouswidthtests {
|
||||||
|
if out := IsAmbiguousWidth(tt.in); out != tt.out {
|
||||||
|
t.Errorf("IsAmbiguousWidth(%q) = %v, want %v", tt.in, out, tt.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringwidthtests = []struct {
|
||||||
|
in string
|
||||||
|
out int
|
||||||
|
eaout int
|
||||||
|
}{
|
||||||
|
{"■㈱の世界①", 10, 12},
|
||||||
|
{"スター☆", 7, 8},
|
||||||
|
{"つのだ☆HIRO", 11, 12},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringWidth(t *testing.T) {
|
||||||
|
c := NewCondition()
|
||||||
|
for _, tt := range stringwidthtests {
|
||||||
|
if out := c.StringWidth(tt.in); out != tt.out {
|
||||||
|
t.Errorf("StringWidth(%q) = %q, want %q", tt.in, out, tt.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.EastAsianWidth = true
|
||||||
|
for _, tt := range stringwidthtests {
|
||||||
|
if out := c.StringWidth(tt.in); out != tt.eaout {
|
||||||
|
t.Errorf("StringWidth(%q) = %q, want %q", tt.in, out, tt.eaout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringWidthInvalid(t *testing.T) {
|
||||||
|
s := "こんにちわ\x00世界"
|
||||||
|
if out := StringWidth(s); out != 14 {
|
||||||
|
t.Errorf("StringWidth(%q) = %q, want %q", s, out, 14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateSmaller(t *testing.T) {
|
||||||
|
s := "あいうえお"
|
||||||
|
expected := "あいうえお"
|
||||||
|
|
||||||
|
if out := Truncate(s, 10, "..."); out != expected {
|
||||||
|
t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncate(t *testing.T) {
|
||||||
|
s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
|
||||||
|
expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..."
|
||||||
|
out := Truncate(s, 80, "...")
|
||||||
|
if out != expected {
|
||||||
|
t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
width := StringWidth(out)
|
||||||
|
if width != 79 {
|
||||||
|
t.Errorf("width of Truncate(%q) should be %d, but %d", s, 79, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateFit(t *testing.T) {
|
||||||
|
s := "aあいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
|
||||||
|
expected := "aあいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..."
|
||||||
|
|
||||||
|
out := Truncate(s, 80, "...")
|
||||||
|
if out != expected {
|
||||||
|
t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
width := StringWidth(out)
|
||||||
|
if width != 80 {
|
||||||
|
t.Errorf("width of Truncate(%q) should be %d, but %d", s, 80, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateJustFit(t *testing.T) {
|
||||||
|
s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
|
||||||
|
expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおお"
|
||||||
|
|
||||||
|
out := Truncate(s, 80, "...")
|
||||||
|
if out != expected {
|
||||||
|
t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
width := StringWidth(out)
|
||||||
|
if width != 80 {
|
||||||
|
t.Errorf("width of Truncate(%q) should be %d, but %d", s, 80, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
s := `東京特許許可局局長はよく柿喰う客だ/東京特許許可局局長はよく柿喰う客だ
|
||||||
|
123456789012345678901234567890
|
||||||
|
|
||||||
|
END`
|
||||||
|
expected := `東京特許許可局局長はよく柿喰う
|
||||||
|
客だ/東京特許許可局局長はよく
|
||||||
|
柿喰う客だ
|
||||||
|
123456789012345678901234567890
|
||||||
|
|
||||||
|
END`
|
||||||
|
|
||||||
|
if out := Wrap(s, 30); out != expected {
|
||||||
|
t.Errorf("Wrap(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncateNoNeeded(t *testing.T) {
|
||||||
|
s := "あいうえおあい"
|
||||||
|
expected := "あいうえおあい"
|
||||||
|
|
||||||
|
if out := Truncate(s, 80, "..."); out != expected {
|
||||||
|
t.Errorf("Truncate(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isneutralwidthtests = []struct {
|
||||||
|
in rune
|
||||||
|
out bool
|
||||||
|
}{
|
||||||
|
{'→', false},
|
||||||
|
{'┊', false},
|
||||||
|
{'┈', false},
|
||||||
|
{'~', false},
|
||||||
|
{'└', false},
|
||||||
|
{'⣀', true},
|
||||||
|
{'⣀', true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsNeutralWidth(t *testing.T) {
|
||||||
|
for _, tt := range isneutralwidthtests {
|
||||||
|
if out := IsNeutralWidth(tt.in); out != tt.out {
|
||||||
|
t.Errorf("IsNeutralWidth(%q) = %v, want %v", tt.in, out, tt.out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillLeft(t *testing.T) {
|
||||||
|
s := "あxいうえお"
|
||||||
|
expected := " あxいうえお"
|
||||||
|
|
||||||
|
if out := FillLeft(s, 15); out != expected {
|
||||||
|
t.Errorf("FillLeft(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillLeftFit(t *testing.T) {
|
||||||
|
s := "あいうえお"
|
||||||
|
expected := "あいうえお"
|
||||||
|
|
||||||
|
if out := FillLeft(s, 10); out != expected {
|
||||||
|
t.Errorf("FillLeft(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillRight(t *testing.T) {
|
||||||
|
s := "あxいうえお"
|
||||||
|
expected := "あxいうえお "
|
||||||
|
|
||||||
|
if out := FillRight(s, 15); out != expected {
|
||||||
|
t.Errorf("FillRight(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillRightFit(t *testing.T) {
|
||||||
|
s := "あいうえお"
|
||||||
|
expected := "あいうえお"
|
||||||
|
|
||||||
|
if out := FillRight(s, 10); out != expected {
|
||||||
|
t.Errorf("FillRight(%q) = %q, want %q", s, out, expected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package runewidth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32")
|
||||||
|
procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsEastAsian return true if the current locale is CJK
|
||||||
|
func IsEastAsian() bool {
|
||||||
|
r1, _, _ := procGetConsoleOutputCP.Call()
|
||||||
|
if r1 == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch int(r1) {
|
||||||
|
case 932, 51932, 936, 949, 950:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,133 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
sqlite "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Computes x^y
|
|
||||||
func pow(x, y int64) int64 {
|
|
||||||
return int64(math.Pow(float64(x), float64(y)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes the bitwise exclusive-or of all its arguments
|
|
||||||
func xor(xs ...int64) int64 {
|
|
||||||
var ret int64
|
|
||||||
for _, x := range xs {
|
|
||||||
ret ^= x
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a random number. It's actually deterministic here because
|
|
||||||
// we don't seed the RNG, but it's an example of a non-pure function
|
|
||||||
// from SQLite's POV.
|
|
||||||
func getrand() int64 {
|
|
||||||
return rand.Int63()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computes the standard deviation of a GROUPed BY set of values
|
|
||||||
type stddev struct {
|
|
||||||
xs []int64
|
|
||||||
// Running average calculation
|
|
||||||
sum int64
|
|
||||||
n int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStddev() *stddev { return &stddev{} }
|
|
||||||
|
|
||||||
func (s *stddev) Step(x int64) {
|
|
||||||
s.xs = append(s.xs, x)
|
|
||||||
s.sum += x
|
|
||||||
s.n++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stddev) Done() float64 {
|
|
||||||
mean := float64(s.sum) / float64(s.n)
|
|
||||||
var sqDiff []float64
|
|
||||||
for _, x := range s.xs {
|
|
||||||
sqDiff = append(sqDiff, math.Pow(float64(x)-mean, 2))
|
|
||||||
}
|
|
||||||
var dev float64
|
|
||||||
for _, x := range sqDiff {
|
|
||||||
dev += x
|
|
||||||
}
|
|
||||||
dev /= float64(len(sqDiff))
|
|
||||||
return math.Sqrt(dev)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sql.Register("sqlite3_custom", &sqlite.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite.SQLiteConn) error {
|
|
||||||
if err := conn.RegisterFunc("pow", pow, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := conn.RegisterFunc("xor", xor, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := conn.RegisterFunc("rand", getrand, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := conn.RegisterAggregator("stddev", newStddev, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3_custom", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to open database:", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
var i int64
|
|
||||||
err = db.QueryRow("SELECT pow(2,3)").Scan(&i)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("POW query error:", err)
|
|
||||||
}
|
|
||||||
fmt.Println("pow(2,3) =", i) // 8
|
|
||||||
|
|
||||||
err = db.QueryRow("SELECT xor(1,2,3,4,5,6)").Scan(&i)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("XOR query error:", err)
|
|
||||||
}
|
|
||||||
fmt.Println("xor(1,2,3,4,5) =", i) // 7
|
|
||||||
|
|
||||||
err = db.QueryRow("SELECT rand()").Scan(&i)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("RAND query error:", err)
|
|
||||||
}
|
|
||||||
fmt.Println("rand() =", i) // pseudorandom
|
|
||||||
|
|
||||||
_, err = db.Exec("create table foo (department integer, profits integer)")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to create table:", err)
|
|
||||||
}
|
|
||||||
_, err = db.Exec("insert into foo values (1, 10), (1, 20), (1, 45), (2, 42), (2, 115)")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Failed to insert records:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := db.Query("select department, stddev(profits) from foo group by department")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("STDDEV query error:", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var dept int64
|
|
||||||
var dev float64
|
|
||||||
if err := rows.Scan(&dept, &dev); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("dept=%d stddev=%f\n", dept, dev)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sqlite3conn := []*sqlite3.SQLiteConn{}
|
|
||||||
sql.Register("sqlite3_with_hook_example",
|
|
||||||
&sqlite3.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
||||||
sqlite3conn = append(sqlite3conn, conn)
|
|
||||||
conn.RegisterUpdateHook(func(op int, db string, table string, rowid int64) {
|
|
||||||
switch op {
|
|
||||||
case sqlite3.SQLITE_INSERT:
|
|
||||||
log.Println("Notified of insert on db", db, "table", table, "rowid", rowid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
os.Remove("./foo.db")
|
|
||||||
os.Remove("./bar.db")
|
|
||||||
|
|
||||||
srcDb, err := sql.Open("sqlite3_with_hook_example", "./foo.db")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer srcDb.Close()
|
|
||||||
srcDb.Ping()
|
|
||||||
|
|
||||||
_, err = srcDb.Exec("create table foo(id int, value text)")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = srcDb.Exec("insert into foo values(1, 'foo')")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = srcDb.Exec("insert into foo values(2, 'bar')")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = srcDb.Query("select * from foo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
destDb, err := sql.Open("sqlite3_with_hook_example", "./bar.db")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer destDb.Close()
|
|
||||||
destDb.Ping()
|
|
||||||
|
|
||||||
bk, err := sqlite3conn[1].Backup("main", sqlite3conn[0], "main")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = bk.Step(-1)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = destDb.Query("select * from foo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = destDb.Exec("insert into foo values(3, 'bar')")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bk.Finish()
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func createBulkInsertQuery(n int, start int) (query string, args []interface{}) {
|
|
||||||
values := make([]string, n)
|
|
||||||
args = make([]interface{}, n*2)
|
|
||||||
pos := 0
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
values[i] = "(?, ?)"
|
|
||||||
args[pos] = start + i
|
|
||||||
args[pos+1] = fmt.Sprintf("こんにちわ世界%03d", i)
|
|
||||||
pos += 2
|
|
||||||
}
|
|
||||||
query = fmt.Sprintf(
|
|
||||||
"insert into foo(id, name) values %s",
|
|
||||||
strings.Join(values, ", "),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func bukInsert(db *sql.DB, query string, args []interface{}) (err error) {
|
|
||||||
stmt, err := db.Prepare(query)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = stmt.Exec(args...)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var sqlite3conn *sqlite3.SQLiteConn
|
|
||||||
sql.Register("sqlite3_with_limit", &sqlite3.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
||||||
sqlite3conn = conn
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
os.Remove("./foo.db")
|
|
||||||
db, err := sql.Open("sqlite3_with_limit", "./foo.db")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
sqlStmt := `
|
|
||||||
create table foo (id integer not null primary key, name text);
|
|
||||||
delete from foo;
|
|
||||||
`
|
|
||||||
_, err = db.Exec(sqlStmt)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%q: %s\n", err, sqlStmt)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if sqlite3conn == nil {
|
|
||||||
log.Fatal("not set sqlite3 connection")
|
|
||||||
}
|
|
||||||
|
|
||||||
limitVariableNumber := sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
|
||||||
log.Printf("default SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
|
||||||
|
|
||||||
num := 400
|
|
||||||
query, args := createBulkInsertQuery(num, 0)
|
|
||||||
err = bukInsert(db, query, args)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
smallLimitVariableNumber := 100
|
|
||||||
sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, smallLimitVariableNumber)
|
|
||||||
|
|
||||||
limitVariableNumber = sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
|
||||||
log.Printf("updated SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
|
||||||
|
|
||||||
query, args = createBulkInsertQuery(num, num)
|
|
||||||
err = bukInsert(db, query, args)
|
|
||||||
if err != nil {
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("expect failed since SQLITE_LIMIT_VARIABLE_NUMBER is too small: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bigLimitVariableNumber := 999999
|
|
||||||
sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, bigLimitVariableNumber)
|
|
||||||
limitVariableNumber = sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
|
||||||
log.Printf("set SQLITE_LIMIT_VARIABLE_NUMBER: %d", bigLimitVariableNumber)
|
|
||||||
log.Printf("updated SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
|
||||||
|
|
||||||
query, args = createBulkInsertQuery(500, num+num)
|
|
||||||
err = bukInsert(db, query, args)
|
|
||||||
if err != nil {
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("no error if SQLITE_LIMIT_VARIABLE_NUMBER > 999")
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
ifeq ($(OS),Windows_NT)
|
|
||||||
EXE=extension.exe
|
|
||||||
EXT=sqlite3_mod_regexp.dll
|
|
||||||
RM=cmd /c del
|
|
||||||
LDFLAG=
|
|
||||||
else
|
|
||||||
EXE=extension
|
|
||||||
EXT=sqlite3_mod_regexp.so
|
|
||||||
RM=rm
|
|
||||||
LDFLAG=-fPIC
|
|
||||||
endif
|
|
||||||
|
|
||||||
all : $(EXE) $(EXT)
|
|
||||||
|
|
||||||
$(EXE) : extension.go
|
|
||||||
go build $<
|
|
||||||
|
|
||||||
$(EXT) : sqlite3_mod_regexp.c
|
|
||||||
gcc $(LDFLAG) -shared -o $@ $< -lsqlite3 -lpcre
|
|
||||||
|
|
||||||
clean :
|
|
||||||
@-$(RM) $(EXE) $(EXT)
|
|
|
@ -1,43 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sql.Register("sqlite3_with_extensions",
|
|
||||||
&sqlite3.SQLiteDriver{
|
|
||||||
Extensions: []string{
|
|
||||||
"sqlite3_mod_regexp",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Force db to make a new connection in pool
|
|
||||||
// by putting the original in a transaction
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer tx.Commit()
|
|
||||||
|
|
||||||
// New connection works (hopefully!)
|
|
||||||
rows, err := db.Query("select 'hello world' where 'hello world' regexp '^hello.*d$'")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var helloworld string
|
|
||||||
rows.Scan(&helloworld)
|
|
||||||
fmt.Println(helloworld)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
#include <pcre.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <sqlite3ext.h>
|
|
||||||
|
|
||||||
SQLITE_EXTENSION_INIT1
|
|
||||||
static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
|
|
||||||
if (argc >= 2) {
|
|
||||||
const char *target = (const char *)sqlite3_value_text(argv[1]);
|
|
||||||
const char *pattern = (const char *)sqlite3_value_text(argv[0]);
|
|
||||||
const char* errstr = NULL;
|
|
||||||
int erroff = 0;
|
|
||||||
int vec[500];
|
|
||||||
int n, rc;
|
|
||||||
pcre* re = pcre_compile(pattern, 0, &errstr, &erroff, NULL);
|
|
||||||
rc = pcre_exec(re, NULL, target, strlen(target), 0, 0, vec, 500);
|
|
||||||
if (rc <= 0) {
|
|
||||||
sqlite3_result_error(context, errstr, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sqlite3_result_int(context, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
__declspec(dllexport)
|
|
||||||
#endif
|
|
||||||
int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
|
|
||||||
SQLITE_EXTENSION_INIT2(api);
|
|
||||||
return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, (void*)db, regexp_func, NULL, NULL);
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
ifeq ($(OS),Windows_NT)
|
|
||||||
EXE=extension.exe
|
|
||||||
EXT=sqlite3_mod_vtable.dll
|
|
||||||
RM=cmd /c del
|
|
||||||
LIBCURL=-lcurldll
|
|
||||||
LDFLAG=
|
|
||||||
else
|
|
||||||
EXE=extension
|
|
||||||
EXT=sqlite3_mod_vtable.so
|
|
||||||
RM=rm
|
|
||||||
LDFLAG=-fPIC
|
|
||||||
LIBCURL=-lcurl
|
|
||||||
endif
|
|
||||||
|
|
||||||
all : $(EXE) $(EXT)
|
|
||||||
|
|
||||||
$(EXE) : extension.go
|
|
||||||
go build $<
|
|
||||||
|
|
||||||
$(EXT) : sqlite3_mod_vtable.cc
|
|
||||||
g++ $(LDFLAG) -shared -o $@ $< -lsqlite3 $(LIBCURL)
|
|
||||||
|
|
||||||
clean :
|
|
||||||
@-$(RM) $(EXE) $(EXT)
|
|
|
@ -1,37 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sql.Register("sqlite3_with_extensions",
|
|
||||||
&sqlite3.SQLiteDriver{
|
|
||||||
Extensions: []string{
|
|
||||||
"sqlite3_mod_vtable",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
db.Exec("create virtual table repo using github(id, full_name, description, html_url)")
|
|
||||||
|
|
||||||
rows, err := db.Query("select id, full_name, description, html_url from repo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var id, fullName, description, htmlURL string
|
|
||||||
rows.Scan(&id, &fullName, &description, &htmlURL)
|
|
||||||
fmt.Printf("%s: %s\n\t%s\n\t%s\n\n", id, fullName, description, htmlURL)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,238 +0,0 @@
|
||||||
#include <string>
|
|
||||||
#include <sstream>
|
|
||||||
#include <sqlite3-binding.h>
|
|
||||||
#include <sqlite3ext.h>
|
|
||||||
#include <curl/curl.h>
|
|
||||||
#include "picojson.h"
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
# define EXPORT __declspec(dllexport)
|
|
||||||
#else
|
|
||||||
# define EXPORT
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SQLITE_EXTENSION_INIT1;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char* data; // response data from server
|
|
||||||
size_t size; // response size of data
|
|
||||||
} MEMFILE;
|
|
||||||
|
|
||||||
MEMFILE*
|
|
||||||
memfopen() {
|
|
||||||
MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
|
|
||||||
if (mf) {
|
|
||||||
mf->data = NULL;
|
|
||||||
mf->size = 0;
|
|
||||||
}
|
|
||||||
return mf;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
memfclose(MEMFILE* mf) {
|
|
||||||
if (mf->data) free(mf->data);
|
|
||||||
free(mf);
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t
|
|
||||||
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
|
|
||||||
MEMFILE* mf = (MEMFILE*) stream;
|
|
||||||
int block = size * nmemb;
|
|
||||||
if (!mf) return block; // through
|
|
||||||
if (!mf->data)
|
|
||||||
mf->data = (char*) malloc(block);
|
|
||||||
else
|
|
||||||
mf->data = (char*) realloc(mf->data, mf->size + block);
|
|
||||||
if (mf->data) {
|
|
||||||
memcpy(mf->data + mf->size, ptr, block);
|
|
||||||
mf->size += block;
|
|
||||||
}
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
char*
|
|
||||||
memfstrdup(MEMFILE* mf) {
|
|
||||||
char* buf;
|
|
||||||
if (mf->size == 0) return NULL;
|
|
||||||
buf = (char*) malloc(mf->size + 1);
|
|
||||||
memcpy(buf, mf->data, mf->size);
|
|
||||||
buf[mf->size] = 0;
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_connect(sqlite3 *db, void *pAux, int argc, const char * const *argv, sqlite3_vtab **ppVTab, char **c) {
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << "CREATE TABLE " << argv[0]
|
|
||||||
<< "(id int, full_name text, description text, html_url text)";
|
|
||||||
int rc = sqlite3_declare_vtab(db, ss.str().c_str());
|
|
||||||
*ppVTab = (sqlite3_vtab *) sqlite3_malloc(sizeof(sqlite3_vtab));
|
|
||||||
memset(*ppVTab, 0, sizeof(sqlite3_vtab));
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_create(sqlite3 *db, void *pAux, int argc, const char * const * argv, sqlite3_vtab **ppVTab, char **c) {
|
|
||||||
return my_connect(db, pAux, argc, argv, ppVTab, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int my_disconnect(sqlite3_vtab *pVTab) {
|
|
||||||
sqlite3_free(pVTab);
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_destroy(sqlite3_vtab *pVTab) {
|
|
||||||
sqlite3_free(pVTab);
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
sqlite3_vtab_cursor base;
|
|
||||||
int index;
|
|
||||||
picojson::value* rows;
|
|
||||||
} cursor;
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_open(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor) {
|
|
||||||
MEMFILE* mf;
|
|
||||||
CURL* curl;
|
|
||||||
char* json;
|
|
||||||
CURLcode res = CURLE_OK;
|
|
||||||
char error[CURL_ERROR_SIZE] = {0};
|
|
||||||
char* cert_file = getenv("SSL_CERT_FILE");
|
|
||||||
|
|
||||||
mf = memfopen();
|
|
||||||
curl = curl_easy_init();
|
|
||||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.29.0");
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repositories");
|
|
||||||
if (cert_file)
|
|
||||||
curl_easy_setopt(curl, CURLOPT_CAINFO, cert_file);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
|
|
||||||
res = curl_easy_perform(curl);
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
if (res != CURLE_OK) {
|
|
||||||
std::cerr << error << std::endl;
|
|
||||||
return SQLITE_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
picojson::value* v = new picojson::value;
|
|
||||||
std::string err;
|
|
||||||
picojson::parse(*v, mf->data, mf->data + mf->size, &err);
|
|
||||||
memfclose(mf);
|
|
||||||
|
|
||||||
if (!err.empty()) {
|
|
||||||
delete v;
|
|
||||||
std::cerr << err << std::endl;
|
|
||||||
return SQLITE_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor *c = (cursor *)sqlite3_malloc(sizeof(cursor));
|
|
||||||
c->rows = v;
|
|
||||||
c->index = 0;
|
|
||||||
*ppCursor = &c->base;
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_close(cursor *c) {
|
|
||||||
delete c->rows;
|
|
||||||
sqlite3_free(c);
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_filter(cursor *c, int idxNum, const char *idxStr, int argc, sqlite3_value **argv) {
|
|
||||||
c->index = 0;
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_next(cursor *c) {
|
|
||||||
c->index++;
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_eof(cursor *c) {
|
|
||||||
return c->index >= c->rows->get<picojson::array>().size() ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_column(cursor *c, sqlite3_context *ctxt, int i) {
|
|
||||||
picojson::value v = c->rows->get<picojson::array>()[c->index];
|
|
||||||
picojson::object row = v.get<picojson::object>();
|
|
||||||
const char* p = NULL;
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
p = row["id"].to_str().c_str();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
p = row["full_name"].to_str().c_str();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
p = row["description"].to_str().c_str();
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
p = row["html_url"].to_str().c_str();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sqlite3_result_text(ctxt, strdup(p), strlen(p), free);
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_rowid(cursor *c, sqlite3_int64 *pRowid) {
|
|
||||||
*pRowid = c->index;
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
my_bestindex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo) {
|
|
||||||
return SQLITE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const sqlite3_module module = {
|
|
||||||
0,
|
|
||||||
my_create,
|
|
||||||
my_connect,
|
|
||||||
my_bestindex,
|
|
||||||
my_disconnect,
|
|
||||||
my_destroy,
|
|
||||||
my_open,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *)) my_close,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *, int, char const *, int, sqlite3_value **)) my_filter,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *)) my_next,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *)) my_eof,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *, sqlite3_context *, int)) my_column,
|
|
||||||
(int (*)(sqlite3_vtab_cursor *, sqlite3_int64 *)) my_rowid,
|
|
||||||
NULL, // my_update
|
|
||||||
NULL, // my_begin
|
|
||||||
NULL, // my_sync
|
|
||||||
NULL, // my_commit
|
|
||||||
NULL, // my_rollback
|
|
||||||
NULL, // my_findfunction
|
|
||||||
NULL, // my_rename
|
|
||||||
};
|
|
||||||
|
|
||||||
static void
|
|
||||||
destructor(void *arg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
EXPORT int
|
|
||||||
sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
|
|
||||||
SQLITE_EXTENSION_INIT2(api);
|
|
||||||
sqlite3_create_module_v2(db, "github", &module, NULL, destructor);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
os.Remove("./foo.db")
|
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", "./foo.db")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
sqlStmt := `
|
|
||||||
create table foo (id integer not null primary key, name text);
|
|
||||||
delete from foo;
|
|
||||||
`
|
|
||||||
_, err = db.Exec(sqlStmt)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%q: %s\n", err, sqlStmt)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
_, err = stmt.Exec(i, fmt.Sprintf("こんにちわ世界%03d", i))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.Commit()
|
|
||||||
|
|
||||||
rows, err := db.Query("select id, name from foo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var id int
|
|
||||||
var name string
|
|
||||||
err = rows.Scan(&id, &name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Println(id, name)
|
|
||||||
}
|
|
||||||
err = rows.Err()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err = db.Prepare("select name from foo where id = ?")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
var name string
|
|
||||||
err = stmt.QueryRow("3").Scan(&name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Println(name)
|
|
||||||
|
|
||||||
_, err = db.Exec("delete from foo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err = db.Query("select id, name from foo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var id int
|
|
||||||
var name string
|
|
||||||
err = rows.Scan(&id, &name)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
fmt.Println(id, name)
|
|
||||||
}
|
|
||||||
err = rows.Err()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,264 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
sqlite3 "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func traceCallback(info sqlite3.TraceInfo) int {
|
|
||||||
// Not very readable but may be useful; uncomment next line in case of doubt:
|
|
||||||
//fmt.Printf("Trace: %#v\n", info)
|
|
||||||
|
|
||||||
var dbErrText string
|
|
||||||
if info.DBError.Code != 0 || info.DBError.ExtendedCode != 0 {
|
|
||||||
dbErrText = fmt.Sprintf("; DB error: %#v", info.DBError)
|
|
||||||
} else {
|
|
||||||
dbErrText = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the Statement-or-Trigger text in curly braces ('{', '}')
|
|
||||||
// since from the *paired* ASCII characters they are
|
|
||||||
// the least used in SQL syntax, therefore better visual delimiters.
|
|
||||||
// Maybe show 'ExpandedSQL' the same way as 'StmtOrTrigger'.
|
|
||||||
//
|
|
||||||
// A known use of curly braces (outside strings) is
|
|
||||||
// for ODBC escape sequences. Not likely to appear here.
|
|
||||||
//
|
|
||||||
// Template languages, etc. don't matter, we should see their *result*
|
|
||||||
// at *this* level.
|
|
||||||
// Strange curly braces in SQL code that reached the database driver
|
|
||||||
// suggest that there is a bug in the application.
|
|
||||||
// The braces are likely to be either template syntax or
|
|
||||||
// a programming language's string interpolation syntax.
|
|
||||||
|
|
||||||
var expandedText string
|
|
||||||
if info.ExpandedSQL != "" {
|
|
||||||
if info.ExpandedSQL == info.StmtOrTrigger {
|
|
||||||
expandedText = " = exp"
|
|
||||||
} else {
|
|
||||||
expandedText = fmt.Sprintf(" expanded {%q}", info.ExpandedSQL)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
expandedText = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLite docs as of September 6, 2016: Tracing and Profiling Functions
|
|
||||||
// https://www.sqlite.org/c3ref/profile.html
|
|
||||||
//
|
|
||||||
// The profile callback time is in units of nanoseconds, however
|
|
||||||
// the current implementation is only capable of millisecond resolution
|
|
||||||
// so the six least significant digits in the time are meaningless.
|
|
||||||
// Future versions of SQLite might provide greater resolution on the profiler callback.
|
|
||||||
|
|
||||||
var runTimeText string
|
|
||||||
if info.RunTimeNanosec == 0 {
|
|
||||||
if info.EventCode == sqlite3.TraceProfile {
|
|
||||||
//runTimeText = "; no time" // seems confusing
|
|
||||||
runTimeText = "; time 0" // no measurement unit
|
|
||||||
} else {
|
|
||||||
//runTimeText = "; no time" // seems useless and confusing
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const nanosPerMillisec = 1000000
|
|
||||||
if info.RunTimeNanosec%nanosPerMillisec == 0 {
|
|
||||||
runTimeText = fmt.Sprintf("; time %d ms", info.RunTimeNanosec/nanosPerMillisec)
|
|
||||||
} else {
|
|
||||||
// unexpected: better than millisecond resolution
|
|
||||||
runTimeText = fmt.Sprintf("; time %d ns!!!", info.RunTimeNanosec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var modeText string
|
|
||||||
if info.AutoCommit {
|
|
||||||
modeText = "-AC-"
|
|
||||||
} else {
|
|
||||||
modeText = "+Tx+"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Trace: ev %d %s conn 0x%x, stmt 0x%x {%q}%s%s%s\n",
|
|
||||||
info.EventCode, modeText, info.ConnHandle, info.StmtHandle,
|
|
||||||
info.StmtOrTrigger, expandedText,
|
|
||||||
runTimeText,
|
|
||||||
dbErrText)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
eventMask := sqlite3.TraceStmt | sqlite3.TraceProfile | sqlite3.TraceRow | sqlite3.TraceClose
|
|
||||||
|
|
||||||
sql.Register("sqlite3_tracing",
|
|
||||||
&sqlite3.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
||||||
err := conn.SetTrace(&sqlite3.TraceConfig{
|
|
||||||
Callback: traceCallback,
|
|
||||||
EventMask: uint(eventMask),
|
|
||||||
WantExpandedSQL: true,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
os.Exit(dbMain())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Harder to do DB work in main().
|
|
||||||
// It's better with a separate function because
|
|
||||||
// 'defer' and 'os.Exit' don't go well together.
|
|
||||||
//
|
|
||||||
// DO NOT use 'log.Fatal...' below: remember that it's equivalent to
|
|
||||||
// Print() followed by a call to os.Exit(1) --- and
|
|
||||||
// we want to avoid Exit() so 'defer' can do cleanup.
|
|
||||||
// Use 'log.Panic...' instead.
|
|
||||||
|
|
||||||
func dbMain() int {
|
|
||||||
db, err := sql.Open("sqlite3_tracing", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to open database: %#+v\n", err)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
err = db.Ping()
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbSetup(db)
|
|
||||||
|
|
||||||
dbDoInsert(db)
|
|
||||||
dbDoInsertPrepared(db)
|
|
||||||
dbDoSelect(db)
|
|
||||||
dbDoSelectPrepared(db)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'DDL' stands for "Data Definition Language":
|
|
||||||
|
|
||||||
// Note: "INTEGER PRIMARY KEY NOT NULL AUTOINCREMENT" causes the error
|
|
||||||
// 'near "AUTOINCREMENT": syntax error'; without "NOT NULL" it works.
|
|
||||||
const tableDDL = `CREATE TABLE t1 (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
note VARCHAR NOT NULL
|
|
||||||
)`
|
|
||||||
|
|
||||||
// 'DML' stands for "Data Manipulation Language":
|
|
||||||
|
|
||||||
const insertDML = "INSERT INTO t1 (note) VALUES (?)"
|
|
||||||
const selectDML = "SELECT id, note FROM t1 WHERE note LIKE ?"
|
|
||||||
|
|
||||||
const textPrefix = "bla-1234567890-"
|
|
||||||
const noteTextPattern = "%Prep%"
|
|
||||||
|
|
||||||
const nGenRows = 4 // Number of Rows to Generate (for *each* approach tested)
|
|
||||||
|
|
||||||
func dbSetup(db *sql.DB) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
_, err = db.Exec("DROP TABLE IF EXISTS t1")
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
_, err = db.Exec(tableDDL)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbDoInsert(db *sql.DB) {
|
|
||||||
const Descr = "DB-Exec"
|
|
||||||
for i := 0; i < nGenRows; i++ {
|
|
||||||
result, err := db.Exec(insertDML, textPrefix+Descr)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultDoCheck(result, Descr, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbDoInsertPrepared(db *sql.DB) {
|
|
||||||
const Descr = "DB-Prepare"
|
|
||||||
|
|
||||||
stmt, err := db.Prepare(insertDML)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
for i := 0; i < nGenRows; i++ {
|
|
||||||
result, err := stmt.Exec(textPrefix + Descr)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultDoCheck(result, Descr, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resultDoCheck(result sql.Result, callerDescr string, callIndex int) {
|
|
||||||
lastID, err := result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
nAffected, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Exec result for %s (%d): ID = %d, affected = %d\n", callerDescr, callIndex, lastID, nAffected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbDoSelect(db *sql.DB) {
|
|
||||||
const Descr = "DB-Query"
|
|
||||||
|
|
||||||
rows, err := db.Query(selectDML, noteTextPattern)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
rowsDoFetch(rows, Descr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dbDoSelectPrepared(db *sql.DB) {
|
|
||||||
const Descr = "DB-Prepare"
|
|
||||||
|
|
||||||
stmt, err := db.Prepare(selectDML)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
rows, err := stmt.Query(noteTextPattern)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
rowsDoFetch(rows, Descr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rowsDoFetch(rows *sql.Rows, callerDescr string) {
|
|
||||||
var nRows int
|
|
||||||
var id int64
|
|
||||||
var note string
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
err := rows.Scan(&id, ¬e)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
log.Printf("Row for %s (%d): id=%d, note=%q\n",
|
|
||||||
callerDescr, nRows, id, note)
|
|
||||||
nRows++
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
log.Printf("Total %d rows for %s.\n", nRows, callerDescr)
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{
|
|
||||||
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
|
||||||
return conn.CreateModule("github", &githubModule{})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
_, err = db.Exec("create virtual table repo using github(id, full_name, description, html_url)")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := db.Query("select id, full_name, description, html_url from repo")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
var id, fullName, description, htmlURL string
|
|
||||||
rows.Scan(&id, &fullName, &description, &htmlURL)
|
|
||||||
fmt.Printf("%s: %s\n\t%s\n\t%s\n\n", id, fullName, description, htmlURL)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type githubRepo struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
HTMLURL string `json:"html_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type githubModule struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *githubModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
|
|
||||||
err := c.DeclareVTab(fmt.Sprintf(`
|
|
||||||
CREATE TABLE %s (
|
|
||||||
id INT,
|
|
||||||
full_name TEXT,
|
|
||||||
description TEXT,
|
|
||||||
html_url TEXT
|
|
||||||
)`, args[0]))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ghRepoTable{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *githubModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
|
|
||||||
return m.Create(c, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *githubModule) DestroyModule() {}
|
|
||||||
|
|
||||||
type ghRepoTable struct {
|
|
||||||
repos []githubRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ghRepoTable) Open() (sqlite3.VTabCursor, error) {
|
|
||||||
resp, err := http.Get("https://api.github.com/repositories")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var repos []githubRepo
|
|
||||||
if err := json.Unmarshal(body, &repos); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ghRepoCursor{0, repos}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ghRepoTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) {
|
|
||||||
return &sqlite3.IndexResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ghRepoTable) Disconnect() error { return nil }
|
|
||||||
func (v *ghRepoTable) Destroy() error { return nil }
|
|
||||||
|
|
||||||
type ghRepoCursor struct {
|
|
||||||
index int
|
|
||||||
repos []githubRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) Column(c *sqlite3.SQLiteContext, col int) error {
|
|
||||||
switch col {
|
|
||||||
case 0:
|
|
||||||
c.ResultInt(vc.repos[vc.index].ID)
|
|
||||||
case 1:
|
|
||||||
c.ResultText(vc.repos[vc.index].FullName)
|
|
||||||
case 2:
|
|
||||||
c.ResultText(vc.repos[vc.index].Description)
|
|
||||||
case 3:
|
|
||||||
c.ResultText(vc.repos[vc.index].HTMLURL)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) Filter(idxNum int, idxStr string, vals []interface{}) error {
|
|
||||||
vc.index = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) Next() error {
|
|
||||||
vc.index++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) EOF() bool {
|
|
||||||
return vc.index >= len(vc.repos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) Rowid() (int64, error) {
|
|
||||||
return int64(vc.index), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (vc *ghRepoCursor) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
// +build ignore
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
site := "https://www.sqlite.org/download.html"
|
|
||||||
fmt.Printf("scraping %v\n", site)
|
|
||||||
doc, err := goquery.NewDocument(site)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
var url string
|
|
||||||
doc.Find("a").Each(func(_ int, s *goquery.Selection) {
|
|
||||||
if url == "" && strings.HasPrefix(s.Text(), "sqlite-amalgamation-") {
|
|
||||||
url = "https://www.sqlite.org/2017/" + s.Text()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if url == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("downloading %v\n", url)
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("extracting %v\n", path.Base(url))
|
|
||||||
r, err := zip.NewReader(bytes.NewReader(b), resp.ContentLength)
|
|
||||||
if err != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
for _, zf := range r.File {
|
|
||||||
var f *os.File
|
|
||||||
switch path.Base(zf.Name) {
|
|
||||||
case "sqlite3.c":
|
|
||||||
f, err = os.Create("sqlite3-binding.c")
|
|
||||||
case "sqlite3.h":
|
|
||||||
f, err = os.Create("sqlite3-binding.h")
|
|
||||||
case "sqlite3ext.h":
|
|
||||||
f, err = os.Create("sqlite3ext.h")
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
zr, err := zf.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.WriteString(f, "#ifndef USE_LIBSQLITE3\n")
|
|
||||||
if err != nil {
|
|
||||||
zr.Close()
|
|
||||||
f.Close()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
scanner := bufio.NewScanner(zr)
|
|
||||||
for scanner.Scan() {
|
|
||||||
text := scanner.Text()
|
|
||||||
if text == `#include "sqlite3.h"` {
|
|
||||||
text = `#include "sqlite3-binding.h"`
|
|
||||||
}
|
|
||||||
_, err = fmt.Fprintln(f, text)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = scanner.Err()
|
|
||||||
if err != nil {
|
|
||||||
zr.Close()
|
|
||||||
f.Close()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = io.WriteString(f, "#else // USE_LIBSQLITE3\n // If users really want to link against the system sqlite3 we\n// need to make this file a noop.\n #endif")
|
|
||||||
if err != nil {
|
|
||||||
zr.Close()
|
|
||||||
f.Close()
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
zr.Close()
|
|
||||||
f.Close()
|
|
||||||
fmt.Printf("extracted %v\n", filepath.Base(f.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Mitchell Hashimoto
|
||||||
|
|
||||||
|
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,39 @@
|
||||||
|
# go-wordwrap
|
||||||
|
|
||||||
|
`go-wordwrap` (Golang package: `wordwrap`) is a package for Go that
|
||||||
|
automatically wraps words into multiple lines. The primary use case for this
|
||||||
|
is in formatting CLI output, but of course word wrapping is a generally useful
|
||||||
|
thing to do.
|
||||||
|
|
||||||
|
## Installation and Usage
|
||||||
|
|
||||||
|
Install using `go get github.com/mitchellh/go-wordwrap`.
|
||||||
|
|
||||||
|
Full documentation is available at
|
||||||
|
http://godoc.org/github.com/mitchellh/go-wordwrap
|
||||||
|
|
||||||
|
Below is an example of its usage ignoring errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
wrapped := wordwrap.WrapString("foo bar baz", 3)
|
||||||
|
fmt.Println(wrapped)
|
||||||
|
```
|
||||||
|
|
||||||
|
Would output:
|
||||||
|
|
||||||
|
```
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
baz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Word Wrap Algorithm
|
||||||
|
|
||||||
|
This library doesn't use any clever algorithm for word wrapping. The wrapping
|
||||||
|
is actually very naive: whenever there is whitespace or an explicit linebreak.
|
||||||
|
The goal of this library is for word wrapping CLI output, so the input is
|
||||||
|
typically pretty well controlled human language. Because of this, the naive
|
||||||
|
approach typically works just fine.
|
||||||
|
|
||||||
|
In the future, we'd like to make the algorithm more advanced. We would do
|
||||||
|
so without breaking the API.
|
|
@ -0,0 +1,73 @@
|
||||||
|
package wordwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WrapString wraps the given string within lim width in characters.
|
||||||
|
//
|
||||||
|
// Wrapping is currently naive and only happens at white-space. A future
|
||||||
|
// version of the library will implement smarter wrapping. This means that
|
||||||
|
// pathological cases can dramatically reach past the limit, such as a very
|
||||||
|
// long word.
|
||||||
|
func WrapString(s string, lim uint) string {
|
||||||
|
// Initialize a buffer with a slightly larger size to account for breaks
|
||||||
|
init := make([]byte, 0, len(s))
|
||||||
|
buf := bytes.NewBuffer(init)
|
||||||
|
|
||||||
|
var current uint
|
||||||
|
var wordBuf, spaceBuf bytes.Buffer
|
||||||
|
|
||||||
|
for _, char := range s {
|
||||||
|
if char == '\n' {
|
||||||
|
if wordBuf.Len() == 0 {
|
||||||
|
if current+uint(spaceBuf.Len()) > lim {
|
||||||
|
current = 0
|
||||||
|
} else {
|
||||||
|
current += uint(spaceBuf.Len())
|
||||||
|
spaceBuf.WriteTo(buf)
|
||||||
|
}
|
||||||
|
spaceBuf.Reset()
|
||||||
|
} else {
|
||||||
|
current += uint(spaceBuf.Len() + wordBuf.Len())
|
||||||
|
spaceBuf.WriteTo(buf)
|
||||||
|
spaceBuf.Reset()
|
||||||
|
wordBuf.WriteTo(buf)
|
||||||
|
wordBuf.Reset()
|
||||||
|
}
|
||||||
|
buf.WriteRune(char)
|
||||||
|
current = 0
|
||||||
|
} else if unicode.IsSpace(char) {
|
||||||
|
if spaceBuf.Len() == 0 || wordBuf.Len() > 0 {
|
||||||
|
current += uint(spaceBuf.Len() + wordBuf.Len())
|
||||||
|
spaceBuf.WriteTo(buf)
|
||||||
|
spaceBuf.Reset()
|
||||||
|
wordBuf.WriteTo(buf)
|
||||||
|
wordBuf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceBuf.WriteRune(char)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
wordBuf.WriteRune(char)
|
||||||
|
|
||||||
|
if current+uint(spaceBuf.Len()+wordBuf.Len()) > lim && uint(wordBuf.Len()) < lim {
|
||||||
|
buf.WriteRune('\n')
|
||||||
|
current = 0
|
||||||
|
spaceBuf.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wordBuf.Len() == 0 {
|
||||||
|
if current+uint(spaceBuf.Len()) <= lim {
|
||||||
|
spaceBuf.WriteTo(buf)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
spaceBuf.WriteTo(buf)
|
||||||
|
wordBuf.WriteTo(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package wordwrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapString(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Input, Output string
|
||||||
|
Lim uint
|
||||||
|
}{
|
||||||
|
// A simple word passes through.
|
||||||
|
{
|
||||||
|
"foo",
|
||||||
|
"foo",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// A single word that is too long passes through.
|
||||||
|
// We do not break words.
|
||||||
|
{
|
||||||
|
"foobarbaz",
|
||||||
|
"foobarbaz",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Lines are broken at whitespace.
|
||||||
|
{
|
||||||
|
"foo bar baz",
|
||||||
|
"foo\nbar\nbaz",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Lines are broken at whitespace, even if words
|
||||||
|
// are too long. We do not break words.
|
||||||
|
{
|
||||||
|
"foo bars bazzes",
|
||||||
|
"foo\nbars\nbazzes",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// A word that would run beyond the width is wrapped.
|
||||||
|
{
|
||||||
|
"fo sop",
|
||||||
|
"fo\nsop",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Whitespace that trails a line and fits the width
|
||||||
|
// passes through, as does whitespace prefixing an
|
||||||
|
// explicit line break. A tab counts as one character.
|
||||||
|
{
|
||||||
|
"foo\nb\t r\n baz",
|
||||||
|
"foo\nb\t r\n baz",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Trailing whitespace is removed if it doesn't fit the width.
|
||||||
|
// Runs of whitespace on which a line is broken are removed.
|
||||||
|
{
|
||||||
|
"foo \nb ar ",
|
||||||
|
"foo\nb\nar",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// An explicit line break at the end of the input is preserved.
|
||||||
|
{
|
||||||
|
"foo bar baz\n",
|
||||||
|
"foo\nbar\nbaz\n",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Explicit break are always preserved.
|
||||||
|
{
|
||||||
|
"\nfoo bar\n\n\nbaz\n",
|
||||||
|
"\nfoo\nbar\n\n\nbaz\n",
|
||||||
|
4,
|
||||||
|
},
|
||||||
|
// Complete example:
|
||||||
|
{
|
||||||
|
" This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ",
|
||||||
|
" This\nis a\nlist: \n\n\t* foo\n\t* bar\n\n\n\t* baz\nBAM",
|
||||||
|
6,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
actual := WrapString(tc.Input, tc.Lim)
|
||||||
|
if actual != tc.Output {
|
||||||
|
t.Fatalf("Case %d Input:\n\n`%s`\n\nActual Output:\n\n`%s`", i, tc.Input, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Please keep this file sorted.
|
||||||
|
|
||||||
|
Georg Reinke <guelfey@googlemail.com>
|
||||||
|
nsf <no.smile.face@gmail.com>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue