@ -0,0 +1,24 @@ |
||||
# 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 |
@ -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,159 @@ |
||||
# 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) |
||||
|
||||
## Update 23/06/2015 |
||||
Pull requests and master branch are freezing, waiting for merging from `refactoring` branch. |
||||
|
||||
## Notice |
||||
termui comes with ABSOLUTELY NO WARRANTY, and there is a breaking change coming up (see refactoring branch) which will change the `Bufferer` interface and many others. These changes reduce calculation overhead and introduce a new drawing buffer with better capacibilities. We will step into the next stage (call it beta) after merging these changes. |
||||
|
||||
## Introduction |
||||
Go terminal dashboard. Inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib), but purely in Go. |
||||
|
||||
Cross-platform, easy to compile, and fully-customizable. |
||||
|
||||
__Demo:__ (cast under osx 10.10; Terminal.app; Menlo Regular 12pt.) |
||||
|
||||
<img src="./example/dashboard.gif" alt="demo" width="600"> |
||||
|
||||
__Grid layout:__ |
||||
|
||||
Expressive syntax, using [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp) |
||||
```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) |
||||
``` |
||||
[demo code:](https://github.com/gizak/termui/blob/master/example/grid.go) |
||||
|
||||
<img src="./example/grid.gif" alt="grid" width="500"> |
||||
|
||||
## Installation |
||||
|
||||
go get github.com/gizak/termui |
||||
|
||||
## Usage |
||||
|
||||
Each component's layout is a bit like HTML block (box model), which has border and padding. |
||||
|
||||
The `Border` property can be chosen to hide or display (with its border label), when it comes to display, the label takes 1 padding space (i.e. in css: `padding: 1;`, innerHeight and innerWidth therefore shrunk by 1). |
||||
|
||||
`````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.Border.Label = "Text Box" |
||||
p.Border.FgColor = ui.ColorCyan |
||||
|
||||
g := ui.NewGauge() |
||||
g.Percent = 50 |
||||
g.Width = 50 |
||||
g.Height = 3 |
||||
g.Y = 11 |
||||
g.Border.Label = "Gauge" |
||||
g.BarColor = ui.ColorRed |
||||
g.Border.FgColor = ui.ColorWhite |
||||
g.Border.LabelFgColor = ui.ColorCyan |
||||
|
||||
ui.Render(p, g) |
||||
|
||||
// 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). |
||||
|
||||
## Themes |
||||
|
||||
_All_ colors in _all_ components can be changed at _any_ time, while there provides some predefined color schemes: |
||||
|
||||
```go |
||||
// for now there are only two themes: default and helloworld |
||||
termui.UseTheme("helloworld") |
||||
|
||||
// create components... |
||||
``` |
||||
The `default ` theme's settings depend on the user's terminal color scheme, which is saying if your terminal default font color is white and background is white, it will be like: |
||||
|
||||
<img src="./example/themedefault.png" alt="default" type="image/png" width="600"> |
||||
|
||||
The `helloworld` color scheme drops in some colors! |
||||
|
||||
<img src="./example/themehelloworld.png" alt="helloworld" type="image/png" width="600"> |
||||
|
||||
## Widgets |
||||
|
||||
#### Par |
||||
|
||||
[demo code](https://github.com/gizak/termui/blob/master/example/par.go) |
||||
|
||||
<img src="./example/par.png" alt="par" type="image/png" width="300"> |
||||
|
||||
#### List |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/list.go) |
||||
|
||||
<img src="./example/list.png" alt="list" type="image/png" width="200"> |
||||
|
||||
#### Gauge |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go) |
||||
|
||||
<img src="./example/gauge.png" alt="gauge" type="image/png" width="350"> |
||||
|
||||
#### Line Chart |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/linechart.go) |
||||
|
||||
<img src="./example/linechart.png" alt="linechart" type="image/png" width="450"> |
||||
|
||||
#### Bar Chart |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/barchart.go) |
||||
|
||||
<img src="./example/barchart.png" alt="barchart" type="image/png" width="150"> |
||||
|
||||
#### Mult-Bar / Stacked-Bar Chart |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/mbarchart.go) |
||||
|
||||
<img src="./example/mbarchart.png" alt="barchart" type="image/png" width="150"> |
||||
|
||||
#### Sparklines |
||||
[demo code](https://github.com/gizak/termui/blob/master/example/sparklines.go) |
||||
|
||||
<img src="./example/sparklines.png" alt="sparklines" type="image/png" width="350"> |
||||
|
||||
|
||||
## GoDoc |
||||
|
||||
[godoc](https://godoc.org/github.com/gizak/termui) |
||||
|
||||
## TODO |
||||
|
||||
- [x] Grid layout |
||||
- [ ] Event system |
||||
- [ ] Canvas widget |
||||
- [ ] Refine APIs |
||||
- [ ] Focusable widgets |
||||
|
||||
## License |
||||
This library is under the [MIT License](http://opensource.org/licenses/MIT) |
@ -0,0 +1,135 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "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 |
||||
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 = theme.BarChartBar |
||||
bc.NumColor = theme.BarChartNum |
||||
bc.TextColor = theme.BarChartText |
||||
bc.BarGap = 1 |
||||
bc.BarWidth = 3 |
||||
return bc |
||||
} |
||||
|
||||
func (bc *BarChart) layout() { |
||||
bc.numBar = bc.innerWidth / (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.innerHeight-1) |
||||
} |
||||
|
||||
func (bc *BarChart) SetMax(max int) { |
||||
|
||||
if max > 0 { |
||||
bc.max = max |
||||
} |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (bc *BarChart) Buffer() []Point { |
||||
ps := 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) |
||||
// plot bar
|
||||
for j := 0; j < bc.BarWidth; j++ { |
||||
for k := 0; k < h; k++ { |
||||
p := Point{} |
||||
p.Ch = ' ' |
||||
p.Bg = bc.BarColor |
||||
if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent!
|
||||
p.Bg |= AttrReverse |
||||
} |
||||
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j |
||||
p.Y = bc.innerY + bc.innerHeight - 2 - k |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
// plot text
|
||||
for j, k := 0, 0; j < len(bc.labels[i]); j++ { |
||||
w := charWidth(bc.labels[i][j]) |
||||
p := Point{} |
||||
p.Ch = bc.labels[i][j] |
||||
p.Bg = bc.BgColor |
||||
p.Fg = bc.TextColor |
||||
p.Y = bc.innerY + bc.innerHeight - 1 |
||||
p.X = bc.innerX + oftX + k |
||||
ps = append(ps, p) |
||||
k += w |
||||
} |
||||
// plot num
|
||||
for j := 0; j < len(bc.dataNum[i]); j++ { |
||||
p := Point{} |
||||
p.Ch = bc.dataNum[i][j] |
||||
p.Fg = bc.NumColor |
||||
p.Bg = bc.BarColor |
||||
if bc.BarColor == ColorDefault { // the same as above
|
||||
p.Bg |= AttrReverse |
||||
} |
||||
if h == 0 { |
||||
p.Bg = bc.BgColor |
||||
} |
||||
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j |
||||
p.Y = bc.innerY + bc.innerHeight - 2 |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
|
||||
return bc.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,142 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 |
||||
|
||||
// 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 { |
||||
X int |
||||
Y int |
||||
Border labeledBorder |
||||
IsDisplay bool |
||||
HasBorder bool |
||||
BgColor Attribute |
||||
Width int |
||||
Height int |
||||
innerWidth int |
||||
innerHeight int |
||||
innerX int |
||||
innerY int |
||||
PaddingTop int |
||||
PaddingBottom int |
||||
PaddingLeft int |
||||
PaddingRight int |
||||
} |
||||
|
||||
// NewBlock returns a *Block which inherits styles from current theme.
|
||||
func NewBlock() *Block { |
||||
d := Block{} |
||||
d.IsDisplay = true |
||||
d.HasBorder = theme.HasBorder |
||||
d.Border.BgColor = theme.BorderBg |
||||
d.Border.FgColor = theme.BorderFg |
||||
d.Border.LabelBgColor = theme.BorderLabelTextBg |
||||
d.Border.LabelFgColor = theme.BorderLabelTextFg |
||||
d.BgColor = theme.BlockBg |
||||
d.Width = 2 |
||||
d.Height = 2 |
||||
return &d |
||||
} |
||||
|
||||
// compute box model
|
||||
func (d *Block) align() { |
||||
d.innerWidth = d.Width - d.PaddingLeft - d.PaddingRight |
||||
d.innerHeight = d.Height - d.PaddingTop - d.PaddingBottom |
||||
d.innerX = d.X + d.PaddingLeft |
||||
d.innerY = d.Y + d.PaddingTop |
||||
|
||||
if d.HasBorder { |
||||
d.innerHeight -= 2 |
||||
d.innerWidth -= 2 |
||||
d.Border.X = d.X |
||||
d.Border.Y = d.Y |
||||
d.Border.Width = d.Width |
||||
d.Border.Height = d.Height |
||||
d.innerX++ |
||||
d.innerY++ |
||||
} |
||||
|
||||
if d.innerHeight < 0 { |
||||
d.innerHeight = 0 |
||||
} |
||||
if d.innerWidth < 0 { |
||||
d.innerWidth = 0 |
||||
} |
||||
|
||||
} |
||||
|
||||
// InnerBounds returns the internal bounds of the block after aligning and
|
||||
// calculating the padding and border, if any.
|
||||
func (d *Block) InnerBounds() (x, y, width, height int) { |
||||
d.align() |
||||
return d.innerX, d.innerY, d.innerWidth, d.innerHeight |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
// Draw background and border (if any).
|
||||
func (d *Block) Buffer() []Point { |
||||
d.align() |
||||
|
||||
ps := []Point{} |
||||
if !d.IsDisplay { |
||||
return ps |
||||
} |
||||
|
||||
if d.HasBorder { |
||||
ps = d.Border.Buffer() |
||||
} |
||||
|
||||
for i := 0; i < d.innerWidth; i++ { |
||||
for j := 0; j < d.innerHeight; j++ { |
||||
p := Point{} |
||||
p.X = d.X + 1 + i |
||||
p.Y = d.Y + 1 + j |
||||
p.Ch = ' ' |
||||
p.Bg = d.BgColor |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
return ps |
||||
} |
||||
|
||||
// GetHeight implements GridBufferer.
|
||||
// It returns current height of the block.
|
||||
func (d Block) GetHeight() int { |
||||
return d.Height |
||||
} |
||||
|
||||
// SetX implements GridBufferer interface, which sets block's x position.
|
||||
func (d *Block) SetX(x int) { |
||||
d.X = x |
||||
} |
||||
|
||||
// SetY implements GridBufferer interface, it sets y position for block.
|
||||
func (d *Block) SetY(y int) { |
||||
d.Y = y |
||||
} |
||||
|
||||
// SetWidth implements GridBuffer interface, it sets block's width.
|
||||
func (d *Block) SetWidth(w int) { |
||||
d.Width = w |
||||
} |
||||
|
||||
// chop the overflow parts
|
||||
func (d *Block) chopOverflow(ps []Point) []Point { |
||||
nps := make([]Point, 0, len(ps)) |
||||
x := d.X |
||||
y := d.Y |
||||
w := d.Width |
||||
h := d.Height |
||||
for _, v := range ps { |
||||
if v.X >= x && |
||||
v.X < x+w && |
||||
v.Y >= y && |
||||
v.Y < y+h { |
||||
nps = append(nps, v) |
||||
} |
||||
} |
||||
return nps |
||||
} |
@ -0,0 +1,46 @@ |
||||
package termui |
||||
|
||||
import "testing" |
||||
|
||||
func TestBlock_InnerBounds(t *testing.T) { |
||||
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) |
||||
cx, cy, cw, ch := b.InnerBounds() |
||||
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", y, cy) |
||||
} |
||||
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.HasBorder = false |
||||
assert("no border, no padding", 10, 11, 12, 13) |
||||
|
||||
b.HasBorder = 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,117 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 |
||||
|
||||
type border struct { |
||||
X int |
||||
Y int |
||||
Width int |
||||
Height int |
||||
FgColor Attribute |
||||
BgColor Attribute |
||||
} |
||||
|
||||
type hline struct { |
||||
X int |
||||
Y int |
||||
Length int |
||||
FgColor Attribute |
||||
BgColor Attribute |
||||
} |
||||
|
||||
type vline struct { |
||||
X int |
||||
Y int |
||||
Length int |
||||
FgColor Attribute |
||||
BgColor Attribute |
||||
} |
||||
|
||||
// Draw a horizontal line.
|
||||
func (l hline) Buffer() []Point { |
||||
pts := make([]Point, l.Length) |
||||
for i := 0; i < l.Length; i++ { |
||||
pts[i].X = l.X + i |
||||
pts[i].Y = l.Y |
||||
pts[i].Ch = HORIZONTAL_LINE |
||||
pts[i].Bg = l.BgColor |
||||
pts[i].Fg = l.FgColor |
||||
} |
||||
return pts |
||||
} |
||||
|
||||
// Draw a vertical line.
|
||||
func (l vline) Buffer() []Point { |
||||
pts := make([]Point, l.Length) |
||||
for i := 0; i < l.Length; i++ { |
||||
pts[i].X = l.X |
||||
pts[i].Y = l.Y + i |
||||
pts[i].Ch = VERTICAL_LINE |
||||
pts[i].Bg = l.BgColor |
||||
pts[i].Fg = l.FgColor |
||||
} |
||||
return pts |
||||
} |
||||
|
||||
// Draw a box border.
|
||||
func (b border) Buffer() []Point { |
||||
if b.Width < 2 || b.Height < 2 { |
||||
return nil |
||||
} |
||||
pts := make([]Point, 2*b.Width+2*b.Height-4) |
||||
|
||||
pts[0].X = b.X |
||||
pts[0].Y = b.Y |
||||
pts[0].Fg = b.FgColor |
||||
pts[0].Bg = b.BgColor |
||||
pts[0].Ch = TOP_LEFT |
||||
|
||||
pts[1].X = b.X + b.Width - 1 |
||||
pts[1].Y = b.Y |
||||
pts[1].Fg = b.FgColor |
||||
pts[1].Bg = b.BgColor |
||||
pts[1].Ch = TOP_RIGHT |
||||
|
||||
pts[2].X = b.X |
||||
pts[2].Y = b.Y + b.Height - 1 |
||||
pts[2].Fg = b.FgColor |
||||
pts[2].Bg = b.BgColor |
||||
pts[2].Ch = BOTTOM_LEFT |
||||
|
||||
pts[3].X = b.X + b.Width - 1 |
||||
pts[3].Y = b.Y + b.Height - 1 |
||||
pts[3].Fg = b.FgColor |
||||
pts[3].Bg = b.BgColor |
||||
pts[3].Ch = BOTTOM_RIGHT |
||||
|
||||
copy(pts[4:], (hline{b.X + 1, b.Y, b.Width - 2, b.FgColor, b.BgColor}).Buffer()) |
||||
copy(pts[4+b.Width-2:], (hline{b.X + 1, b.Y + b.Height - 1, b.Width - 2, b.FgColor, b.BgColor}).Buffer()) |
||||
copy(pts[4+2*b.Width-4:], (vline{b.X, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer()) |
||||
copy(pts[4+2*b.Width-4+b.Height-2:], (vline{b.X + b.Width - 1, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer()) |
||||
|
||||
return pts |
||||
} |
||||
|
||||
type labeledBorder struct { |
||||
border |
||||
Label string |
||||
LabelFgColor Attribute |
||||
LabelBgColor Attribute |
||||
} |
||||
|
||||
// Draw a box border with label.
|
||||
func (lb labeledBorder) Buffer() []Point { |
||||
ps := lb.border.Buffer() |
||||
maxTxtW := lb.Width - 2 |
||||
rs := trimStr2Runes(lb.Label, maxTxtW) |
||||
|
||||
for i, j, w := 0, 0, 0; i < len(rs); i++ { |
||||
w = charWidth(rs[i]) |
||||
ps = append(ps, newPointWithAttrs(rs[i], lb.X+1+j, lb.Y, lb.LabelFgColor, lb.LabelBgColor)) |
||||
j += w |
||||
} |
||||
|
||||
return ps |
||||
} |
@ -0,0 +1,14 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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,14 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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,74 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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() []Point { |
||||
ps := make([]Point, len(c)) |
||||
i := 0 |
||||
for k, v := range c { |
||||
ps[i] = newPoint(v+brailleBase, k[0], k[1]) |
||||
i++ |
||||
} |
||||
return ps |
||||
} |
@ -0,0 +1,55 @@ |
||||
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() |
||||
rs := make([]rune, len(bufs)) |
||||
for i, v := range bufs { |
||||
rs[i] = v.Ch |
||||
} |
||||
spew.Dump(string(rs)) |
||||
} |
@ -0,0 +1,336 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "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 |
||||
axisYLebelGap int |
||||
axisXLebelGap int |
||||
topValue float64 |
||||
bottomValue float64 |
||||
labelX [][]rune |
||||
labelY [][]rune |
||||
labelYSpace int |
||||
maxY float64 |
||||
minY float64 |
||||
} |
||||
|
||||
// NewLineChart returns a new LineChart with current theme.
|
||||
func NewLineChart() *LineChart { |
||||
lc := &LineChart{Block: *NewBlock()} |
||||
lc.AxesColor = theme.LineChartAxes |
||||
lc.LineColor = theme.LineChartLine |
||||
lc.Mode = "braille" |
||||
lc.DotStyle = '•' |
||||
lc.axisXLebelGap = 2 |
||||
lc.axisYLebelGap = 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() []Point { |
||||
ps := []Point{} |
||||
|
||||
// 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 { |
||||
p := Point{} |
||||
p.Ch = braillePatterns[[2]int{m0, m1}] |
||||
p.Bg = lc.BgColor |
||||
p.Fg = lc.LineColor |
||||
p.Y = lc.innerY + lc.innerHeight - 3 - b0 |
||||
p.X = lc.innerX + lc.labelYSpace + 1 + i |
||||
ps = append(ps, p) |
||||
} else { |
||||
p0 := newPointWithAttrs(lSingleBraille[m0], |
||||
lc.innerX+lc.labelYSpace+1+i, |
||||
lc.innerY+lc.innerHeight-3-b0, |
||||
lc.LineColor, |
||||
lc.BgColor) |
||||
p1 := newPointWithAttrs(rSingleBraille[m1], |
||||
lc.innerX+lc.labelYSpace+1+i, |
||||
lc.innerY+lc.innerHeight-3-b1, |
||||
lc.LineColor, |
||||
lc.BgColor) |
||||
ps = append(ps, p0, p1) |
||||
} |
||||
|
||||
} |
||||
return ps |
||||
} |
||||
|
||||
func (lc *LineChart) renderDot() []Point { |
||||
ps := []Point{} |
||||
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ { |
||||
p := Point{} |
||||
p.Ch = lc.DotStyle |
||||
p.Fg = lc.LineColor |
||||
p.Bg = lc.BgColor |
||||
p.X = lc.innerX + lc.labelYSpace + 1 + i |
||||
p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5) |
||||
ps = append(ps, p) |
||||
} |
||||
|
||||
return ps |
||||
} |
||||
|
||||
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.axisXLebelGap |
||||
} 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.axisXLebelGap |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
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.axisYLebelGap + 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.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.innerWidth |
||||
if lc.Mode == "braille" { |
||||
vrange = 2 * lc.innerWidth |
||||
} |
||||
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.innerHeight - 2 |
||||
lc.calcLabelY() |
||||
|
||||
lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace |
||||
lc.calcLabelX() |
||||
|
||||
lc.drawingX = lc.innerX + 1 + lc.labelYSpace |
||||
lc.drawingY = lc.innerY |
||||
} |
||||
|
||||
func (lc *LineChart) plotAxes() []Point { |
||||
origY := lc.innerY + lc.innerHeight - 2 |
||||
origX := lc.innerX + lc.labelYSpace |
||||
|
||||
ps := []Point{newPointWithAttrs(ORIGIN, origX, origY, lc.AxesColor, lc.BgColor)} |
||||
|
||||
for x := origX + 1; x < origX+lc.axisXWidth; x++ { |
||||
p := Point{} |
||||
p.X = x |
||||
p.Y = origY |
||||
p.Bg = lc.BgColor |
||||
p.Fg = lc.AxesColor |
||||
p.Ch = HDASH |
||||
ps = append(ps, p) |
||||
} |
||||
|
||||
for dy := 1; dy <= lc.axisYHeight; dy++ { |
||||
p := Point{} |
||||
p.X = origX |
||||
p.Y = origY - dy |
||||
p.Bg = lc.BgColor |
||||
p.Fg = lc.AxesColor |
||||
p.Ch = VDASH |
||||
ps = append(ps, p) |
||||
} |
||||
|
||||
// x label
|
||||
oft := 0 |
||||
for _, rs := range lc.labelX { |
||||
if oft+len(rs) > lc.axisXWidth { |
||||
break |
||||
} |
||||
for j, r := range rs { |
||||
p := Point{} |
||||
p.Ch = r |
||||
p.Fg = lc.AxesColor |
||||
p.Bg = lc.BgColor |
||||
p.X = origX + oft + j |
||||
p.Y = lc.innerY + lc.innerHeight - 1 |
||||
ps = append(ps, p) |
||||
} |
||||
oft += len(rs) + lc.axisXLebelGap |
||||
} |
||||
|
||||
// y labels
|
||||
for i, rs := range lc.labelY { |
||||
for j, r := range rs { |
||||
p := Point{} |
||||
p.Ch = r |
||||
p.Fg = lc.AxesColor |
||||
p.Bg = lc.BgColor |
||||
p.X = lc.innerX + j |
||||
p.Y = origY - i*(lc.axisYLebelGap+1) |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
|
||||
return ps |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (lc *LineChart) Buffer() []Point { |
||||
ps := lc.Block.Buffer() |
||||
if lc.Data == nil || len(lc.Data) == 0 { |
||||
return ps |
||||
} |
||||
lc.calcLayout() |
||||
ps = append(ps, lc.plotAxes()...) |
||||
|
||||
if lc.Mode == "dot" { |
||||
ps = append(ps, lc.renderDot()...) |
||||
} else { |
||||
ps = append(ps, lc.renderBraille()...) |
||||
} |
||||
|
||||
return lc.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,11 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 2015 Zack Guo <gizak@icloud.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,27 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "Gauge" |
||||
|
||||
ui.Render(g) |
||||
} |
||||
*/ |
||||
package termui |
@ -0,0 +1,219 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
//
|
||||
// Portions of this file uses [termbox-go](https://github.com/nsf/termbox-go/blob/54b74d087b7c397c402d0e3b66d2ccb6eaf5c2b4/api_common.go)
|
||||
// by [authors](https://github.com/nsf/termbox-go/blob/master/AUTHORS)
|
||||
// under [license](https://github.com/nsf/termbox-go/blob/master/LICENSE)
|
||||
|
||||
package termui |
||||
|
||||
import "github.com/nsf/termbox-go" |
||||
|
||||
/***********************************termbox-go**************************************/ |
||||
|
||||
type ( |
||||
EventType uint8 |
||||
Modifier uint8 |
||||
Key uint16 |
||||
) |
||||
|
||||
// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
|
||||
// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
|
||||
// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
|
||||
type Event struct { |
||||
Type EventType // one of Event* constants
|
||||
Mod Modifier // one of Mod* constants or 0
|
||||
Key Key // one of Key* constants, invalid if 'Ch' is not 0
|
||||
Ch rune // a unicode character
|
||||
Width int // width of the screen
|
||||
Height int // height of the screen
|
||||
Err error // error in case if input failed
|
||||
MouseX int // x coord of mouse
|
||||
MouseY int // y coord of mouse
|
||||
N int // number of bytes written when getting a raw event
|
||||
} |
||||
|
||||
const ( |
||||
KeyF1 Key = 0xFFFF - iota |
||||
KeyF2 |
||||
KeyF3 |
||||
KeyF4 |
||||
KeyF5 |
||||
KeyF6 |
||||
KeyF7 |
||||
KeyF8 |
||||
KeyF9 |
||||
KeyF10 |
||||
KeyF11 |
||||
KeyF12 |
||||
KeyInsert |
||||
KeyDelete |
||||
KeyHome |
||||
KeyEnd |
||||
KeyPgup |
||||
KeyPgdn |
||||
KeyArrowUp |
||||
KeyArrowDown |
||||
KeyArrowLeft |
||||
KeyArrowRight |
||||
key_min // see terminfo
|
||||
MouseLeft |
||||
MouseMiddle |
||||
MouseRight |
||||
) |
||||
|
||||
const ( |
||||
KeyCtrlTilde Key = 0x00 |
||||
KeyCtrl2 Key = 0x00 |
||||
KeyCtrlSpace Key = 0x00 |
||||
KeyCtrlA Key = 0x01 |
||||
KeyCtrlB Key = 0x02 |
||||
KeyCtrlC Key = 0x03 |
||||
KeyCtrlD Key = 0x04 |
||||
KeyCtrlE Key = 0x05 |
||||
KeyCtrlF Key = 0x06 |
||||
KeyCtrlG Key = 0x07 |
||||
KeyBackspace Key = 0x08 |
||||
KeyCtrlH Key = 0x08 |
||||
KeyTab Key = 0x09 |
||||
KeyCtrlI Key = 0x09 |
||||
KeyCtrlJ Key = 0x0A |
||||
KeyCtrlK Key = 0x0B |
||||
KeyCtrlL Key = 0x0C |
||||
KeyEnter Key = 0x0D |
||||
KeyCtrlM Key = 0x0D |
||||
KeyCtrlN Key = 0x0E |
||||
KeyCtrlO Key = 0x0F |
||||
KeyCtrlP Key = 0x10 |
||||
KeyCtrlQ Key = 0x11 |
||||
KeyCtrlR Key = 0x12 |
||||
KeyCtrlS Key = 0x13 |
||||
KeyCtrlT Key = 0x14 |
||||
KeyCtrlU Key = 0x15 |
||||
KeyCtrlV Key = 0x16 |
||||
KeyCtrlW Key = 0x17 |
||||
KeyCtrlX Key = 0x18 |
||||
KeyCtrlY Key = 0x19 |
||||
KeyCtrlZ Key = 0x1A |
||||
KeyEsc Key = 0x1B |
||||
KeyCtrlLsqBracket Key = 0x1B |
||||
KeyCtrl3 Key = 0x1B |
||||
KeyCtrl4 Key = 0x1C |
||||
KeyCtrlBackslash Key = 0x1C |
||||
KeyCtrl5 Key = 0x1D |
||||
KeyCtrlRsqBracket Key = 0x1D |
||||
KeyCtrl6 Key = 0x1E |
||||
KeyCtrl7 Key = 0x1F |
||||
KeyCtrlSlash Key = 0x1F |
||||
KeyCtrlUnderscore Key = 0x1F |
||||
KeySpace Key = 0x20 |
||||
KeyBackspace2 Key = 0x7F |
||||
KeyCtrl8 Key = 0x7F |
||||
) |
||||
|
||||
// Alt modifier constant, see Event.Mod field and SetInputMode function.
|
||||
const ( |
||||
ModAlt Modifier = 0x01 |
||||
) |
||||
|
||||
// Event type. See Event.Type field.
|
||||
const ( |
||||
EventKey EventType = iota |
||||
EventResize |
||||
EventMouse |
||||
EventError |
||||
EventInterrupt |
||||
EventRaw |
||||
EventNone |
||||
) |
||||
|
||||
/**************************************end**************************************/ |
||||
|
||||
// convert termbox.Event to termui.Event
|
||||
func uiEvt(e termbox.Event) Event { |
||||
event := Event{} |
||||
event.Type = EventType(e.Type) |
||||
event.Mod = Modifier(e.Mod) |
||||
event.Key = Key(e.Key) |
||||
event.Ch = e.Ch |
||||
event.Width = e.Width |
||||
event.Height = e.Height |
||||
event.Err = e.Err |
||||
event.MouseX = e.MouseX |
||||
event.MouseY = e.MouseY |
||||
event.N = e.N |
||||
|
||||
return event |
||||
} |
||||
|
||||
var evtChs = make([]chan Event, 0) |
||||
|
||||
// EventCh returns an output-only event channel.
|
||||
// This function can be called many times (multiplexer).
|
||||
func EventCh() <-chan Event { |
||||
out := make(chan Event) |
||||
evtChs = append(evtChs, out) |
||||
return out |
||||
} |
||||
|
||||
// turn on event listener
|
||||
func evtListen() { |
||||
go func() { |
||||
for { |
||||
e := termbox.PollEvent() |
||||
// dispatch
|
||||
for _, c := range evtChs { |
||||
go func(ch chan Event) { |
||||
ch <- uiEvt(e) |
||||
}(c) |
||||
} |
||||
} |
||||
}() |
||||
} |
||||
|
||||
/* |
||||
// EventHandlers is a handler sequence
|
||||
var EventHandlers []func(Event) |
||||
|
||||
var signalQuit = make(chan bool) |
||||
|
||||
// Quit sends quit signal to terminate termui
|
||||
func Quit() { |
||||
signalQuit <- true |
||||
} |
||||
|
||||
// Wait listening to signalQuit, block operation.
|
||||
func Wait() { |
||||
<-signalQuit |
||||
} |
||||
|
||||
// RegEvtHandler register function into TSEventHandler sequence.
|
||||
func RegEvtHandler(fn func(Event)) { |
||||
EventHandlers = append(EventHandlers, fn) |
||||
} |
||||
|
||||
// EventLoop handles all events and
|
||||
// redirects every event to callbacks in EventHandlers
|
||||
func EventLoop() { |
||||
evt := make(chan termbox.Event) |
||||
|
||||
go func() { |
||||
for { |
||||
evt <- termbox.PollEvent() |
||||
} |
||||
}() |
||||
|
||||
for { |
||||
select { |
||||
case c := <-signalQuit: |
||||
defer func() { signalQuit <- c }() |
||||
return |
||||
case e := <-evt: |
||||
for _, fn := range EventHandlers { |
||||
fn(uiEvt(e)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
*/ |
@ -0,0 +1,28 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.com>. All rights reserved.
|
||||
// Use of this source code is governed by a MIT license that can
|
||||
// be found in the LICENSE file.
|
||||
//
|
||||
// Portions of this file uses [termbox-go](https://github.com/nsf/termbox-go/blob/54b74d087b7c397c402d0e3b66d2ccb6eaf5c2b4/api_common.go)
|
||||
// by [authors](https://github.com/nsf/termbox-go/blob/master/AUTHORS)
|
||||
// under [license](https://github.com/nsf/termbox-go/blob/master/LICENSE)
|
||||
|
||||
package termui |
||||
|
||||
import ( |
||||
"errors" |
||||
"testing" |
||||
|
||||
termbox "github.com/nsf/termbox-go" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
type boxEvent termbox.Event |
||||
|
||||
func TestUiEvt(t *testing.T) { |
||||
err := errors.New("This is a mock error") |
||||
event := boxEvent{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} |
||||
expetced := Event{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} |
||||
|
||||
// We need to do that ugly casting so that vet does not complain
|
||||
assert.Equal(t, uiEvt(termbox.Event(event)), expetced) |
||||
} |
@ -0,0 +1,35 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
bc := termui.NewBarChart() |
||||
data := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} |
||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} |
||||
bc.Border.Label = "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 |
||||
|
||||
termui.Render(bc) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 443 KiB |
@ -0,0 +1,148 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import ui "github.com/gizak/termui" |
||||
import "math" |
||||
|
||||
import "time" |
||||
|
||||
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.Border.Label = "Text Box" |
||||
p.Border.FgColor = ui.ColorCyan |
||||
|
||||
strs := []string{"[0] 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"} |
||||
list := ui.NewList() |
||||
list.Items = strs |
||||
list.ItemFgColor = ui.ColorYellow |
||||
list.Border.Label = "List" |
||||
list.Height = 7 |
||||
list.Width = 25 |
||||
list.Y = 4 |
||||
|
||||
g := ui.NewGauge() |
||||
g.Percent = 50 |
||||
g.Width = 50 |
||||
g.Height = 3 |
||||
g.Y = 11 |
||||
g.Border.Label = "Gauge" |
||||
g.BarColor = ui.ColorRed |
||||
g.Border.FgColor = ui.ColorWhite |
||||
g.Border.LabelFgColor = ui.ColorCyan |
||||
|
||||
spark := ui.Sparkline{} |
||||
spark.Height = 1 |
||||
spark.Title = "srv 0:" |
||||
spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} |
||||
spark.Data = spdata |
||||
spark.LineColor = ui.ColorCyan |
||||
spark.TitleColor = ui.ColorWhite |
||||
|
||||
spark1 := ui.Sparkline{} |
||||
spark1.Height = 1 |
||||
spark1.Title = "srv 1:" |
||||
spark1.Data = spdata |
||||
spark1.TitleColor = ui.ColorWhite |
||||
spark1.LineColor = ui.ColorRed |
||||
|
||||
sp := ui.NewSparklines(spark, spark1) |
||||
sp.Width = 25 |
||||
sp.Height = 7 |
||||
sp.Border.Label = "Sparkline" |
||||
sp.Y = 4 |
||||
sp.X = 25 |
||||
|
||||
sinps := (func() []float64 { |
||||
n := 220 |
||||
ps := make([]float64, n) |
||||
for i := range ps { |
||||
ps[i] = 1 + math.Sin(float64(i)/5) |
||||
} |
||||
return ps |
||||
})() |
||||
|
||||
lc := ui.NewLineChart() |
||||
lc.Border.Label = "dot-mode Line Chart" |
||||
lc.Data = sinps |
||||
lc.Width = 50 |
||||
lc.Height = 11 |
||||
lc.X = 0 |
||||
lc.Y = 14 |
||||
lc.AxesColor = ui.ColorWhite |
||||
lc.LineColor = ui.ColorRed | ui.AttrBold |
||||
lc.Mode = "dot" |
||||
|
||||
bc := ui.NewBarChart() |
||||
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} |
||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} |
||||
bc.Border.Label = "Bar Chart" |
||||
bc.Width = 26 |
||||
bc.Height = 10 |
||||
bc.X = 51 |
||||
bc.Y = 0 |
||||
bc.DataLabels = bclabels |
||||
bc.BarColor = ui.ColorGreen |
||||
bc.NumColor = ui.ColorBlack |
||||
|
||||
lc1 := ui.NewLineChart() |
||||
lc1.Border.Label = "braille-mode Line Chart" |
||||
lc1.Data = sinps |
||||
lc1.Width = 26 |
||||
lc1.Height = 11 |
||||
lc1.X = 51 |
||||
lc1.Y = 14 |
||||
lc1.AxesColor = ui.ColorWhite |
||||
lc1.LineColor = ui.ColorYellow | ui.AttrBold |
||||
|
||||
p1 := ui.NewPar("Hey!\nI am a borderless block!") |
||||
p1.HasBorder = false |
||||
p1.Width = 26 |
||||
p1.Height = 2 |
||||
p1.TextFgColor = ui.ColorMagenta |
||||
p1.X = 52 |
||||
p1.Y = 11 |
||||
|
||||
draw := func(t int) { |
||||
g.Percent = t % 101 |
||||
list.Items = strs[t%9:] |
||||
sp.Lines[0].Data = spdata[:30+t%50] |
||||
sp.Lines[1].Data = spdata[:35+t%50] |
||||
lc.Data = sinps[t/2:] |
||||
lc1.Data = sinps[2*t:] |
||||
bc.Data = bcdata[t/2%10:] |
||||
ui.Render(p, list, g, sp, lc, bc, lc1, p1) |
||||
} |
||||
|
||||
evt := ui.EventCh() |
||||
|
||||
i := 0 |
||||
for { |
||||
select { |
||||
case e := <-evt: |
||||
if e.Type == ui.EventKey && e.Ch == 'q' { |
||||
return |
||||
} |
||||
default: |
||||
draw(i) |
||||
i++ |
||||
if i == 102 { |
||||
return |
||||
} |
||||
time.Sleep(time.Second / 2) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
g0 := termui.NewGauge() |
||||
g0.Percent = 40 |
||||
g0.Width = 50 |
||||
g0.Height = 3 |
||||
g0.Border.Label = "Slim Gauge" |
||||
g0.BarColor = termui.ColorRed |
||||
g0.Border.FgColor = termui.ColorWhite |
||||
g0.Border.LabelFgColor = termui.ColorCyan |
||||
|
||||
g2 := termui.NewGauge() |
||||
g2.Percent = 60 |
||||
g2.Width = 50 |
||||
g2.Height = 3 |
||||
g2.PercentColor = termui.ColorBlue |
||||
g2.Y = 3 |
||||
g2.Border.Label = "Slim Gauge" |
||||
g2.BarColor = termui.ColorYellow |
||||
g2.Border.FgColor = termui.ColorWhite |
||||
|
||||
g1 := termui.NewGauge() |
||||
g1.Percent = 30 |
||||
g1.Width = 50 |
||||
g1.Height = 5 |
||||
g1.Y = 6 |
||||
g1.Border.Label = "Big Gauge" |
||||
g1.PercentColor = termui.ColorYellow |
||||
g1.BarColor = termui.ColorGreen |
||||
g1.Border.FgColor = termui.ColorWhite |
||||
g1.Border.LabelFgColor = termui.ColorMagenta |
||||
|
||||
g3 := termui.NewGauge() |
||||
g3.Percent = 50 |
||||
g3.Width = 50 |
||||
g3.Height = 3 |
||||
g3.Y = 11 |
||||
g3.Border.Label = "Gauge with custom label" |
||||
g3.Label = "{{percent}}% (100MBs free)" |
||||
g3.LabelAlign = termui.AlignRight |
||||
|
||||
termui.Render(g0, g1, g2, g3) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 782 KiB |
@ -0,0 +1,134 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import ui "github.com/gizak/termui" |
||||
import "math" |
||||
import "time" |
||||
|
||||
func main() { |
||||
err := ui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer ui.Close() |
||||
|
||||
sinps := (func() []float64 { |
||||
n := 400 |
||||
ps := make([]float64, n) |
||||
for i := range ps { |
||||
ps[i] = 1 + math.Sin(float64(i)/5) |
||||
} |
||||
return ps |
||||
})() |
||||
sinpsint := (func() []int { |
||||
ps := make([]int, len(sinps)) |
||||
for i, v := range sinps { |
||||
ps[i] = int(100*v + 10) |
||||
} |
||||
return ps |
||||
})() |
||||
|
||||
ui.UseTheme("helloworld") |
||||
|
||||
spark := ui.Sparkline{} |
||||
spark.Height = 8 |
||||
spdata := sinpsint |
||||
spark.Data = spdata[:100] |
||||
spark.LineColor = ui.ColorCyan |
||||
spark.TitleColor = ui.ColorWhite |
||||
|
||||
sp := ui.NewSparklines(spark) |
||||
sp.Height = 11 |
||||
sp.Border.Label = "Sparkline" |
||||
|
||||
lc := ui.NewLineChart() |
||||
lc.Border.Label = "braille-mode Line Chart" |
||||
lc.Data = sinps |
||||
lc.Height = 11 |
||||
lc.AxesColor = ui.ColorWhite |
||||
lc.LineColor = ui.ColorYellow | ui.AttrBold |
||||
|
||||
gs := make([]*ui.Gauge, 3) |
||||
for i := range gs { |
||||
gs[i] = ui.NewGauge() |
||||
gs[i].Height = 2 |
||||
gs[i].HasBorder = false |
||||
gs[i].Percent = i * 10 |
||||
gs[i].PaddingBottom = 1 |
||||
gs[i].BarColor = ui.ColorRed |
||||
} |
||||
|
||||
ls := ui.NewList() |
||||
ls.HasBorder = false |
||||
ls.Items = []string{ |
||||
"[1] Downloading File 1", |
||||
"", // == \newline
|
||||
"[2] Downloading File 2", |
||||
"", |
||||
"[3] Uploading File 3", |
||||
} |
||||
ls.Height = 5 |
||||
|
||||
par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget") |
||||
par.Height = 5 |
||||
par.Border.Label = "Demonstration" |
||||
|
||||
// build layout
|
||||
ui.Body.AddRows( |
||||
ui.NewRow( |
||||
ui.NewCol(6, 0, sp), |
||||
ui.NewCol(6, 0, lc)), |
||||
ui.NewRow( |
||||
ui.NewCol(3, 0, ls), |
||||
ui.NewCol(3, 0, gs[0], gs[1], gs[2]), |
||||
ui.NewCol(6, 0, par))) |
||||
|
||||
// calculate layout
|
||||
ui.Body.Align() |
||||
|
||||
done := make(chan bool) |
||||
redraw := make(chan bool) |
||||
|
||||
update := func() { |
||||
for i := 0; i < 103; i++ { |
||||
for _, g := range gs { |
||||
g.Percent = (g.Percent + 3) % 100 |
||||
} |
||||
|
||||
sp.Lines[0].Data = spdata[:100+i] |
||||
lc.Data = sinps[2*i:] |
||||
|
||||
time.Sleep(time.Second / 2) |
||||
redraw <- true |
||||
} |
||||
done <- true |
||||
} |
||||
|
||||
evt := ui.EventCh() |
||||
|
||||
ui.Render(ui.Body) |
||||
go update() |
||||
|
||||
for { |
||||
select { |
||||
case e := <-evt: |
||||
if e.Type == ui.EventKey && e.Ch == 'q' { |
||||
return |
||||
} |
||||
if e.Type == ui.EventResize { |
||||
ui.Body.Width = ui.TermWidth() |
||||
ui.Body.Align() |
||||
go func() { redraw <- true }() |
||||
} |
||||
case <-done: |
||||
return |
||||
case <-redraw: |
||||
ui.Render(ui.Body) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,68 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import ( |
||||
"math" |
||||
|
||||
"github.com/gizak/termui" |
||||
) |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
sinps := (func() []float64 { |
||||
n := 220 |
||||
ps := make([]float64, n) |
||||
for i := range ps { |
||||
ps[i] = 1 + math.Sin(float64(i)/5) |
||||
} |
||||
return ps |
||||
})() |
||||
|
||||
lc0 := termui.NewLineChart() |
||||
lc0.Border.Label = "braille-mode Line Chart" |
||||
lc0.Data = sinps |
||||
lc0.Width = 50 |
||||
lc0.Height = 12 |
||||
lc0.X = 0 |
||||
lc0.Y = 0 |
||||
lc0.AxesColor = termui.ColorWhite |
||||
lc0.LineColor = termui.ColorGreen | termui.AttrBold |
||||
|
||||
lc1 := termui.NewLineChart() |
||||
lc1.Border.Label = "dot-mode Line Chart" |
||||
lc1.Mode = "dot" |
||||
lc1.Data = sinps |
||||
lc1.Width = 26 |
||||
lc1.Height = 12 |
||||
lc1.X = 51 |
||||
lc1.DotStyle = '+' |
||||
lc1.AxesColor = termui.ColorWhite |
||||
lc1.LineColor = termui.ColorYellow | termui.AttrBold |
||||
|
||||
lc2 := termui.NewLineChart() |
||||
lc2.Border.Label = "dot-mode Line Chart" |
||||
lc2.Mode = "dot" |
||||
lc2.Data = sinps[4:] |
||||
lc2.Width = 77 |
||||
lc2.Height = 16 |
||||
lc2.X = 0 |
||||
lc2.Y = 12 |
||||
lc2.AxesColor = termui.ColorWhite |
||||
lc2.LineColor = termui.ColorCyan | termui.AttrBold |
||||
|
||||
termui.Render(lc0, lc1, lc2) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 136 KiB |
@ -0,0 +1,41 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
strs := []string{ |
||||
"[0] github.com/gizak/termui", |
||||
"[1] 你好,世界", |
||||
"[2] こんにちは世界", |
||||
"[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.Border.Label = "List" |
||||
ls.Height = 7 |
||||
ls.Width = 25 |
||||
ls.Y = 0 |
||||
|
||||
termui.Render(ls) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 36 KiB |
@ -0,0 +1,50 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
bc := termui.NewMBarChart() |
||||
math := []int{90, 85, 90, 80} |
||||
english := []int{70, 85, 75, 60} |
||||
science := []int{75, 60, 80, 85} |
||||
compsci := []int{100, 100, 100, 100} |
||||
bc.Data[0] = math |
||||
bc.Data[1] = english |
||||
bc.Data[2] = science |
||||
bc.Data[3] = compsci |
||||
studentsName := []string{"Ken", "Rob", "Dennis", "Linus"} |
||||
bc.Border.Label = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %" |
||||
bc.Width = 100 |
||||
bc.Height = 50 |
||||
bc.Y = 10 |
||||
bc.BarWidth = 10 |
||||
bc.DataLabels = studentsName |
||||
bc.ShowScale = true //Show y_axis scale value (min and max)
|
||||
bc.SetMax(400) |
||||
|
||||
bc.TextColor = termui.ColorGreen //this is color for label (x-axis)
|
||||
bc.BarColor[3] = termui.ColorGreen //BarColor for computerscience
|
||||
bc.BarColor[1] = termui.ColorYellow //Bar Color for english
|
||||
bc.NumColor[3] = termui.ColorRed // Num color for computerscience
|
||||
bc.NumColor[1] = termui.ColorRed // num color for english
|
||||
|
||||
//Other colors are automatically populated, btw All the students seems do well in computerscience. :p
|
||||
|
||||
termui.Render(bc) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,48 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
par0 := termui.NewPar("Borderless Text") |
||||
par0.Height = 1 |
||||
par0.Width = 20 |
||||
par0.Y = 1 |
||||
par0.HasBorder = false |
||||
|
||||
par1 := termui.NewPar("你好,世界。") |
||||
par1.Height = 3 |
||||
par1.Width = 17 |
||||
par1.X = 20 |
||||
par1.Border.Label = "标签" |
||||
|
||||
par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically") |
||||
par2.Height = 5 |
||||
par2.Width = 37 |
||||
par2.Y = 4 |
||||
par2.Border.Label = "Multiline" |
||||
par2.Border.FgColor = termui.ColorYellow |
||||
|
||||
par3 := termui.NewPar("Long text with label and it is auto trimmed.") |
||||
par3.Height = 3 |
||||
par3.Width = 37 |
||||
par3.Y = 9 |
||||
par3.Border.Label = "Auto Trim" |
||||
|
||||
termui.Render(par0, par1, par2, par3) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 64 KiB |
@ -0,0 +1,65 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import "github.com/gizak/termui" |
||||
|
||||
func main() { |
||||
err := termui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer termui.Close() |
||||
|
||||
termui.UseTheme("helloworld") |
||||
|
||||
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, 7, 10, 10, 14, 13, 6} |
||||
spl0 := termui.NewSparkline() |
||||
spl0.Data = data[3:] |
||||
spl0.Title = "Sparkline 0" |
||||
spl0.LineColor = termui.ColorGreen |
||||
|
||||
// single
|
||||
spls0 := termui.NewSparklines(spl0) |
||||
spls0.Height = 2 |
||||
spls0.Width = 20 |
||||
spls0.HasBorder = false |
||||
|
||||
spl1 := termui.NewSparkline() |
||||
spl1.Data = data |
||||
spl1.Title = "Sparkline 1" |
||||
spl1.LineColor = termui.ColorRed |
||||
|
||||
spl2 := termui.NewSparkline() |
||||
spl2.Data = data[5:] |
||||
spl2.Title = "Sparkline 2" |
||||
spl2.LineColor = termui.ColorMagenta |
||||
|
||||
// group
|
||||
spls1 := termui.NewSparklines(spl0, spl1, spl2) |
||||
spls1.Height = 8 |
||||
spls1.Width = 20 |
||||
spls1.Y = 3 |
||||
spls1.Border.Label = "Group Sparklines" |
||||
|
||||
spl3 := termui.NewSparkline() |
||||
spl3.Data = data |
||||
spl3.Title = "Enlarged Sparkline" |
||||
spl3.Height = 8 |
||||
spl3.LineColor = termui.ColorYellow |
||||
|
||||
spls2 := termui.NewSparklines(spl3) |
||||
spls2.Height = 11 |
||||
spls2.Width = 30 |
||||
spls2.Border.FgColor = termui.ColorCyan |
||||
spls2.X = 21 |
||||
spls2.Border.Label = "Tweeked Sparkline" |
||||
|
||||
termui.Render(spls0, spls1, spls2) |
||||
|
||||
<-termui.EventCh() |
||||
} |
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,143 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 main |
||||
|
||||
import ui "github.com/gizak/termui" |
||||
import "math" |
||||
|
||||
import "time" |
||||
|
||||
func main() { |
||||
err := ui.Init() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer ui.Close() |
||||
|
||||
ui.UseTheme("helloworld") |
||||
|
||||
p := ui.NewPar(":PRESS q TO QUIT DEMO") |
||||
p.Height = 3 |
||||
p.Width = 50 |
||||
p.Border.Label = "Text Box" |
||||
|
||||
strs := []string{"[0] 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"} |
||||
list := ui.NewList() |
||||
list.Items = strs |
||||
list.Border.Label = "List" |
||||
list.Height = 7 |
||||
list.Width = 25 |
||||
list.Y = 4 |
||||
|
||||
g := ui.NewGauge() |
||||
g.Percent = 50 |
||||
g.Width = 50 |
||||
g.Height = 3 |
||||
g.Y = 11 |
||||
g.Border.Label = "Gauge" |
||||
|
||||
spark := ui.NewSparkline() |
||||
spark.Title = "srv 0:" |
||||
spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} |
||||
spark.Data = spdata |
||||
|
||||
spark1 := ui.NewSparkline() |
||||
spark1.Title = "srv 1:" |
||||
spark1.Data = spdata |
||||
|
||||
sp := ui.NewSparklines(spark, spark1) |
||||
sp.Width = 25 |
||||
sp.Height = 7 |
||||
sp.Border.Label = "Sparkline" |
||||
sp.Y = 4 |
||||
sp.X = 25 |
||||
|
||||
lc := ui.NewLineChart() |
||||
sinps := (func() []float64 { |
||||
n := 100 |
||||
ps := make([]float64, n) |
||||
for i := range ps { |
||||
ps[i] = 1 + math.Sin(float64(i)/4) |
||||
} |
||||
return ps |
||||
})() |
||||
|
||||
lc.Border.Label = "Line Chart" |
||||
lc.Data = sinps |
||||
lc.Width = 50 |
||||
lc.Height = 11 |
||||
lc.X = 0 |
||||
lc.Y = 14 |
||||
lc.Mode = "dot" |
||||
|
||||
bc := ui.NewBarChart() |
||||
bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} |
||||
bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} |
||||
bc.Border.Label = "Bar Chart" |
||||
bc.Width = 26 |
||||
bc.Height = 10 |
||||
bc.X = 51 |
||||
bc.Y = 0 |
||||
bc.DataLabels = bclabels |
||||
|
||||
lc1 := ui.NewLineChart() |
||||
lc1.Border.Label = "Line Chart" |
||||
rndwalk := (func() []float64 { |
||||
n := 150 |
||||
d := make([]float64, n) |
||||
for i := 1; i < n; i++ { |
||||
if i < 20 { |
||||
d[i] = d[i-1] + 0.01 |
||||
} |
||||
if i > 20 { |
||||
d[i] = d[i-1] - 0.05 |
||||
} |
||||
} |
||||
return d |
||||
})() |
||||
lc1.Data = rndwalk |
||||
lc1.Width = 26 |
||||
lc1.Height = 11 |
||||
lc1.X = 51 |
||||
lc1.Y = 14 |
||||
|
||||
p1 := ui.NewPar("Hey!\nI am a borderless block!") |
||||
p1.HasBorder = false |
||||
p1.Width = 26 |
||||
p1.Height = 2 |
||||
p1.X = 52 |
||||
p1.Y = 11 |
||||
|
||||
draw := func(t int) { |
||||
g.Percent = t % 101 |
||||
list.Items = strs[t%9:] |
||||
sp.Lines[0].Data = spdata[t%10:] |
||||
sp.Lines[1].Data = spdata[t/2%10:] |
||||
lc.Data = sinps[t/2:] |
||||
lc1.Data = rndwalk[t:] |
||||
bc.Data = bcdata[t/2%10:] |
||||
ui.Render(p, list, g, sp, lc, bc, lc1, p1) |
||||
} |
||||
|
||||
evt := ui.EventCh() |
||||
i := 0 |
||||
for { |
||||
select { |
||||
case e := <-evt: |
||||
if e.Type == ui.EventKey && e.Ch == 'q' { |
||||
return |
||||
} |
||||
default: |
||||
draw(i) |
||||
i++ |
||||
if i == 102 { |
||||
return |
||||
} |
||||
time.Sleep(time.Second / 2) |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 88 KiB |
@ -0,0 +1,113 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "Slim Gauge" |
||||
g.BarColor = termui.ColorRed |
||||
g.PercentColor = termui.ColorBlue |
||||
*/ |
||||
|
||||
// Align is the position of the gauge's label.
|
||||
type Align int |
||||
|
||||
// All supported positions.
|
||||
const ( |
||||
AlignLeft Align = iota |
||||
AlignCenter |
||||
AlignRight |
||||
) |
||||
|
||||
type Gauge struct { |
||||
Block |
||||
Percent int |
||||
BarColor Attribute |
||||
PercentColor Attribute |
||||
Label string |
||||
LabelAlign Align |
||||
} |
||||
|
||||
// NewGauge return a new gauge with current theme.
|
||||
func NewGauge() *Gauge { |
||||
g := &Gauge{ |
||||
Block: *NewBlock(), |
||||
PercentColor: theme.GaugePercent, |
||||
BarColor: theme.GaugeBar, |
||||
Label: "{{percent}}%", |
||||
LabelAlign: AlignCenter, |
||||
} |
||||
|
||||
g.Width = 12 |
||||
g.Height = 5 |
||||
return g |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (g *Gauge) Buffer() []Point { |
||||
ps := g.Block.Buffer() |
||||
|
||||
// plot bar
|
||||
w := g.Percent * g.innerWidth / 100 |
||||
for i := 0; i < g.innerHeight; i++ { |
||||
for j := 0; j < w; j++ { |
||||
p := Point{} |
||||
p.X = g.innerX + j |
||||
p.Y = g.innerY + i |
||||
p.Ch = ' ' |
||||
p.Bg = g.BarColor |
||||
if p.Bg == ColorDefault { |
||||
p.Bg |= AttrReverse |
||||
} |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
|
||||
// plot percentage
|
||||
s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1) |
||||
pry := g.innerY + g.innerHeight/2 |
||||
rs := str2runes(s) |
||||
var pos int |
||||
switch g.LabelAlign { |
||||
case AlignLeft: |
||||
pos = 0 |
||||
|
||||
case AlignCenter: |
||||
pos = (g.innerWidth - strWidth(s)) / 2 |
||||
|
||||
case AlignRight: |
||||
pos = g.innerWidth - strWidth(s) |
||||
} |
||||
|
||||
for i, v := range rs { |
||||
p := Point{} |
||||
p.X = 1 + pos + i |
||||
p.Y = pry |
||||
p.Ch = v |
||||
p.Fg = g.PercentColor |
||||
if w+g.innerX > pos+i { |
||||
p.Bg = g.BarColor |
||||
if p.Bg == ColorDefault { |
||||
p.Bg |= AttrReverse |
||||
} |
||||
|
||||
} else { |
||||
p.Bg = g.Block.BgColor |
||||
} |
||||
|
||||
ps = append(ps, p) |
||||
} |
||||
return g.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,279 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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() []Point { |
||||
merged := []Point{} |
||||
|
||||
if r.isRenderableLeaf() { |
||||
return r.Widget.Buffer() |
||||
} |
||||
|
||||
// for those are not leaves but have a renderable widget
|
||||
if r.Widget != nil { |
||||
merged = append(merged, r.Widget.Buffer()...) |
||||
} |
||||
|
||||
// collect buffer from children
|
||||
if !r.isLeaf() { |
||||
for _, c := range r.Cols { |
||||
merged = append(merged, 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() []Point { |
||||
ps := []Point{} |
||||
for _, r := range g.Rows { |
||||
ps = append(ps, r.Buffer()...) |
||||
} |
||||
return ps |
||||
} |
||||
|
||||
// Body corresponds to the entire terminal display region.
|
||||
var Body *Grid |
@ -0,0 +1,98 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 := NewPar("p0") |
||||
p0.Height = 1 |
||||
p1 := NewPar("p1") |
||||
p1.Height = 1 |
||||
p2 := NewPar("p2") |
||||
p2.Height = 1 |
||||
p3 := NewPar("p3") |
||||
p3.Height = 1 |
||||
|
||||
/* test against tree: |
||||
|
||||
r |
||||
/ \
|
||||
0:w 1 |
||||
/ \
|
||||
10:w 11 |
||||
/ |
||||
110:w |
||||
/ |
||||
1100:w |
||||
*/ |
||||
/* |
||||
r = &row{ |
||||
Span: 12, |
||||
Cols: []*row{ |
||||
&row{Widget: p0, Span: 6}, |
||||
&row{ |
||||
Span: 6, |
||||
Cols: []*row{ |
||||
&row{Widget: p1, Span: 6}, |
||||
&row{ |
||||
Span: 6, |
||||
Cols: []*row{ |
||||
&row{ |
||||
Span: 12, |
||||
Widget: p2, |
||||
Cols: []*row{ |
||||
&row{Span: 12, Widget: p3}}}}}}}}} |
||||
*/ |
||||
|
||||
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,66 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 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 |
||||
|
||||
const ( |
||||
ColorDefault Attribute = iota |
||||
ColorBlack |
||||
ColorRed |
||||
ColorGreen |
||||
ColorYellow |
||||
ColorBlue |
||||
ColorMagenta |
||||
ColorCyan |
||||
ColorWhite |
||||
) |
||||
|
||||
const NumberofColors = 8 //Have a constant that defines number of colors
|
||||
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) |
||||
} |
||||
|
||||
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) //[]rune(rw.Truncate(s, w, ""))
|
||||
} |
||||
|
||||
func strWidth(s string) int { |
||||
return rw.StringWidth(s) |
||||
} |
||||
|
||||
func charWidth(ch rune) int { |
||||
return rw.RuneWidth(ch) |
||||
} |
@ -0,0 +1,58 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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" |
||||
) |
||||
|
||||
func TestStr2Rune(t *testing.T) { |
||||
s := "你好,世界." |
||||
rs := str2runes(s) |
||||
if len(rs) != 6 { |
||||
t.Error() |
||||
} |
||||
} |
||||
|
||||
func TestWidth(t *testing.T) { |
||||
s0 := "つのだ☆HIRO" |
||||
s1 := "11111111111" |
||||
spew.Dump(s0) |
||||
spew.Dump(s1) |
||||
// 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") |
||||
} |
||||
} |
@ -0,0 +1,104 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "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 = theme.ListItemFg |
||||
l.ItemBgColor = theme.ListItemBg |
||||
return l |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (l *List) Buffer() []Point { |
||||
ps := l.Block.Buffer() |
||||
switch l.Overflow { |
||||
case "wrap": |
||||
rs := str2runes(strings.Join(l.Items, "\n")) |
||||
i, j, k := 0, 0, 0 |
||||
for i < l.innerHeight && k < len(rs) { |
||||
w := charWidth(rs[k]) |
||||
if rs[k] == '\n' || j+w > l.innerWidth { |
||||
i++ |
||||
j = 0 |
||||
if rs[k] == '\n' { |
||||
k++ |
||||
} |
||||
continue |
||||
} |
||||
pi := Point{} |
||||
pi.X = l.innerX + j |
||||
pi.Y = l.innerY + i |
||||
|
||||
pi.Ch = rs[k] |
||||
pi.Bg = l.ItemBgColor |
||||
pi.Fg = l.ItemFgColor |
||||
|
||||
ps = append(ps, pi) |
||||
k++ |
||||
j++ |
||||
} |
||||
|
||||
case "hidden": |
||||
trimItems := l.Items |
||||
if len(trimItems) > l.innerHeight { |
||||
trimItems = trimItems[:l.innerHeight] |
||||
} |
||||
for i, v := range trimItems { |
||||
rs := trimStr2Runes(v, l.innerWidth) |
||||
|
||||
j := 0 |
||||
for _, vv := range rs { |
||||
w := charWidth(vv) |
||||
p := Point{} |
||||
p.X = l.innerX + j |
||||
p.Y = l.innerY + i |
||||
|
||||
p.Ch = vv |
||||
p.Bg = l.ItemBgColor |
||||
p.Fg = l.ItemFgColor |
||||
|
||||
ps = append(ps, p) |
||||
j += w |
||||
} |
||||
} |
||||
} |
||||
return l.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,233 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "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] = theme.MBarChartBar |
||||
bc.NumColor[0] = theme.MBarChartNum |
||||
bc.TextColor = theme.MBarChartText |
||||
bc.BarGap = 1 |
||||
bc.BarWidth = 3 |
||||
return bc |
||||
} |
||||
|
||||
func (bc *MBarChart) layout() { |
||||
bc.numBar = bc.innerWidth / (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.innerHeight-2) |
||||
} else { |
||||
bc.scale = float64(bc.max) / float64(bc.innerHeight-1) |
||||
} |
||||
|
||||
} |
||||
|
||||
func (bc *MBarChart) SetMax(max int) { |
||||
|
||||
if max > 0 { |
||||
bc.max = max |
||||
} |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (bc *MBarChart) Buffer() []Point { |
||||
ps := 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++ { |
||||
p := Point{} |
||||
p.Ch = ' ' |
||||
p.Bg = bc.BarColor[i1] |
||||
if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent!
|
||||
p.Bg |= AttrReverse |
||||
} |
||||
p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j |
||||
p.Y = bc.innerY + bc.innerHeight - 2 - k - ph |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
ph += h |
||||
} |
||||
// plot text
|
||||
for j, k := 0, 0; j < len(bc.labels[i]); j++ { |
||||
w := charWidth(bc.labels[i][j]) |
||||
p := Point{} |
||||
p.Ch = bc.labels[i][j] |
||||
p.Bg = bc.BgColor |
||||
p.Fg = bc.TextColor |
||||
p.Y = bc.innerY + bc.innerHeight - 1 |
||||
p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k |
||||
ps = append(ps, p) |
||||
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++ { |
||||
p := Point{} |
||||
p.Ch = bc.dataNum[i1][i][j] |
||||
p.Fg = bc.NumColor[i1] |
||||
p.Bg = bc.BarColor[i1] |
||||
if bc.BarColor[i1] == ColorDefault { // the same as above
|
||||
p.Bg |= AttrReverse |
||||
} |
||||
if h == 0 { |
||||
p.Bg = bc.BgColor |
||||
} |
||||
p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j |
||||
p.Y = bc.innerY + bc.innerHeight - 2 - ph |
||||
ps = append(ps, p) |
||||
} |
||||
ph += h |
||||
} |
||||
} |
||||
|
||||
if bc.ShowScale { |
||||
//Currently bar graph only supprts data range from 0 to MAX
|
||||
//Plot 0
|
||||
p := Point{} |
||||
p.Ch = '0' |
||||
p.Bg = bc.BgColor |
||||
p.Fg = bc.TextColor |
||||
p.Y = bc.innerY + bc.innerHeight - 2 |
||||
p.X = bc.X |
||||
ps = append(ps, p) |
||||
|
||||
//Plot the maximum sacle value
|
||||
for i := 0; i < len(bc.maxScale); i++ { |
||||
p := Point{} |
||||
p.Ch = bc.maxScale[i] |
||||
p.Bg = bc.BgColor |
||||
p.Fg = bc.TextColor |
||||
p.Y = bc.innerY |
||||
p.X = bc.X + i |
||||
ps = append(ps, p) |
||||
} |
||||
|
||||
} |
||||
|
||||
return bc.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,71 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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.Border.Label = "Label" |
||||
*/ |
||||
type Par struct { |
||||
Block |
||||
Text string |
||||
TextFgColor Attribute |
||||
TextBgColor Attribute |
||||
} |
||||
|
||||
// NewPar returns a new *Par with given text as its content.
|
||||
func NewPar(s string) *Par { |
||||
return &Par{ |
||||
Block: *NewBlock(), |
||||
Text: s, |
||||
TextFgColor: theme.ParTextFg, |
||||
TextBgColor: theme.ParTextBg} |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (p *Par) Buffer() []Point { |
||||
ps := p.Block.Buffer() |
||||
|
||||
rs := str2runes(p.Text) |
||||
i, j, k := 0, 0, 0 |
||||
for i < p.innerHeight && k < len(rs) { |
||||
// the width of char is about to print
|
||||
w := charWidth(rs[k]) |
||||
|
||||
if rs[k] == '\n' || j+w > p.innerWidth { |
||||
i++ |
||||
j = 0 // set x = 0
|
||||
if rs[k] == '\n' { |
||||
k++ |
||||
} |
||||
|
||||
if i >= p.innerHeight { |
||||
ps = append(ps, newPointWithAttrs('…', |
||||
p.innerX+p.innerWidth-1, |
||||
p.innerY+p.innerHeight-1, |
||||
p.TextFgColor, p.TextBgColor)) |
||||
break |
||||
} |
||||
|
||||
continue |
||||
} |
||||
pi := Point{} |
||||
pi.X = p.innerX + j |
||||
pi.Y = p.innerY + i |
||||
|
||||
pi.Ch = rs[k] |
||||
pi.Bg = p.TextBgColor |
||||
pi.Fg = p.TextFgColor |
||||
|
||||
ps = append(ps, pi) |
||||
|
||||
k++ |
||||
j += w |
||||
} |
||||
return p.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,28 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 |
||||
|
||||
// Point stands for a single cell in terminal.
|
||||
type Point struct { |
||||
Ch rune |
||||
Bg Attribute |
||||
Fg Attribute |
||||
X int |
||||
Y int |
||||
} |
||||
|
||||
func newPoint(c rune, x, y int) (p Point) { |
||||
p.Ch = c |
||||
p.X = x |
||||
p.Y = y |
||||
return |
||||
} |
||||
|
||||
func newPointWithAttrs(c rune, x, y int, fg, bg Attribute) Point { |
||||
p := newPoint(c, x, y) |
||||
p.Bg = bg |
||||
p.Fg = fg |
||||
return p |
||||
} |
@ -0,0 +1,60 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 tm "github.com/nsf/termbox-go" |
||||
|
||||
// Bufferer should be implemented by all renderable components.
|
||||
type Bufferer interface { |
||||
Buffer() []Point |
||||
} |
||||
|
||||
// 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 { |
||||
Body = NewGrid() |
||||
Body.X = 0 |
||||
Body.Y = 0 |
||||
Body.BgColor = theme.BodyBg |
||||
defer func() { |
||||
w, _ := tm.Size() |
||||
Body.Width = w |
||||
evtListen() |
||||
}() |
||||
return tm.Init() |
||||
} |
||||
|
||||
// Close finalizes termui library,
|
||||
// should be called after successful initialization when termui's functionality isn't required anymore.
|
||||
func Close() { |
||||
tm.Close() |
||||
} |
||||
|
||||
// TermWidth returns the current terminal's width.
|
||||
func TermWidth() int { |
||||
tm.Sync() |
||||
w, _ := tm.Size() |
||||
return w |
||||
} |
||||
|
||||
// TermHeight returns the current terminal's height.
|
||||
func TermHeight() int { |
||||
tm.Sync() |
||||
_, h := tm.Size() |
||||
return h |
||||
} |
||||
|
||||
// Render renders all Bufferer in the given order from left to right,
|
||||
// right could overlap on left ones.
|
||||
func Render(rs ...Bufferer) { |
||||
tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg)) |
||||
for _, r := range rs { |
||||
buf := r.Buffer() |
||||
for _, v := range buf { |
||||
tm.SetCell(v.X, v.Y, v.Ch, toTmAttr(v.Fg), toTmAttr(v.Bg)) |
||||
} |
||||
} |
||||
tm.Flush() |
||||
} |
@ -0,0 +1,156 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 "math" |
||||
|
||||
// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃
|
||||
/* |
||||
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: theme.SparklineTitle, |
||||
LineColor: theme.SparklineLine} |
||||
} |
||||
|
||||
// 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.innerWidth |
||||
|
||||
// get how many lines gotta display
|
||||
h := 0 |
||||
sl.displayLines = 0 |
||||
for _, v := range sl.Lines { |
||||
if h+v.displayHeight <= sl.innerHeight { |
||||
sl.displayLines++ |
||||
} else { |
||||
break |
||||
} |
||||
h += v.displayHeight |
||||
} |
||||
|
||||
for i := 0; i < sl.displayLines; i++ { |
||||
data := sl.Lines[i].Data |
||||
|
||||
max := math.MinInt32 |
||||
for _, v := range data { |
||||
if max < v { |
||||
max = v |
||||
} |
||||
} |
||||
sl.Lines[i].max = max |
||||
sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max) |
||||
} |
||||
} |
||||
|
||||
// Buffer implements Bufferer interface.
|
||||
func (sl *Sparklines) Buffer() []Point { |
||||
ps := 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.innerWidth { |
||||
data = data[len(data)-sl.innerWidth:] |
||||
} |
||||
|
||||
if l.Title != "" { |
||||
rs := trimStr2Runes(l.Title, sl.innerWidth) |
||||
oftX := 0 |
||||
for _, v := range rs { |
||||
w := charWidth(v) |
||||
p := Point{} |
||||
p.Ch = v |
||||
p.Fg = l.TitleColor |
||||
p.Bg = sl.BgColor |
||||
p.X = sl.innerX + oftX |
||||
p.Y = sl.innerY + oftY |
||||
ps = append(ps, p) |
||||
oftX += w |
||||
} |
||||
} |
||||
|
||||
for j, v := range data { |
||||
h := int(float32(v)*l.scale + 0.5) |
||||
barCnt := h / 8 |
||||
barMod := h % 8 |
||||
for jj := 0; jj < barCnt; jj++ { |
||||
p := Point{} |
||||
p.X = sl.innerX + j |
||||
p.Y = sl.innerY + oftY + l.Height - jj |
||||
p.Ch = ' ' // => sparks[7]
|
||||
p.Bg = l.LineColor |
||||
//p.Bg = sl.BgColor
|
||||
ps = append(ps, p) |
||||
} |
||||
if barMod != 0 { |
||||
p := Point{} |
||||
p.X = sl.innerX + j |
||||
p.Y = sl.innerY + oftY + l.Height - barCnt |
||||
p.Ch = sparks[barMod-1] |
||||
p.Fg = l.LineColor |
||||
p.Bg = sl.BgColor |
||||
ps = append(ps, p) |
||||
} |
||||
} |
||||
|
||||
oftY += l.displayHeight |
||||
} |
||||
|
||||
return sl.Block.chopOverflow(ps) |
||||
} |
@ -0,0 +1,84 @@ |
||||
// Copyright 2015 Zack Guo <gizak@icloud.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 |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
// 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, |
||||
} |
||||
|
||||
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 |
||||
} |
||||
} |
@ -0,0 +1,23 @@ |
||||
# 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 |
@ -0,0 +1,362 @@ |
||||
Mozilla Public License, version 2.0 |
||||
|
||||
1. Definitions |
||||
|
||||
1.1. "Contributor" |
||||
|
||||
means each individual or legal entity that creates, contributes to the |
||||
creation of, or owns Covered Software. |
||||
|
||||
1.2. "Contributor Version" |
||||
|
||||
means the combination of the Contributions of others (if any) used by a |
||||
Contributor and that particular Contributor's Contribution. |
||||
|
||||
1.3. "Contribution" |
||||
|
||||
means Covered Software of a particular Contributor. |
||||
|
||||
1.4. "Covered Software" |
||||
|
||||
means Source Code Form to which the initial Contributor has attached the |
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and |
||||
Modifications of such Source Code Form, in each case including portions |
||||
thereof. |
||||
|
||||
1.5. "Incompatible With Secondary Licenses" |
||||
means |
||||
|
||||
a. that the initial Contributor has attached the notice described in |
||||
Exhibit B to the Covered Software; or |
||||
|
||||
b. that the Covered Software was made available under the terms of |
||||
version 1.1 or earlier of the License, but not also under the terms of |
||||
a Secondary License. |
||||
|
||||
1.6. "Executable Form" |
||||
|
||||
means any form of the work other than Source Code Form. |
||||
|
||||
1.7. "Larger Work" |
||||
|
||||
means a work that combines Covered Software with other material, in a |
||||
separate file or files, that is not Covered Software. |
||||
|
||||
1.8. "License" |
||||
|
||||
means this document. |
||||
|
||||
1.9. "Licensable" |
||||
|
||||
means having the right to grant, to the maximum extent possible, whether |
||||
at the time of the initial grant or subsequently, any and all of the |
||||
rights conveyed by this License. |
||||
|
||||
1.10. "Modifications" |
||||
|
||||
means any of the following: |
||||
|
||||
a. any file in Source Code Form that results from an addition to, |
||||
deletion from, or modification of the contents of Covered Software; or |
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software. |
||||
|
||||
1.11. "Patent Claims" of a Contributor |
||||
|
||||
means any patent claim(s), including without limitation, method, |
||||
process, and apparatus claims, in any patent Licensable by such |
||||
Contributor that would be infringed, but for the grant of the License, |
||||
by the making, using, selling, offering for sale, having made, import, |
||||
or transfer of either its Contributions or its Contributor Version. |
||||
|
||||
1.12. "Secondary License" |
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser |
||||
General Public License, Version 2.1, the GNU Affero General Public |
||||
License, Version 3.0, or any later versions of those licenses. |
||||
|
||||
1.13. "Source Code Form" |
||||
|
||||
means the form of the work preferred for making modifications. |
||||
|
||||
1.14. "You" (or "Your") |
||||
|
||||
means an individual or a legal entity exercising rights under this |
||||
License. For legal entities, "You" includes any entity that controls, is |
||||
controlled by, or is under common control with You. For purposes of this |
||||
definition, "control" means (a) the power, direct or indirect, to cause |
||||
the direction or management of such entity, whether by contract or |
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the |
||||
outstanding shares or beneficial ownership of such entity. |
||||
|
||||
|
||||
2. License Grants and Conditions |
||||
|
||||
2.1. Grants |
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free, |
||||
non-exclusive license: |
||||
|
||||
a. under intellectual property rights (other than patent or trademark) |
||||
Licensable by such Contributor to use, reproduce, make available, |
||||
modify, display, perform, distribute, and otherwise exploit its |
||||
Contributions, either on an unmodified basis, with Modifications, or |
||||
as part of a Larger Work; and |
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for |
||||
sale, have made, import, and otherwise transfer either its |
||||
Contributions or its Contributor Version. |
||||
|
||||
2.2. Effective Date |
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution |
||||
become effective for each Contribution on the date the Contributor first |
||||
distributes such Contribution. |
||||
|
||||
2.3. Limitations on Grant Scope |
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under |
||||
this License. No additional rights or licenses will be implied from the |
||||
distribution or licensing of Covered Software under this License. |
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a |
||||
Contributor: |
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or |
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's |
||||
modifications of Covered Software, or (ii) the combination of its |
||||
Contributions with other software (except as part of its Contributor |
||||
Version); or |
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of |
||||
its Contributions. |
||||
|
||||
This License does not grant any rights in the trademarks, service marks, |
||||
or logos of any Contributor (except as may be necessary to comply with |
||||
the notice requirements in Section 3.4). |
||||
|
||||
2.4. Subsequent Licenses |
||||
|
||||
No Contributor makes additional grants as a result of Your choice to |
||||
distribute the Covered Software under a subsequent version of this |
||||
License (see Section 10.2) or under the terms of a Secondary License (if |
||||
permitted under the terms of Section 3.3). |
||||
|
||||
2.5. Representation |
||||
|
||||
Each Contributor represents that the Contributor believes its |
||||
Contributions are its original creation(s) or it has sufficient rights to |
||||
grant the rights to its Contributions conveyed by this License. |
||||
|
||||
2.6. Fair Use |
||||
|
||||
This License is not intended to limit any rights You have under |
||||
applicable copyright doctrines of fair use, fair dealing, or other |
||||
equivalents. |
||||
|
||||
2.7. Conditions |
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in |
||||
Section 2.1. |
||||
|
||||
|
||||
3. Responsibilities |
||||
|
||||
3.1. Distribution of Source Form |
||||
|
||||
All distribution of Covered Software in Source Code Form, including any |
||||
Modifications that You create or to which You contribute, must be under |
||||
the terms of this License. You must inform recipients that the Source |
||||
Code Form of the Covered Software is governed by the terms of this |
||||
License, and how they can obtain a copy of this License. You may not |
||||
attempt to alter or restrict the recipients' rights in the Source Code |
||||
Form. |
||||
|
||||
3.2. Distribution of Executable Form |
||||
|
||||
If You distribute Covered Software in Executable Form then: |
||||
|
||||
a. such Covered Software must also be made available in Source Code Form, |
||||
as described in Section 3.1, and You must inform recipients of the |
||||
Executable Form how they can obtain a copy of such Source Code Form by |
||||
reasonable means in a timely manner, at a charge no more than the cost |
||||
of distribution to the recipient; and |
||||
|
||||
b. You may distribute such Executable Form under the terms of this |
||||
License, or sublicense it under different terms, provided that the |
||||
license for the Executable Form does not attempt to limit or alter the |
||||
recipients' rights in the Source Code Form under this License. |
||||
|
||||
3.3. Distribution of a Larger Work |
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice, |
||||
provided that You also comply with the requirements of this License for |
||||
the Covered Software. If the Larger Work is a combination of Covered |
||||
Software with a work governed by one or more Secondary Licenses, and the |
||||
Covered Software is not Incompatible With Secondary Licenses, this |
||||
License permits You to additionally distribute such Covered Software |
||||
under the terms of such Secondary License(s), so that the recipient of |
||||
the Larger Work may, at their option, further distribute the Covered |
||||
Software under the terms of either this License or such Secondary |
||||
License(s). |
||||
|
||||
3.4. Notices |
||||
|
||||
You may not remove or alter the substance of any license notices |
||||
(including copyright notices, patent notices, disclaimers of warranty, or |
||||
limitations of liability) contained within the Source Code Form of the |
||||
Covered Software, except that You may alter any license notices to the |
||||
extent required to remedy known factual inaccuracies. |
||||
|
||||
3.5. Application of Additional Terms |
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support, |
||||
indemnity or liability obligations to one or more recipients of Covered |
||||
Software. However, You may do so only on Your own behalf, and not on |
||||
behalf of any Contributor. You must make it absolutely clear that any |
||||
such warranty, support, indemnity, or liability obligation is offered by |
||||
You alone, and You hereby agree to indemnify every Contributor for any |
||||
liability incurred by such Contributor as a result of warranty, support, |
||||
indemnity or liability terms You offer. You may include additional |
||||
disclaimers of warranty and limitations of liability specific to any |
||||
jurisdiction. |
||||
|
||||
4. Inability to Comply Due to Statute or Regulation |
||||
|
||||
If it is impossible for You to comply with any of the terms of this License |
||||
with respect to some or all of the Covered Software due to statute, |
||||
judicial order, or regulation then You must: (a) comply with the terms of |
||||
this License to the maximum extent possible; and (b) describe the |
||||
limitations and the code they affect. Such description must be placed in a |
||||
text file included with all distributions of the Covered Software under |
||||
this License. Except to the extent prohibited by statute or regulation, |
||||
such description must be sufficiently detailed for a recipient of ordinary |
||||
skill to be able to understand it. |
||||
|
||||
5. Termination |
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You |
||||
fail to comply with any of its terms. However, if You become compliant, |
||||
then the rights granted under this License from a particular Contributor |
||||
are reinstated (a) provisionally, unless and until such Contributor |
||||
explicitly and finally terminates Your grants, and (b) on an ongoing |
||||
basis, if such Contributor fails to notify You of the non-compliance by |
||||
some reasonable means prior to 60 days after You have come back into |
||||
compliance. Moreover, Your grants from a particular Contributor are |
||||
reinstated on an ongoing basis if such Contributor notifies You of the |
||||
non-compliance by some reasonable means, this is the first time You have |
||||
received notice of non-compliance with this License from such |
||||
Contributor, and You become compliant prior to 30 days after Your receipt |
||||
of the notice. |
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent |
||||
infringement claim (excluding declaratory judgment actions, |
||||
counter-claims, and cross-claims) alleging that a Contributor Version |
||||
directly or indirectly infringes any patent, then the rights granted to |
||||
You by any and all Contributors for the Covered Software under Section |
||||
2.1 of this License shall terminate. |
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user |
||||
license agreements (excluding distributors and resellers) which have been |
||||
validly granted by You or Your distributors under this License prior to |
||||
termination shall survive termination. |
||||
|
||||
6. Disclaimer of Warranty |
||||
|
||||
Covered Software is provided under this License on an "as is" basis, |
||||
without warranty of any kind, either expressed, implied, or statutory, |
||||
including, without limitation, warranties that the Covered Software is free |
||||
of defects, merchantable, fit for a particular purpose or non-infringing. |
||||
The entire risk as to the quality and performance of the Covered Software |
||||
is with You. Should any Covered Software prove defective in any respect, |
||||
You (not any Contributor) assume the cost of any necessary servicing, |
||||
repair, or correction. This disclaimer of warranty constitutes an essential |
||||
part of this License. No use of any Covered Software is authorized under |
||||
this License except under this disclaimer. |
||||
|
||||
7. Limitation of Liability |
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including |
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who |
||||
distributes Covered Software as permitted above, be liable to You for any |
||||
direct, indirect, special, incidental, or consequential damages of any |
||||
character including, without limitation, damages for lost profits, loss of |
||||
goodwill, work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses, even if such party shall have been |
||||
informed of the possibility of such damages. This limitation of liability |
||||
shall not apply to liability for death or personal injury resulting from |
||||
such party's negligence to the extent applicable law prohibits such |
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of |
||||
incidental or consequential damages, so this exclusion and limitation may |
||||
not apply to You. |
||||
|
||||
8. Litigation |
||||
|
||||
Any litigation relating to this License may be brought only in the courts |
||||
of a jurisdiction where the defendant maintains its principal place of |
||||
business and such litigation shall be governed by laws of that |
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing |
||||
in this Section shall prevent a party's ability to bring cross-claims or |
||||
counter-claims. |
||||
|
||||
9. Miscellaneous |
||||
|
||||
This License represents the complete agreement concerning the subject |
||||
matter hereof. If any provision of this License is held to be |
||||
unenforceable, such provision shall be reformed only to the extent |
||||
necessary to make it enforceable. Any law or regulation which provides that |
||||
the language of a contract shall be construed against the drafter shall not |
||||
be used to construe this License against a Contributor. |
||||
|
||||
|
||||
10. Versions of the License |
||||
|
||||
10.1. New Versions |
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section |
||||
10.3, no one other than the license steward has the right to modify or |
||||
publish new versions of this License. Each version will be given a |
||||
distinguishing version number. |
||||
|
||||
10.2. Effect of New Versions |
||||
|
||||
You may distribute the Covered Software under the terms of the version |
||||
of the License under which You originally received the Covered Software, |
||||
or under the terms of any subsequent version published by the license |
||||
steward. |
||||
|
||||
10.3. Modified Versions |
||||
|
||||
If you create software not governed by this License, and you want to |
||||
create a new license for such software, you may create and use a |
||||
modified version of this License if you rename the license and remove |
||||
any references to the name of the license steward (except to note that |
||||
such modified license differs from this License). |
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary |
||||
Licenses If You choose to distribute Source Code Form that is |
||||
Incompatible With Secondary Licenses under the terms of this version of |
||||
the License, the notice described in Exhibit B of this License must be |
||||
attached. |
||||
|
||||
Exhibit A - Source Code Form License Notice |
||||
|
||||
This Source Code Form is subject to the |
||||
terms of the Mozilla Public License, v. |
||||
2.0. If a copy of the MPL was not |
||||
distributed with this file, You can |
||||
obtain one at |
||||
http://mozilla.org/MPL/2.0/. |
||||
|
||||
If it is not possible or desirable to put the notice in a particular file, |
||||
then You may include the notice in a location (such as a LICENSE file in a |
||||
relevant directory) where a recipient would be likely to look for such a |
||||
notice. |
||||
|
||||
You may add additional accurate notices of copyright ownership. |
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice |
||||
|
||||
This Source Code Form is "Incompatible |
||||
With Secondary Licenses", as defined by |
||||
the Mozilla Public License, v. 2.0. |
@ -0,0 +1,25 @@ |
||||
golang-lru |
||||
========== |
||||
|
||||
This provides the `lru` package which implements a fixed-size |
||||
thread safe LRU cache. It is based on the cache in Groupcache. |
||||
|
||||
Documentation |
||||
============= |
||||
|
||||
Full docs are available on [Godoc](http://godoc.org/github.com/hashicorp/golang-lru) |
||||
|
||||
Example |
||||
======= |
||||
|
||||
Using the LRU is very simple: |
||||
|
||||
```go |
||||
l, _ := New(128) |
||||
for i := 0; i < 256; i++ { |
||||
l.Add(i, nil) |
||||
} |
||||
if l.Len() != 128 { |
||||
panic(fmt.Sprintf("bad len: %v", l.Len())) |
||||
} |
||||
``` |
@ -0,0 +1,175 @@ |
||||
// This package provides a simple LRU cache. It is based on the
|
||||
// LRU implementation in groupcache:
|
||||
// https://github.com/golang/groupcache/tree/master/lru
|
||||
package lru |
||||
|
||||
import ( |
||||
"container/list" |
||||
"errors" |
||||
"sync" |
||||
) |
||||
|
||||
// Cache is a thread-safe fixed size LRU cache.
|
||||
type Cache struct { |
||||
size int |
||||
evictList *list.List |
||||
items map[interface{}]*list.Element |
||||
lock sync.RWMutex |
||||
onEvicted func(key interface{}, value interface{}) |
||||
} |
||||
|
||||
// entry is used to hold a value in the evictList
|
||||
type entry struct { |
||||
key interface{} |
||||
value interface{} |
||||
} |
||||
|
||||
// New creates an LRU of the given size
|
||||
func New(size int) (*Cache, error) { |
||||
return NewWithEvict(size, nil) |
||||
} |
||||
|
||||
func NewWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { |
||||
if size <= 0 { |
||||
return nil, errors.New("Must provide a positive size") |
||||
} |
||||
c := &Cache{ |
||||
size: size, |
||||
evictList: list.New(), |
||||
items: make(map[interface{}]*list.Element, size), |
||||
onEvicted: onEvicted, |
||||
} |
||||
return c, nil |
||||
} |
||||
|
||||
// Purge is used to completely clear the cache
|
||||
func (c *Cache) Purge() { |
||||
c.lock.Lock() |
||||
defer c.lock.Unlock() |
||||
|
||||
if c.onEvicted != nil { |
||||
for k, v := range c.items { |
||||
c.onEvicted(k, v.Value.(*entry).value) |
||||
} |
||||
} |
||||
|
||||
c.evictList = list.New() |
||||
c.items = make(map[interface{}]*list.Element, c.size) |
||||
} |
||||
|
||||
// Add adds a value to the cache. Returns true if an eviction occured.
|
||||
func (c *Cache) Add(key, value interface{}) bool { |
||||
c.lock.Lock() |
||||
defer c.lock.Unlock() |
||||
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok { |
||||
c.evictList.MoveToFront(ent) |
||||
ent.Value.(*entry).value = value |
||||
return false |
||||
} |
||||
|
||||
// Add new item
|
||||
ent := &entry{key, value} |
||||
entry := c.evictList.PushFront(ent) |
||||
c.items[key] = entry |
||||
|
||||
evict := c.evictList.Len() > c.size |
||||
// Verify size not exceeded
|
||||
if evict { |
||||
c.removeOldest() |
||||
} |
||||
return evict |
||||
} |
||||
|
||||
// Get looks up a key's value from the cache.
|
||||
func (c *Cache) Get(key interface{}) (value interface{}, ok bool) { |
||||
c.lock.Lock() |
||||
defer c.lock.Unlock() |
||||
|
||||
if ent, ok := c.items[key]; ok { |
||||
c.evictList.MoveToFront(ent) |
||||
return ent.Value.(*entry).value, true |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Check if a key is in the cache, without updating the recent-ness or deleting it for being stale.
|
||||
func (c *Cache) Contains(key interface{}) (ok bool) { |
||||
c.lock.RLock() |
||||
defer c.lock.RUnlock() |
||||
|
||||
_, ok = c.items[key] |
||||
return ok |
||||
} |
||||
|
||||
// Returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
||||
// (If you find yourself using this a lot, you might be using the wrong sort of data structure, but there are some use cases where it's handy.)
|
||||
func (c *Cache) Peek(key interface{}) (value interface{}, ok bool) { |
||||
c.lock.RLock() |
||||
defer c.lock.RUnlock() |
||||
|
||||
if ent, ok := c.items[key]; ok { |
||||
return ent.Value.(*entry).value, true |
||||
} |
||||
return nil, ok |
||||
} |
||||
|
||||
// Remove removes the provided key from the cache.
|
||||
func (c *Cache) Remove(key interface{}) { |
||||
c.lock.Lock() |
||||
defer c.lock.Unlock() |
||||
|
||||
if ent, ok := c.items[key]; ok { |
||||
c.removeElement(ent) |
||||
} |
||||
} |
||||
|
||||
// RemoveOldest removes the oldest item from the cache.
|
||||
func (c *Cache) RemoveOldest() { |
||||
c.lock.Lock() |
||||
defer c.lock.Unlock() |
||||
c.removeOldest() |
||||
} |
||||
|
||||
// Keys returns a slice of the keys in the cache, from oldest to newest.
|
||||
func (c *Cache) Keys() []interface{} { |
||||
c.lock.RLock() |
||||
defer c.lock.RUnlock() |
||||
|
||||
keys := make([]interface{}, len(c.items)) |
||||
ent := c.evictList.Back() |
||||
i := 0 |
||||
for ent != nil { |
||||
keys[i] = ent.Value.(*entry).key |
||||
ent = ent.Prev() |
||||
i++ |
||||
} |
||||
|
||||
return keys |
||||
} |
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int { |
||||
c.lock.RLock() |
||||
defer c.lock.RUnlock() |
||||
return c.evictList.Len() |
||||
} |
||||
|
||||
// removeOldest removes the oldest item from the cache.
|
||||
func (c *Cache) removeOldest() { |
||||
ent := c.evictList.Back() |
||||
if ent != nil { |
||||
c.removeElement(ent) |
||||
} |
||||
} |
||||
|
||||
// removeElement is used to remove a given list element from the cache
|
||||
func (c *Cache) removeElement(e *list.Element) { |
||||
c.evictList.Remove(e) |
||||
kv := e.Value.(*entry) |
||||
delete(c.items, kv.key) |
||||
if c.onEvicted != nil { |
||||
c.onEvicted(kv.key, kv.value) |
||||
} |
||||
} |
@ -0,0 +1,127 @@ |
||||
package lru |
||||
|
||||
import "testing" |
||||
|
||||
func TestLRU(t *testing.T) { |
||||
evictCounter := 0 |
||||
onEvicted := func(k interface{}, v interface{}) { |
||||
if k != v { |
||||
t.Fatalf("Evict values not equal (%v!=%v)", k, v) |
||||
} |
||||
evictCounter += 1 |
||||
} |
||||
l, err := NewWithEvict(128, onEvicted) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
for i := 0; i < 256; i++ { |
||||
l.Add(i, i) |
||||
} |
||||
if l.Len() != 128 { |
||||
t.Fatalf("bad len: %v", l.Len()) |
||||
} |
||||
|
||||
if evictCounter != 128 { |
||||
t.Fatalf("bad evict count: %v", evictCounter) |
||||
} |
||||
|
||||
for i, k := range l.Keys() { |
||||
if v, ok := l.Get(k); !ok || v != k || v != i+128 { |
||||
t.Fatalf("bad key: %v", k) |
||||
} |
||||
} |
||||
for i := 0; i < 128; i++ { |
||||
_, ok := l.Get(i) |
||||
if ok { |
||||
t.Fatalf("should be evicted") |
||||
} |
||||
} |
||||
for i := 128; i < 256; i++ { |
||||
_, ok := l.Get(i) |
||||
if !ok { |
||||
t.Fatalf("should not be evicted") |
||||
} |
||||
} |
||||
for i := 128; i < 192; i++ { |
||||
l.Remove(i) |
||||
_, ok := l.Get(i) |
||||
if ok { |
||||
t.Fatalf("should be deleted") |
||||
} |
||||
} |
||||
|
||||
l.Get(192) // expect 192 to be last key in l.Keys()
|
||||
|
||||
for i, k := range l.Keys() { |
||||
if (i < 63 && k != i+193) || (i == 63 && k != 192) { |
||||
t.Fatalf("out of order key: %v", k) |
||||
} |
||||
} |
||||
|
||||
l.Purge() |
||||
if l.Len() != 0 { |
||||
t.Fatalf("bad len: %v", l.Len()) |
||||
} |
||||
if _, ok := l.Get(200); ok { |
||||
t.Fatalf("should contain nothing") |
||||
} |
||||
} |
||||
|
||||
// test that Add returns true/false if an eviction occured
|
||||
func TestLRUAdd(t *testing.T) { |
||||
evictCounter := 0 |
||||
onEvicted := func(k interface{}, v interface{}) { |
||||
evictCounter += 1 |
||||
} |
||||
|
||||
l, err := NewWithEvict(1, onEvicted) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
if l.Add(1, 1) == true || evictCounter != 0 { |
||||
t.Errorf("should not have an eviction") |
||||
} |
||||
if l.Add(2, 2) == false || evictCounter != 1 { |
||||
t.Errorf("should have an eviction") |
||||
} |
||||
} |
||||
|
||||
// test that Contains doesn't update recent-ness
|
||||
func TestLRUContains(t *testing.T) { |
||||
l, err := New(2) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
l.Add(1, 1) |
||||
l.Add(2, 2) |
||||
if !l.Contains(1) { |
||||
t.Errorf("1 should be contained") |
||||
} |
||||
|
||||
l.Add(3, 3) |
||||
if l.Contains(1) { |
||||
t.Errorf("Contains should not have updated recent-ness of 1") |
||||
} |
||||
} |
||||
|
||||
// test that Peek doesn't update recent-ness
|
||||
func TestLRUPeek(t *testing.T) { |
||||
l, err := New(2) |
||||
if err != nil { |
||||
t.Fatalf("err: %v", err) |
||||
} |
||||
|
||||
l.Add(1, 1) |
||||
l.Add(2, 2) |
||||
if v, ok := l.Peek(1); !ok || v != 1 { |
||||
t.Errorf("1 should be set to 1: %v, %v", v, ok) |
||||
} |
||||
|
||||
l.Add(3, 3) |
||||
if l.Contains(1) { |
||||
t.Errorf("should not have updated recent-ness of 1") |
||||
} |
||||
} |
@ -1,5 +0,0 @@ |
||||
# Setup a Global .gitignore for OS and editor generated files: |
||||
# https://help.github.com/articles/ignoring-files |
||||
# git config --global core.excludesfile ~/.gitignore_global |
||||
|
||||
.vagrant |
@ -1,28 +0,0 @@ |
||||
# Names should be added to this file as |
||||
# Name or Organization <email address> |
||||
# The email address is not required for organizations. |
||||
|
||||
# You can update this list using the following command: |
||||
# |
||||
# $ git shortlog -se | awk '{print $2 " " $3 " " $4}' |
||||
|
||||
# Please keep the list sorted. |
||||
|
||||
Adrien Bustany <adrien@bustany.org> |
||||
Caleb Spare <cespare@gmail.com> |
||||
Case Nelson <case@teammating.com> |
||||
Chris Howey <howeyc@gmail.com> <chris@howey.me> |
||||
Christoffer Buchholz <christoffer.buchholz@gmail.com> |
||||
Dave Cheney <dave@cheney.net> |
||||
Francisco Souza <f@souza.cc> |
||||
John C Barstow |
||||
Kelvin Fo <vmirage@gmail.com> |
||||
Nathan Youngman <git@nathany.com> |
||||
Paul Hammond <paul@paulhammond.org> |
||||
Pursuit92 <JoshChase@techpursuit.net> |
||||
Rob Figueiredo <robfig@gmail.com> |
||||
Travis Cline <travis.cline@gmail.com> |
||||
Tudor Golubenco <tudor.g@gmail.com> |
||||
bronze1man <bronze1man@gmail.com> |
||||
debrando <denis.brandolini@gmail.com> |
||||
henrikedwards <henrik.edwards@gmail.com> |
@ -1,160 +0,0 @@ |
||||
# Changelog |
||||
|
||||
## v0.9.0 / 2014-01-17 |
||||
|
||||
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany) |
||||
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare) |
||||
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library. |
||||
|
||||
## v0.8.12 / 2013-11-13 |
||||
|
||||
* [API] Remove FD_SET and friends from Linux adapter |
||||
|
||||
## v0.8.11 / 2013-11-02 |
||||
|
||||
* [Doc] Add Changelog [#72][] (thanks @nathany) |
||||
* [Doc] Spotlight and double modify events on OS X [#62][] (reported by @paulhammond) |
||||
|
||||
## v0.8.10 / 2013-10-19 |
||||
|
||||
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott) |
||||
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer) |
||||
* [Doc] specify OS-specific limits in README (thanks @debrando) |
||||
|
||||
## v0.8.9 / 2013-09-08 |
||||
|
||||
* [Doc] Contributing (thanks @nathany) |
||||
* [Doc] update package path in example code [#63][] (thanks @paulhammond) |
||||
* [Doc] GoCI badge in README (Linux only) [#60][] |
||||
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany) |
||||
|
||||
## v0.8.8 / 2013-06-17 |
||||
|
||||
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie) |
||||
|
||||
## v0.8.7 / 2013-06-03 |
||||
|
||||
* [API] Make syscall flags internal |
||||
* [Fix] inotify: ignore event changes |
||||
* [Fix] race in symlink test [#45][] (reported by @srid) |
||||
* [Fix] tests on Windows |
||||
* lower case error messages |
||||
|
||||
## v0.8.6 / 2013-05-23 |
||||
|
||||
* kqueue: Use EVT_ONLY flag on Darwin |
||||
* [Doc] Update README with full example |
||||
|
||||
## v0.8.5 / 2013-05-09 |
||||
|
||||
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg) |
||||
|
||||
## v0.8.4 / 2013-04-07 |
||||
|
||||
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz) |
||||
|
||||
## v0.8.3 / 2013-03-13 |
||||
|
||||
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin) |
||||
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin) |
||||
|
||||
## v0.8.2 / 2013-02-07 |
||||
|
||||
* [Doc] add Authors |
||||
* [Fix] fix data races for map access [#29][] (thanks @fsouza) |
||||
|
||||
## v0.8.1 / 2013-01-09 |
||||
|
||||
* [Fix] Windows path separators |
||||
* [Doc] BSD License |
||||
|
||||
## v0.8.0 / 2012-11-09 |
||||
|
||||
* kqueue: directory watching improvements (thanks @vmirage) |
||||
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto) |
||||
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr) |
||||
|
||||
## v0.7.4 / 2012-10-09 |
||||
|
||||
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji) |
||||
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig) |
||||
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig) |
||||
* [Fix] kqueue: modify after recreation of file |
||||
|
||||
## v0.7.3 / 2012-09-27 |
||||
|
||||
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage) |
||||
* [Fix] kqueue: no longer get duplicate CREATE events |
||||
|
||||
## v0.7.2 / 2012-09-01 |
||||
|
||||
* kqueue: events for created directories |
||||
|
||||
## v0.7.1 / 2012-07-14 |
||||
|
||||
* [Fix] for renaming files |
||||
|
||||
## v0.7.0 / 2012-07-02 |
||||
|
||||
* [Feature] FSNotify flags |
||||
* [Fix] inotify: Added file name back to event path |
||||
|
||||
## v0.6.0 / 2012-06-06 |
||||
|
||||
* kqueue: watch files after directory created (thanks @tmc) |
||||
|
||||
## v0.5.1 / 2012-05-22 |
||||
|
||||
* [Fix] inotify: remove all watches before Close() |
||||
|
||||
## v0.5.0 / 2012-05-03 |
||||
|
||||
* [API] kqueue: return errors during watch instead of sending over channel |
||||
* kqueue: match symlink behavior on Linux |
||||
* inotify: add `DELETE_SELF` (requested by @taralx) |
||||
* [Fix] kqueue: handle EINTR (reported by @robfig) |
||||
* [Doc] Godoc example [#1][] (thanks @davecheney) |
||||
|
||||
## v0.4.0 / 2012-03-30 |
||||
|
||||
* Go 1 released: build with go tool |
||||
* [Feature] Windows support using winfsnotify |
||||
* Windows does not have attribute change notifications |
||||
* Roll attribute notifications into IsModify |
||||
|
||||
## v0.3.0 / 2012-02-19 |
||||
|
||||
* kqueue: add files when watch directory |
||||
|
||||
## v0.2.0 / 2011-12-30 |
||||
|
||||
* update to latest Go weekly code |
||||
|
||||
## v0.1.0 / 2011-10-19 |
||||
|
||||
* kqueue: add watch on file creation to match inotify |
||||
* kqueue: create file event |
||||
* inotify: ignore `IN_IGNORED` events |
||||
* event String() |
||||
* linux: common FileEvent functions |
||||
* initial commit |
||||
|
||||
[#79]: https://github.com/howeyc/fsnotify/pull/79 |
||||
[#77]: https://github.com/howeyc/fsnotify/pull/77 |
||||
[#72]: https://github.com/howeyc/fsnotify/issues/72 |
||||
[#71]: https://github.com/howeyc/fsnotify/issues/71 |
||||
[#70]: https://github.com/howeyc/fsnotify/issues/70 |
||||
[#63]: https://github.com/howeyc/fsnotify/issues/63 |
||||
[#62]: https://github.com/howeyc/fsnotify/issues/62 |
||||
[#60]: https://github.com/howeyc/fsnotify/issues/60 |
||||
[#59]: https://github.com/howeyc/fsnotify/issues/59 |
||||
[#49]: https://github.com/howeyc/fsnotify/issues/49 |
||||
[#45]: https://github.com/howeyc/fsnotify/issues/45 |
||||
[#40]: https://github.com/howeyc/fsnotify/issues/40 |
||||
[#36]: https://github.com/howeyc/fsnotify/issues/36 |
||||
[#33]: https://github.com/howeyc/fsnotify/issues/33 |
||||
[#29]: https://github.com/howeyc/fsnotify/issues/29 |
||||
[#25]: https://github.com/howeyc/fsnotify/issues/25 |
||||
[#24]: https://github.com/howeyc/fsnotify/issues/24 |
||||
[#21]: https://github.com/howeyc/fsnotify/issues/21 |
||||
[#1]: https://github.com/howeyc/fsnotify/issues/1 |
@ -1,7 +0,0 @@ |
||||
# Contributing |
||||
|
||||
## Moving Notice |
||||
|
||||
There is a fork being actively developed with a new API in preparation for the Go Standard Library: |
||||
[github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify) |
||||
|
@ -1,28 +0,0 @@ |
||||
Copyright (c) 2012 The Go Authors. All rights reserved. |
||||
Copyright (c) 2012 fsnotify Authors. All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above |
||||
copyright notice, this list of conditions and the following disclaimer |
||||
in the documentation and/or other materials provided with the |
||||
distribution. |
||||
* Neither the name of Google Inc. nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -1,92 +0,0 @@ |
||||
# File system notifications for Go |
||||
|
||||
[![GoDoc](https://godoc.org/github.com/howeyc/fsnotify?status.png)](http://godoc.org/github.com/howeyc/fsnotify) |
||||
|
||||
Cross platform: Windows, Linux, BSD and OS X. |
||||
|
||||
## Moving Notice |
||||
|
||||
There is a fork being actively developed with a new API in preparation for the Go Standard Library: |
||||
[github.com/go-fsnotify/fsnotify](https://github.com/go-fsnotify/fsnotify) |
||||
|
||||
## Example: |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
|
||||
"github.com/howeyc/fsnotify" |
||||
) |
||||
|
||||
func main() { |
||||
watcher, err := fsnotify.NewWatcher() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
done := make(chan bool) |
||||
|
||||
// Process events |
||||
go func() { |
||||
for { |
||||
select { |
||||
case ev := <-watcher.Event: |
||||
log.Println("event:", ev) |
||||
case err := <-watcher.Error: |
||||
log.Println("error:", err) |
||||
} |
||||
} |
||||
}() |
||||
|
||||
err = watcher.Watch("testDir") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
<-done |
||||
|
||||
/* ... do stuff ... */ |
||||
watcher.Close() |
||||
} |
||||
``` |
||||
|
||||
For each event: |
||||
* Name |
||||
* IsCreate() |
||||
* IsDelete() |
||||
* IsModify() |
||||
* IsRename() |
||||
|
||||
## FAQ |
||||
|
||||
**When a file is moved to another directory is it still being watched?** |
||||
|
||||
No (it shouldn't be, unless you are watching where it was moved to). |
||||
|
||||
**When I watch a directory, are all subdirectories watched as well?** |
||||
|
||||
No, you must add watches for any directory you want to watch (a recursive watcher is in the works [#56][]). |
||||
|
||||
**Do I have to watch the Error and Event channels in a separate goroutine?** |
||||
|
||||
As of now, yes. Looking into making this single-thread friendly (see [#7][]) |
||||
|
||||
**Why am I receiving multiple events for the same file on OS X?** |
||||
|
||||
Spotlight indexing on OS X can result in multiple events (see [#62][]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#54][]). |
||||
|
||||
**How many files can be watched at once?** |
||||
|
||||
There are OS-specific limits as to how many watches can be created: |
||||
* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, |
||||
reaching this limit results in a "no space left on device" error. |
||||
* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error. |
||||
|
||||
|
||||
[#62]: https://github.com/howeyc/fsnotify/issues/62 |
||||
[#56]: https://github.com/howeyc/fsnotify/issues/56 |
||||
[#54]: https://github.com/howeyc/fsnotify/issues/54 |
||||
[#7]: https://github.com/howeyc/fsnotify/issues/7 |
||||
|
@ -1,34 +0,0 @@ |
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fsnotify_test |
||||
|
||||
import ( |
||||
"log" |
||||
|
||||
"github.com/howeyc/fsnotify" |
||||
) |
||||
|
||||
func ExampleNewWatcher() { |
||||
watcher, err := fsnotify.NewWatcher() |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
select { |
||||
case ev := <-watcher.Event: |
||||
log.Println("event:", ev) |
||||
case err := <-watcher.Error: |
||||
log.Println("error:", err) |
||||
} |
||||
} |
||||
}() |
||||
|
||||
err = watcher.Watch("/tmp/foo") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
} |
@ -1,111 +0,0 @@ |
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package fsnotify implements file system notification.
|
||||
package fsnotify |
||||
|
||||
import "fmt" |
||||
|
||||
const ( |
||||
FSN_CREATE = 1 |
||||
FSN_MODIFY = 2 |
||||
FSN_DELETE = 4 |
||||
FSN_RENAME = 8 |
||||
|
||||
FSN_ALL = FSN_MODIFY | FSN_DELETE | FSN_RENAME | FSN_CREATE |
||||
) |
||||
|
||||
// Purge events from interal chan to external chan if passes filter
|
||||
func (w *Watcher) purgeEvents() { |
||||
for ev := range w.internalEvent { |
||||
sendEvent := false |
||||
w.fsnmut.Lock() |
||||
fsnFlags := w.fsnFlags[ev.Name] |
||||
w.fsnmut.Unlock() |
||||
|
||||
if (fsnFlags&FSN_CREATE == FSN_CREATE) && ev.IsCreate() { |
||||
sendEvent = true |
||||
} |
||||
|
||||
if (fsnFlags&FSN_MODIFY == FSN_MODIFY) && ev.IsModify() { |
||||
sendEvent = true |
||||
} |
||||
|
||||
if (fsnFlags&FSN_DELETE == FSN_DELETE) && ev.IsDelete() { |
||||
sendEvent = true |
||||
} |
||||
|
||||
if (fsnFlags&FSN_RENAME == FSN_RENAME) && ev.IsRename() { |
||||
sendEvent = true |
||||
} |
||||
|
||||
if sendEvent { |
||||
w.Event <- ev |
||||
} |
||||
|
||||
// If there's no file, then no more events for user
|
||||
// BSD must keep watch for internal use (watches DELETEs to keep track
|
||||
// what files exist for create events)
|
||||
if ev.IsDelete() { |
||||
w.fsnmut.Lock() |
||||
delete(w.fsnFlags, ev.Name) |
||||
w.fsnmut.Unlock() |
||||
} |
||||
} |
||||
|
||||
close(w.Event) |
||||
} |
||||
|
||||
// Watch a given file path
|
||||
func (w *Watcher) Watch(path string) error { |
||||
return w.WatchFlags(path, FSN_ALL) |
||||
} |
||||
|
||||
// Watch a given file path for a particular set of notifications (FSN_MODIFY etc.)
|
||||
func (w *Watcher) WatchFlags(path string, flags uint32) error { |
||||
w.fsnmut.Lock() |
||||
w.fsnFlags[path] = flags |
||||
w.fsnmut.Unlock() |
||||
return w.watch(path) |
||||
} |
||||
|
||||
// Remove a watch on a file
|
||||
func (w *Watcher) RemoveWatch(path string) error { |
||||
w.fsnmut.Lock() |
||||
delete(w.fsnFlags, path) |
||||
w.fsnmut.Unlock() |
||||
return w.removeWatch(path) |
||||
} |
||||
|
||||
// String formats the event e in the form
|
||||
// "filename: DELETE|MODIFY|..."
|
||||
func (e *FileEvent) String() string { |
||||
var events string = "" |
||||
|
||||
if e.IsCreate() { |
||||
events += "|" + "CREATE" |
||||
} |
||||
|
||||
if e.IsDelete() { |
||||
events += "|" + "DELETE" |
||||
} |
||||
|
||||
if e.IsModify() { |
||||
events += "|" + "MODIFY" |
||||
} |
||||
|
||||
if e.IsRename() { |
||||
events += "|" + "RENAME" |
||||
} |
||||
|
||||
if e.IsAttrib() { |
||||
events += "|" + "ATTRIB" |
||||
} |
||||
|
||||
if len(events) > 0 { |
||||
events = events[1:] |
||||
} |
||||
|
||||
return fmt.Sprintf("%q: %s", e.Name, events) |
||||
} |
@ -1,496 +0,0 @@ |
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd openbsd netbsd darwin
|
||||
|
||||
package fsnotify |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"path/filepath" |
||||
"sync" |
||||
"syscall" |
||||
) |
||||
|
||||
const ( |
||||
// Flags (from <sys/event.h>)
|
||||
sys_NOTE_DELETE = 0x0001 /* vnode was removed */ |
||||
sys_NOTE_WRITE = 0x0002 /* data contents changed */ |
||||
sys_NOTE_EXTEND = 0x0004 /* size increased */ |
||||
sys_NOTE_ATTRIB = 0x0008 /* attributes changed */ |
||||
sys_NOTE_LINK = 0x0010 /* link count changed */ |
||||
sys_NOTE_RENAME = 0x0020 /* vnode was renamed */ |
||||
sys_NOTE_REVOKE = 0x0040 /* vnode access was revoked */ |
||||
|
||||
// Watch all events
|
||||
sys_NOTE_ALLEVENTS = sys_NOTE_DELETE | sys_NOTE_WRITE | sys_NOTE_ATTRIB | sys_NOTE_RENAME |
||||
|
||||
// Block for 100 ms on each call to kevent
|
||||
keventWaitTime = 100e6 |
||||
) |
||||
|
||||
type FileEvent struct { |
||||
mask uint32 // Mask of events
|
||||
Name string // File name (optional)
|
||||
create bool // set by fsnotify package if found new file
|
||||
} |
||||
|
||||
// IsCreate reports whether the FileEvent was triggered by a creation
|
||||
func (e *FileEvent) IsCreate() bool { return e.create } |
||||
|
||||
// IsDelete reports whether the FileEvent was triggered by a delete
|
||||
func (e *FileEvent) IsDelete() bool { return (e.mask & sys_NOTE_DELETE) == sys_NOTE_DELETE } |
||||
|
||||
// IsModify reports whether the FileEvent was triggered by a file modification
|
||||
func (e *FileEvent) IsModify() bool { |
||||
return ((e.mask&sys_NOTE_WRITE) == sys_NOTE_WRITE || (e.mask&sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB) |
||||
} |
||||
|
||||
// IsRename reports whether the FileEvent was triggered by a change name
|
||||
func (e *FileEvent) IsRename() bool { return (e.mask & sys_NOTE_RENAME) == sys_NOTE_RENAME } |
||||
|
||||
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
|
||||
func (e *FileEvent) IsAttrib() bool { |
||||
return (e.mask & sys_NOTE_ATTRIB) == sys_NOTE_ATTRIB |
||||
} |
||||
|
||||
type Watcher struct { |
||||
mu sync.Mutex // Mutex for the Watcher itself.
|
||||
kq int // File descriptor (as returned by the kqueue() syscall)
|
||||
watches map[string]int // Map of watched file descriptors (key: path)
|
||||
wmut sync.Mutex // Protects access to watches.
|
||||
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
|
||||
fsnmut sync.Mutex // Protects access to fsnFlags.
|
||||
enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue
|
||||
enmut sync.Mutex // Protects access to enFlags.
|
||||
paths map[int]string // Map of watched paths (key: watch descriptor)
|
||||
finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor)
|
||||
pmut sync.Mutex // Protects access to paths and finfo.
|
||||
fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events)
|
||||
femut sync.Mutex // Protects access to fileExists.
|
||||
externalWatches map[string]bool // Map of watches added by user of the library.
|
||||
ewmut sync.Mutex // Protects access to externalWatches.
|
||||
Error chan error // Errors are sent on this channel
|
||||
internalEvent chan *FileEvent // Events are queued on this channel
|
||||
Event chan *FileEvent // Events are returned on this channel
|
||||
done chan bool // Channel for sending a "quit message" to the reader goroutine
|
||||
isClosed bool // Set to true when Close() is first called
|
||||
} |
||||
|
||||
// NewWatcher creates and returns a new kevent instance using kqueue(2)
|
||||
func NewWatcher() (*Watcher, error) { |
||||
fd, errno := syscall.Kqueue() |
||||
if fd == -1 { |
||||
return nil, os.NewSyscallError("kqueue", errno) |
||||
} |
||||
w := &Watcher{ |
||||
kq: fd, |
||||
watches: make(map[string]int), |
||||
fsnFlags: make(map[string]uint32), |
||||
enFlags: make(map[string]uint32), |
||||
paths: make(map[int]string), |
||||
finfo: make(map[int]os.FileInfo), |
||||
fileExists: make(map[string]bool), |
||||
externalWatches: make(map[string]bool), |
||||
internalEvent: make(chan *FileEvent), |
||||
Event: make(chan *FileEvent), |
||||
Error: make(chan error), |
||||
done: make(chan bool, 1), |
||||
} |
||||
|
||||
go w.readEvents() |
||||
go w.purgeEvents() |
||||
return w, nil |
||||
} |
||||
|
||||
// Close closes a kevent watcher instance
|
||||
// It sends a message to the reader goroutine to quit and removes all watches
|
||||
// associated with the kevent instance
|
||||
func (w *Watcher) Close() error { |
||||
w.mu.Lock() |
||||
if w.isClosed { |
||||
w.mu.Unlock() |
||||
return nil |
||||
} |
||||
w.isClosed = true |
||||
w.mu.Unlock() |
||||
|
||||
// Send "quit" message to the reader goroutine
|
||||
w.done <- true |
||||
w.wmut.Lock() |
||||
ws := w.watches |
||||
w.wmut.Unlock() |
||||
for path := range ws { |
||||
w.removeWatch(path) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// AddWatch adds path to the watched file set.
|
||||
// The flags are interpreted as described in kevent(2).
|
||||
func (w *Watcher) addWatch(path string, flags uint32) error { |
||||
w.mu.Lock() |
||||
if w.isClosed { |
||||
w.mu.Unlock() |
||||
return errors.New("kevent instance already closed") |
||||
} |
||||
w.mu.Unlock() |
||||
|
||||
watchDir := false |
||||
|
||||
w.wmut.Lock() |
||||
watchfd, found := w.watches[path] |
||||
w.wmut.Unlock() |
||||
if !found { |
||||
fi, errstat := os.Lstat(path) |
||||
if errstat != nil { |
||||
return errstat |
||||
} |
||||
|
||||
// don't watch socket
|
||||
if fi.Mode()&os.ModeSocket == os.ModeSocket { |
||||
return nil |
||||
} |
||||
|
||||
// Follow Symlinks
|
||||
// Unfortunately, Linux can add bogus symlinks to watch list without
|
||||
// issue, and Windows can't do symlinks period (AFAIK). To maintain
|
||||
// consistency, we will act like everything is fine. There will simply
|
||||
// be no file events for broken symlinks.
|
||||
// Hence the returns of nil on errors.
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { |
||||
path, err := filepath.EvalSymlinks(path) |
||||
if err != nil { |
||||
return nil |
||||
} |
||||
|
||||
fi, errstat = os.Lstat(path) |
||||
if errstat != nil { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
fd, errno := syscall.Open(path, open_FLAGS, 0700) |
||||
if fd == -1 { |
||||
return errno |
||||
} |
||||
watchfd = fd |
||||
|
||||
w.wmut.Lock() |
||||
w.watches[path] = watchfd |
||||
w.wmut.Unlock() |
||||
|
||||
w.pmut.Lock() |
||||
w.paths[watchfd] = path |
||||
w.finfo[watchfd] = fi |
||||
w.pmut.Unlock() |
||||
} |
||||
// Watch the directory if it has not been watched before.
|
||||
w.pmut.Lock() |
||||
w.enmut.Lock() |
||||
if w.finfo[watchfd].IsDir() && |
||||
(flags&sys_NOTE_WRITE) == sys_NOTE_WRITE && |
||||
(!found || (w.enFlags[path]&sys_NOTE_WRITE) != sys_NOTE_WRITE) { |
||||
watchDir = true |
||||
} |
||||
w.enmut.Unlock() |
||||
w.pmut.Unlock() |
||||
|
||||
w.enmut.Lock() |
||||
w.enFlags[path] = flags |
||||
w.enmut.Unlock() |
||||
|
||||
var kbuf [1]syscall.Kevent_t |
||||
watchEntry := &kbuf[0] |
||||
watchEntry.Fflags = flags |
||||
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) |
||||
entryFlags := watchEntry.Flags |
||||
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) |
||||
if success == -1 { |
||||
return errno |
||||
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { |
||||
return errors.New("kevent add error") |
||||
} |
||||
|
||||
if watchDir { |
||||
errdir := w.watchDirectoryFiles(path) |
||||
if errdir != nil { |
||||
return errdir |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Watch adds path to the watched file set, watching all events.
|
||||
func (w *Watcher) watch(path string) error { |
||||
w.ewmut.Lock() |
||||
w.externalWatches[path] = true |
||||
w.ewmut.Unlock() |
||||
return w.addWatch(path, sys_NOTE_ALLEVENTS) |
||||
} |
||||
|
||||
// RemoveWatch removes path from the watched file set.
|
||||
func (w *Watcher) removeWatch(path string) error { |
||||
w.wmut.Lock() |
||||
watchfd, ok := w.watches[path] |
||||
w.wmut.Unlock() |
||||
if !ok { |
||||
return errors.New(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path)) |
||||
} |
||||
var kbuf [1]syscall.Kevent_t |
||||
watchEntry := &kbuf[0] |
||||
syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_DELETE) |
||||
entryFlags := watchEntry.Flags |
||||
success, errno := syscall.Kevent(w.kq, kbuf[:], nil, nil) |
||||
if success == -1 { |
||||
return os.NewSyscallError("kevent_rm_watch", errno) |
||||
} else if (entryFlags & syscall.EV_ERROR) == syscall.EV_ERROR { |
||||
return errors.New("kevent rm error") |
||||
} |
||||
syscall.Close(watchfd) |
||||
w.wmut.Lock() |
||||
delete(w.watches, path) |
||||
w.wmut.Unlock() |
||||
w.enmut.Lock() |
||||
delete(w.enFlags, path) |
||||
w.enmut.Unlock() |
||||
w.pmut.Lock() |
||||
delete(w.paths, watchfd) |
||||
fInfo := w.finfo[watchfd] |
||||
delete(w.finfo, watchfd) |
||||
w.pmut.Unlock() |
||||
|
||||
// Find all watched paths that are in this directory that are not external.
|
||||
if fInfo.IsDir() { |
||||
var pathsToRemove []string |
||||
w.pmut.Lock() |
||||
for _, wpath := range w.paths { |
||||
wdir, _ := filepath.Split(wpath) |
||||
if filepath.Clean(wdir) == filepath.Clean(path) { |
||||
w.ewmut.Lock() |
||||
if !w.externalWatches[wpath] { |
||||
pathsToRemove = append(pathsToRemove, wpath) |
||||
} |
||||
w.ewmut.Unlock() |
||||
} |
||||
} |
||||
w.pmut.Unlock() |
||||
for _, p := range pathsToRemove { |
||||
// Since these are internal, not much sense in propagating error
|
||||
// to the user, as that will just confuse them with an error about
|
||||
// a path they did not explicitly watch themselves.
|
||||
w.removeWatch(p) |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// readEvents reads from the kqueue file descriptor, converts the
|
||||
// received events into Event objects and sends them via the Event channel
|
||||
func (w *Watcher) readEvents() { |
||||
var ( |
||||
eventbuf [10]syscall.Kevent_t // Event buffer
|
||||
events []syscall.Kevent_t // Received events
|
||||
twait *syscall.Timespec // Time to block waiting for events
|
||||
n int // Number of events returned from kevent
|
||||
errno error // Syscall errno
|
||||
) |
||||
events = eventbuf[0:0] |
||||
twait = new(syscall.Timespec) |
||||
*twait = syscall.NsecToTimespec(keventWaitTime) |
||||
|
||||
for { |
||||
// See if there is a message on the "done" channel
|
||||
var done bool |
||||
select { |
||||
case done = <-w.done: |
||||
default: |
||||
} |
||||
|
||||
// If "done" message is received
|
||||
if done { |
||||
errno := syscall.Close(w.kq) |
||||
if errno != nil { |
||||
w.Error <- os.NewSyscallError("close", errno) |
||||
} |
||||
close(w.internalEvent) |
||||
close(w.Error) |
||||
return |
||||
} |
||||
|
||||
// Get new events
|
||||
if len(events) == 0 { |
||||
n, errno = syscall.Kevent(w.kq, nil, eventbuf[:], twait) |
||||
|
||||
// EINTR is okay, basically the syscall was interrupted before
|
||||
// timeout expired.
|
||||
if errno != nil && errno != syscall.EINTR { |
||||
w.Error <- os.NewSyscallError("kevent", errno) |
||||
continue |
||||
} |
||||
|
||||
// Received some events
|
||||
if n > 0 { |
||||
events = eventbuf[0:n] |
||||
} |
||||
} |
||||
|
||||
// Flush the events we received to the events channel
|
||||
for len(events) > 0 { |
||||
fileEvent := new(FileEvent) |
||||
watchEvent := &events[0] |
||||
fileEvent.mask = uint32(watchEvent.Fflags) |
||||
w.pmut.Lock() |
||||
fileEvent.Name = w.paths[int(watchEvent.Ident)] |
||||
fileInfo := w.finfo[int(watchEvent.Ident)] |
||||
w.pmut.Unlock() |
||||
if fileInfo != nil && fileInfo.IsDir() && !fileEvent.IsDelete() { |
||||
// Double check to make sure the directory exist. This can happen when
|
||||
// we do a rm -fr on a recursively watched folders and we receive a
|
||||
// modification event first but the folder has been deleted and later
|
||||
// receive the delete event
|
||||
if _, err := os.Lstat(fileEvent.Name); os.IsNotExist(err) { |
||||
// mark is as delete event
|
||||
fileEvent.mask |= sys_NOTE_DELETE |
||||
} |
||||
} |
||||
|
||||
if fileInfo != nil && fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() { |
||||
w.sendDirectoryChangeEvents(fileEvent.Name) |
||||
} else { |
||||
// Send the event on the events channel
|
||||
w.internalEvent <- fileEvent |
||||
} |
||||
|
||||
// Move to next event
|
||||
events = events[1:] |
||||
|
||||
if fileEvent.IsRename() { |
||||
w.removeWatch(fileEvent.Name) |
||||
w.femut.Lock() |
||||
delete(w.fileExists, fileEvent.Name) |
||||
w.femut.Unlock() |
||||
} |
||||
if fileEvent.IsDelete() { |
||||
w.removeWatch(fileEvent.Name) |
||||
w.femut.Lock() |
||||
delete(w.fileExists, fileEvent.Name) |
||||
w.femut.Unlock() |
||||
|
||||
// Look for a file that may have overwritten this
|
||||
// (ie mv f1 f2 will delete f2 then create f2)
|
||||
fileDir, _ := filepath.Split(fileEvent.Name) |
||||
fileDir = filepath.Clean(fileDir) |
||||
w.wmut.Lock() |
||||
_, found := w.watches[fileDir] |
||||
w.wmut.Unlock() |
||||
if found { |
||||
// make sure the directory exist before we watch for changes. When we
|
||||
// do a recursive watch and perform rm -fr, the parent directory might
|
||||
// have gone missing, ignore the missing directory and let the
|
||||
// upcoming delete event remove the watch form the parent folder
|
||||
if _, err := os.Lstat(fileDir); !os.IsNotExist(err) { |
||||
w.sendDirectoryChangeEvents(fileDir) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (w *Watcher) watchDirectoryFiles(dirPath string) error { |
||||
// Get all files
|
||||
files, err := ioutil.ReadDir(dirPath) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Search for new files
|
||||
for _, fileInfo := range files { |
||||
filePath := filepath.Join(dirPath, fileInfo.Name()) |
||||
|
||||
// Inherit fsnFlags from parent directory
|
||||
w.fsnmut.Lock() |
||||
if flags, found := w.fsnFlags[dirPath]; found { |
||||
w.fsnFlags[filePath] = flags |
||||
} else { |
||||
w.fsnFlags[filePath] = FSN_ALL |
||||
} |
||||
w.fsnmut.Unlock() |
||||
|
||||
if fileInfo.IsDir() == false { |
||||
// Watch file to mimic linux fsnotify
|
||||
e := w.addWatch(filePath, sys_NOTE_ALLEVENTS) |
||||
if e != nil { |
||||
return e |
||||
} |
||||
} else { |
||||
// If the user is currently watching directory
|
||||
// we want to preserve the flags used
|
||||
w.enmut.Lock() |
||||
currFlags, found := w.enFlags[filePath] |
||||
w.enmut.Unlock() |
||||
var newFlags uint32 = sys_NOTE_DELETE |
||||
if found { |
||||
newFlags |= currFlags |
||||
} |
||||
|
||||
// Linux gives deletes if not explicitly watching
|
||||
e := w.addWatch(filePath, newFlags) |
||||
if e != nil { |
||||
return e |
||||
} |
||||
} |
||||
w.femut.Lock() |
||||
w.fileExists[filePath] = true |
||||
w.femut.Unlock() |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// sendDirectoryEvents searches the directory for newly created files
|
||||
// and sends them over the event channel. This functionality is to have
|
||||
// the BSD version of fsnotify match linux fsnotify which provides a
|
||||
// create event for files created in a watched directory.
|
||||
func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { |
||||
// Get all files
|
||||
files, err := ioutil.ReadDir(dirPath) |
||||
if err != nil { |
||||
w.Error <- err |
||||
} |
||||
|
||||
// Search for new files
|
||||
for _, fileInfo := range files { |
||||
filePath := filepath.Join(dirPath, fileInfo.Name()) |
||||
w.femut.Lock() |
||||
_, doesExist := w.fileExists[filePath] |
||||
w.femut.Unlock() |
||||
if !doesExist { |
||||
// Inherit fsnFlags from parent directory
|
||||
w.fsnmut.Lock() |
||||
if flags, found := w.fsnFlags[dirPath]; found { |
||||
w.fsnFlags[filePath] = flags |
||||
} else { |
||||
w.fsnFlags[filePath] = FSN_ALL |
||||
} |
||||
w.fsnmut.Unlock() |
||||
|
||||
// Send create event
|
||||
fileEvent := new(FileEvent) |
||||
fileEvent.Name = filePath |
||||
fileEvent.create = true |
||||
w.internalEvent <- fileEvent |
||||
} |
||||
w.femut.Lock() |
||||
w.fileExists[filePath] = true |
||||
w.femut.Unlock() |
||||
} |
||||
w.watchDirectoryFiles(dirPath) |
||||
} |
@ -1,304 +0,0 @@ |
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package fsnotify |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"strings" |
||||
"sync" |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
const ( |
||||
// Options for inotify_init() are not exported
|
||||
// sys_IN_CLOEXEC uint32 = syscall.IN_CLOEXEC
|
||||
// sys_IN_NONBLOCK uint32 = syscall.IN_NONBLOCK
|
||||
|
||||
// Options for AddWatch
|
||||
sys_IN_DONT_FOLLOW uint32 = syscall.IN_DONT_FOLLOW |
||||
sys_IN_ONESHOT uint32 = syscall.IN_ONESHOT |
||||
sys_IN_ONLYDIR uint32 = syscall.IN_ONLYDIR |
||||
|
||||
// The "sys_IN_MASK_ADD" option is not exported, as AddWatch
|
||||
// adds it automatically, if there is already a watch for the given path
|
||||
// sys_IN_MASK_ADD uint32 = syscall.IN_MASK_ADD
|
||||
|
||||
// Events
|
||||
sys_IN_ACCESS uint32 = syscall.IN_ACCESS |
||||
sys_IN_ALL_EVENTS uint32 = syscall.IN_ALL_EVENTS |
||||
sys_IN_ATTRIB uint32 = syscall.IN_ATTRIB |
||||
sys_IN_CLOSE uint32 = syscall.IN_CLOSE |
||||
sys_IN_CLOSE_NOWRITE uint32 = syscall.IN_CLOSE_NOWRITE |
||||
sys_IN_CLOSE_WRITE uint32 = syscall.IN_CLOSE_WRITE |
||||
sys_IN_CREATE uint32 = syscall.IN_CREATE |
||||
sys_IN_DELETE uint32 = syscall.IN_DELETE |
||||
sys_IN_DELETE_SELF uint32 = syscall.IN_DELETE_SELF |
||||
sys_IN_MODIFY uint32 = syscall.IN_MODIFY |
||||
sys_IN_MOVE uint32 = syscall.IN_MOVE |
||||
sys_IN_MOVED_FROM uint32 = syscall.IN_MOVED_FROM |
||||
sys_IN_MOVED_TO uint32 = syscall.IN_MOVED_TO |
||||
sys_IN_MOVE_SELF uint32 = syscall.IN_MOVE_SELF |
||||
sys_IN_OPEN uint32 = syscall.IN_OPEN |
||||
|
||||
sys_AGNOSTIC_EVENTS = sys_IN_MOVED_TO | sys_IN_MOVED_FROM | sys_IN_CREATE | sys_IN_ATTRIB | sys_IN_MODIFY | sys_IN_MOVE_SELF | sys_IN_DELETE | sys_IN_DELETE_SELF |
||||
|
||||
// Special events
|
||||
sys_IN_ISDIR uint32 = syscall.IN_ISDIR |
||||
sys_IN_IGNORED uint32 = syscall.IN_IGNORED |
||||
sys_IN_Q_OVERFLOW uint32 = syscall.IN_Q_OVERFLOW |
||||
sys_IN_UNMOUNT uint32 = syscall.IN_UNMOUNT |
||||
) |
||||
|
||||
type FileEvent struct { |
||||
mask uint32 // Mask of events
|
||||
cookie uint32 // Unique cookie associating related events (for rename(2))
|
||||
Name string // File name (optional)
|
||||
} |
||||
|
||||
// IsCreate reports whether the FileEvent was triggered by a creation
|
||||
func (e *FileEvent) IsCreate() bool { |
||||
return (e.mask&sys_IN_CREATE) == sys_IN_CREATE || (e.mask&sys_IN_MOVED_TO) == sys_IN_MOVED_TO |
||||
} |
||||
|
||||
// IsDelete reports whether the FileEvent was triggered by a delete
|
||||
func (e *FileEvent) IsDelete() bool { |
||||
return (e.mask&sys_IN_DELETE_SELF) == sys_IN_DELETE_SELF || (e.mask&sys_IN_DELETE) == sys_IN_DELETE |
||||
} |
||||
|
||||
// IsModify reports whether the FileEvent was triggered by a file modification or attribute change
|
||||
func (e *FileEvent) IsModify() bool { |
||||
return ((e.mask&sys_IN_MODIFY) == sys_IN_MODIFY || (e.mask&sys_IN_ATTRIB) == sys_IN_ATTRIB) |
||||
} |
||||
|
||||
// IsRename reports whether the FileEvent was triggered by a change name
|
||||
func (e *FileEvent) IsRename() bool { |
||||
return ((e.mask&sys_IN_MOVE_SELF) == sys_IN_MOVE_SELF || (e.mask&sys_IN_MOVED_FROM) == sys_IN_MOVED_FROM) |
||||
} |
||||
|
||||
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
|
||||
func (e *FileEvent) IsAttrib() bool { |
||||
return (e.mask & sys_IN_ATTRIB) == sys_IN_ATTRIB |
||||
} |
||||
|
||||
type watch struct { |
||||
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||
} |
||||
|
||||
type Watcher struct { |
||||
mu sync.Mutex // Map access
|
||||
fd int // File descriptor (as returned by the inotify_init() syscall)
|
||||
watches map[string]*watch // Map of inotify watches (key: path)
|
||||
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
|
||||
fsnmut sync.Mutex // Protects access to fsnFlags.
|
||||
paths map[int]string // Map of watched paths (key: watch descriptor)
|
||||
Error chan error // Errors are sent on this channel
|
||||
internalEvent chan *FileEvent // Events are queued on this channel
|
||||
Event chan *FileEvent // Events are returned on this channel
|
||||
done chan bool // Channel for sending a "quit message" to the reader goroutine
|
||||
isClosed bool // Set to true when Close() is first called
|
||||
} |
||||
|
||||
// NewWatcher creates and returns a new inotify instance using inotify_init(2)
|
||||
func NewWatcher() (*Watcher, error) { |
||||
fd, errno := syscall.InotifyInit() |
||||
if fd == -1 { |
||||
return nil, os.NewSyscallError("inotify_init", errno) |
||||
} |
||||
w := &Watcher{ |
||||
fd: fd, |
||||
watches: make(map[string]*watch), |
||||
fsnFlags: make(map[string]uint32), |
||||
paths: make(map[int]string), |
||||
internalEvent: make(chan *FileEvent), |
||||
Event: make(chan *FileEvent), |
||||
Error: make(chan error), |
||||
done: make(chan bool, 1), |
||||
} |
||||
|
||||
go w.readEvents() |
||||
go w.purgeEvents() |
||||
return w, nil |
||||
} |
||||
|
||||
// Close closes an inotify watcher instance
|
||||
// It sends a message to the reader goroutine to quit and removes all watches
|
||||
// associated with the inotify instance
|
||||
func (w *Watcher) Close() error { |
||||
if w.isClosed { |
||||
return nil |
||||
} |
||||
w.isClosed = true |
||||
|
||||
// Remove all watches
|
||||
for path := range w.watches { |
||||
w.RemoveWatch(path) |
||||
} |
||||
|
||||
// Send "quit" message to the reader goroutine
|
||||
w.done <- true |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// AddWatch adds path to the watched file set.
|
||||
// The flags are interpreted as described in inotify_add_watch(2).
|
||||
func (w *Watcher) addWatch(path string, flags uint32) error { |
||||
if w.isClosed { |
||||
return errors.New("inotify instance already closed") |
||||
} |
||||
|
||||
w.mu.Lock() |
||||
watchEntry, found := w.watches[path] |
||||
w.mu.Unlock() |
||||
if found { |
||||
watchEntry.flags |= flags |
||||
flags |= syscall.IN_MASK_ADD |
||||
} |
||||
wd, errno := syscall.InotifyAddWatch(w.fd, path, flags) |
||||
if wd == -1 { |
||||
return errno |
||||
} |
||||
|
||||
w.mu.Lock() |
||||
w.watches[path] = &watch{wd: uint32(wd), flags: flags} |
||||
w.paths[wd] = path |
||||
w.mu.Unlock() |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Watch adds path to the watched file set, watching all events.
|
||||
func (w *Watcher) watch(path string) error { |
||||
return w.addWatch(path, sys_AGNOSTIC_EVENTS) |
||||
} |
||||
|
||||
// RemoveWatch removes path from the watched file set.
|
||||
func (w *Watcher) removeWatch(path string) error { |
||||
w.mu.Lock() |
||||
defer w.mu.Unlock() |
||||
watch, ok := w.watches[path] |
||||
if !ok { |
||||
return errors.New(fmt.Sprintf("can't remove non-existent inotify watch for: %s", path)) |
||||
} |
||||
success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) |
||||
if success == -1 { |
||||
return os.NewSyscallError("inotify_rm_watch", errno) |
||||
} |
||||
delete(w.watches, path) |
||||
return nil |
||||
} |
||||
|
||||
// readEvents reads from the inotify file descriptor, converts the
|
||||
// received events into Event objects and sends them via the Event channel
|
||||
func (w *Watcher) readEvents() { |
||||
var ( |
||||
buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
||||
n int // Number of bytes read with read()
|
||||
errno error // Syscall errno
|
||||
) |
||||
|
||||
for { |
||||
// See if there is a message on the "done" channel
|
||||
select { |
||||
case <-w.done: |
||||
syscall.Close(w.fd) |
||||
close(w.internalEvent) |
||||
close(w.Error) |
||||
return |
||||
default: |
||||
} |
||||
|
||||
n, errno = syscall.Read(w.fd, buf[:]) |
||||
|
||||
// If EOF is received
|
||||
if n == 0 { |
||||
syscall.Close(w.fd) |
||||
close(w.internalEvent) |
||||
close(w.Error) |
||||
return |
||||
} |
||||
|
||||
if n < 0 { |
||||
w.Error <- os.NewSyscallError("read", errno) |
||||
continue |
||||
} |
||||
if n < syscall.SizeofInotifyEvent { |
||||
w.Error <- errors.New("inotify: short read in readEvents()") |
||||
continue |
||||
} |
||||
|
||||
var offset uint32 = 0 |
||||
// We don't know how many events we just read into the buffer
|
||||
// While the offset points to at least one whole event...
|
||||
for offset <= uint32(n-syscall.SizeofInotifyEvent) { |
||||
// Point "raw" to the event in the buffer
|
||||
raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) |
||||
event := new(FileEvent) |
||||
event.mask = uint32(raw.Mask) |
||||
event.cookie = uint32(raw.Cookie) |
||||
nameLen := uint32(raw.Len) |
||||
// If the event happened to the watched directory or the watched file, the kernel
|
||||
// doesn't append the filename to the event, but we would like to always fill the
|
||||
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||
// the "paths" map.
|
||||
w.mu.Lock() |
||||
event.Name = w.paths[int(raw.Wd)] |
||||
w.mu.Unlock() |
||||
watchedName := event.Name |
||||
if nameLen > 0 { |
||||
// Point "bytes" at the first byte of the filename
|
||||
bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) |
||||
// The filename is padded with NUL bytes. TrimRight() gets rid of those.
|
||||
event.Name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") |
||||
} |
||||
|
||||
// Send the events that are not ignored on the events channel
|
||||
if !event.ignoreLinux() { |
||||
// Setup FSNotify flags (inherit from directory watch)
|
||||
w.fsnmut.Lock() |
||||
if _, fsnFound := w.fsnFlags[event.Name]; !fsnFound { |
||||
if fsnFlags, watchFound := w.fsnFlags[watchedName]; watchFound { |
||||
w.fsnFlags[event.Name] = fsnFlags |
||||
} else { |
||||
w.fsnFlags[event.Name] = FSN_ALL |
||||
} |
||||
} |
||||
w.fsnmut.Unlock() |
||||
|
||||
w.internalEvent <- event |
||||
} |
||||
|
||||
// Move to the next event in the buffer
|
||||
offset += syscall.SizeofInotifyEvent + nameLen |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Certain types of events can be "ignored" and not sent over the Event
|
||||
// channel. Such as events marked ignore by the kernel, or MODIFY events
|
||||
// against files that do not exist.
|
||||
func (e *FileEvent) ignoreLinux() bool { |
||||
// Ignore anything the inotify API says to ignore
|
||||
if e.mask&sys_IN_IGNORED == sys_IN_IGNORED { |
||||
return true |
||||
} |
||||
|
||||
// If the event is not a DELETE or RENAME, the file must exist.
|
||||
// Otherwise the event is ignored.
|
||||
// *Note*: this was put in place because it was seen that a MODIFY
|
||||
// event was sent after the DELETE. This ignores that MODIFY and
|
||||
// assumes a DELETE will come or has come if the file doesn't exist.
|
||||
if !(e.IsDelete() || e.IsRename()) { |
||||
_, statErr := os.Lstat(e.Name) |
||||
return os.IsNotExist(statErr) |
||||
} |
||||
return false |
||||
} |
@ -1,11 +0,0 @@ |
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd openbsd netbsd
|
||||
|
||||
package fsnotify |
||||
|
||||
import "syscall" |
||||
|
||||
const open_FLAGS = syscall.O_NONBLOCK | syscall.O_RDONLY |
@ -1,11 +0,0 @@ |
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin
|
||||
|
||||
package fsnotify |
||||
|
||||
import "syscall" |
||||
|
||||
const open_FLAGS = syscall.O_EVTONLY |
@ -1,74 +0,0 @@ |
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd openbsd netbsd darwin linux
|
||||
|
||||
package fsnotify |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
func TestFsnotifyFakeSymlink(t *testing.T) { |
||||
watcher := newWatcher(t) |
||||
|
||||
// Create directory to watch
|
||||
testDir := tempMkdir(t) |
||||
defer os.RemoveAll(testDir) |
||||
|
||||
var errorsReceived counter |
||||
// Receive errors on the error channel on a separate goroutine
|
||||
go func() { |
||||
for errors := range watcher.Error { |
||||
t.Logf("Received error: %s", errors) |
||||
errorsReceived.increment() |
||||
} |
||||
}() |
||||
|
||||
// Count the CREATE events received
|
||||
var createEventsReceived, otherEventsReceived counter |
||||
go func() { |
||||
for ev := range watcher.Event { |
||||
t.Logf("event received: %s", ev) |
||||
if ev.IsCreate() { |
||||
createEventsReceived.increment() |
||||
} else { |
||||
otherEventsReceived.increment() |
||||
} |
||||
} |
||||
}() |
||||
|
||||
addWatch(t, watcher, testDir) |
||||
|
||||
if err := os.Symlink(filepath.Join(testDir, "zzz"), filepath.Join(testDir, "zzznew")); err != nil { |
||||
t.Fatalf("Failed to create bogus symlink: %s", err) |
||||
} |
||||
t.Logf("Created bogus symlink") |
||||
|
||||
// We expect this event to be received almost immediately, but let's wait 500 ms to be sure
|
||||
time.Sleep(500 * time.Millisecond) |
||||
|
||||
// Should not be error, just no events for broken links (watching nothing)
|
||||
if errorsReceived.value() > 0 { |
||||
t.Fatal("fsnotify errors have been received.") |
||||
} |
||||
if otherEventsReceived.value() > 0 { |
||||
t.Fatal("fsnotify other events received on the broken link") |
||||
} |
||||
|
||||
// Except for 1 create event (for the link itself)
|
||||
if createEventsReceived.value() == 0 { |
||||
t.Fatal("fsnotify create events were not received after 500 ms") |
||||
} |
||||
if createEventsReceived.value() > 1 { |
||||
t.Fatal("fsnotify more create events received than expected") |
||||
} |
||||
|
||||
// Try closing the fsnotify instance
|
||||
t.Log("calling Close()") |
||||
watcher.Close() |
||||
} |
@ -1,598 +0,0 @@ |
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package fsnotify |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"runtime" |
||||
"sync" |
||||
"syscall" |
||||
"unsafe" |
||||
) |
||||
|
||||
const ( |
||||
// Options for AddWatch
|
||||
sys_FS_ONESHOT = 0x80000000 |
||||
sys_FS_ONLYDIR = 0x1000000 |
||||
|
||||
// Events
|
||||
sys_FS_ACCESS = 0x1 |
||||
sys_FS_ALL_EVENTS = 0xfff |
||||
sys_FS_ATTRIB = 0x4 |
||||
sys_FS_CLOSE = 0x18 |
||||
sys_FS_CREATE = 0x100 |
||||
sys_FS_DELETE = 0x200 |
||||
sys_FS_DELETE_SELF = 0x400 |
||||
sys_FS_MODIFY = 0x2 |
||||
sys_FS_MOVE = 0xc0 |
||||
sys_FS_MOVED_FROM = 0x40 |
||||
sys_FS_MOVED_TO = 0x80 |
||||
sys_FS_MOVE_SELF = 0x800 |
||||
|
||||
// Special events
|
||||
sys_FS_IGNORED = 0x8000 |
||||
sys_FS_Q_OVERFLOW = 0x4000 |
||||
) |
||||
|
||||
const ( |
||||
// TODO(nj): Use syscall.ERROR_MORE_DATA from ztypes_windows in Go 1.3+
|
||||
sys_ERROR_MORE_DATA syscall.Errno = 234 |
||||
) |
||||
|
||||
// Event is the type of the notification messages
|
||||
// received on the watcher's Event channel.
|
||||
type FileEvent struct { |
||||
mask uint32 // Mask of events
|
||||
cookie uint32 // Unique cookie associating related events (for rename)
|
||||
Name string // File name (optional)
|
||||
} |
||||
|
||||
// IsCreate reports whether the FileEvent was triggered by a creation
|
||||
func (e *FileEvent) IsCreate() bool { return (e.mask & sys_FS_CREATE) == sys_FS_CREATE } |
||||
|
||||
// IsDelete reports whether the FileEvent was triggered by a delete
|
||||
func (e *FileEvent) IsDelete() bool { |
||||
return ((e.mask&sys_FS_DELETE) == sys_FS_DELETE || (e.mask&sys_FS_DELETE_SELF) == sys_FS_DELETE_SELF) |
||||
} |
||||
|
||||
// IsModify reports whether the FileEvent was triggered by a file modification or attribute change
|
||||
func (e *FileEvent) IsModify() bool { |
||||
return ((e.mask&sys_FS_MODIFY) == sys_FS_MODIFY || (e.mask&sys_FS_ATTRIB) == sys_FS_ATTRIB) |
||||
} |
||||
|
||||
// IsRename reports whether the FileEvent was triggered by a change name
|
||||
func (e *FileEvent) IsRename() bool { |
||||
return ((e.mask&sys_FS_MOVE) == sys_FS_MOVE || (e.mask&sys_FS_MOVE_SELF) == sys_FS_MOVE_SELF || (e.mask&sys_FS_MOVED_FROM) == sys_FS_MOVED_FROM || (e.mask&sys_FS_MOVED_TO) == sys_FS_MOVED_TO) |
||||
} |
||||
|
||||
// IsAttrib reports whether the FileEvent was triggered by a change in the file metadata.
|
||||
func (e *FileEvent) IsAttrib() bool { |
||||
return (e.mask & sys_FS_ATTRIB) == sys_FS_ATTRIB |
||||
} |
||||
|
||||
const ( |
||||
opAddWatch = iota |
||||
opRemoveWatch |
||||
) |
||||
|
||||
const ( |
||||
provisional uint64 = 1 << (32 + iota) |
||||
) |
||||
|
||||
type input struct { |
||||
op int |
||||
path string |
||||
flags uint32 |
||||
reply chan error |
||||
} |
||||
|
||||
type inode struct { |
||||
handle syscall.Handle |
||||
volume uint32 |
||||
index uint64 |
||||
} |
||||
|
||||
type watch struct { |
||||
ov syscall.Overlapped |
||||
ino *inode // i-number
|
||||
path string // Directory path
|
||||
mask uint64 // Directory itself is being watched with these notify flags
|
||||
names map[string]uint64 // Map of names being watched and their notify flags
|
||||
rename string // Remembers the old name while renaming a file
|
||||
buf [4096]byte |
||||
} |
||||
|
||||
type indexMap map[uint64]*watch |
||||
type watchMap map[uint32]indexMap |
||||
|
||||
// A Watcher waits for and receives event notifications
|
||||
// for a specific set of files and directories.
|
||||
type Watcher struct { |
||||
mu sync.Mutex // Map access
|
||||
port syscall.Handle // Handle to completion port
|
||||
watches watchMap // Map of watches (key: i-number)
|
||||
fsnFlags map[string]uint32 // Map of watched files to flags used for filter
|
||||
fsnmut sync.Mutex // Protects access to fsnFlags.
|
||||
input chan *input // Inputs to the reader are sent on this channel
|
||||
internalEvent chan *FileEvent // Events are queued on this channel
|
||||
Event chan *FileEvent // Events are returned on this channel
|
||||
Error chan error // Errors are sent on this channel
|
||||
isClosed bool // Set to true when Close() is first called
|
||||
quit chan chan<- error |
||||
cookie uint32 |
||||
} |
||||
|
||||
// NewWatcher creates and returns a Watcher.
|
||||
func NewWatcher() (*Watcher, error) { |
||||
port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) |
||||
if e != nil { |
||||
return nil, os.NewSyscallError("CreateIoCompletionPort", e) |
||||
} |
||||
w := &Watcher{ |
||||
port: port, |
||||
watches: make(watchMap), |
||||
fsnFlags: make(map[string]uint32), |
||||
input: make(chan *input, 1), |
||||
Event: make(chan *FileEvent, 50), |
||||
internalEvent: make(chan *FileEvent), |
||||
Error: make(chan error), |
||||
quit: make(chan chan<- error, 1), |
||||
} |
||||
go w.readEvents() |
||||
go w.purgeEvents() |
||||
return w, nil |
||||
} |
||||
|
||||
// Close closes a Watcher.
|
||||
// It sends a message to the reader goroutine to quit and removes all watches
|
||||
// associated with the watcher.
|
||||
func (w *Watcher) Close() error { |
||||
if w.isClosed { |
||||
return nil |
||||
} |
||||
w.isClosed = true |
||||
|
||||
// Send "quit" message to the reader goroutine
|
||||
ch := make(chan error) |
||||
w.quit <- ch |
||||
if err := w.wakeupReader(); err != nil { |
||||
return err |
||||
} |
||||
return <-ch |
||||
} |
||||
|
||||
// AddWatch adds path to the watched file set.
|
||||
func (w *Watcher) AddWatch(path string, flags uint32) error { |
||||
if w.isClosed { |
||||
return errors.New("watcher already closed") |
||||
} |
||||
in := &input{ |
||||
op: opAddWatch, |
||||
path: filepath.Clean(path), |
||||
flags: flags, |
||||
reply: make(chan error), |
||||
} |
||||
w.input <- in |
||||
if err := w.wakeupReader(); err != nil { |
||||
return err |
||||
} |
||||
return <-in.reply |
||||
} |
||||
|
||||
// Watch adds path to the watched file set, watching all events.
|
||||
func (w *Watcher) watch(path string) error { |
||||
return w.AddWatch(path, sys_FS_ALL_EVENTS) |
||||
} |
||||
|
||||
// RemoveWatch removes path from the watched file set.
|
||||
func (w *Watcher) removeWatch(path string) error { |
||||
in := &input{ |
||||
op: opRemoveWatch, |
||||
path: filepath.Clean(path), |
||||
reply: make(chan error), |
||||
} |
||||
w.input <- in |
||||
if err := w.wakeupReader(); err != nil { |
||||
return err |
||||
} |
||||
return <-in.reply |
||||
} |
||||
|
||||
func (w *Watcher) wakeupReader() error { |
||||
e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) |
||||
if e != nil { |
||||
return os.NewSyscallError("PostQueuedCompletionStatus", e) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func getDir(pathname string) (dir string, err error) { |
||||
attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) |
||||
if e != nil { |
||||
return "", os.NewSyscallError("GetFileAttributes", e) |
||||
} |
||||
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { |
||||
dir = pathname |
||||
} else { |
||||
dir, _ = filepath.Split(pathname) |
||||
dir = filepath.Clean(dir) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func getIno(path string) (ino *inode, err error) { |
||||
h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), |
||||
syscall.FILE_LIST_DIRECTORY, |
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, |
||||
nil, syscall.OPEN_EXISTING, |
||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) |
||||
if e != nil { |
||||
return nil, os.NewSyscallError("CreateFile", e) |
||||
} |
||||
var fi syscall.ByHandleFileInformation |
||||
if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { |
||||
syscall.CloseHandle(h) |
||||
return nil, os.NewSyscallError("GetFileInformationByHandle", e) |
||||
} |
||||
ino = &inode{ |
||||
handle: h, |
||||
volume: fi.VolumeSerialNumber, |
||||
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), |
||||
} |
||||
return ino, nil |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (m watchMap) get(ino *inode) *watch { |
||||
if i := m[ino.volume]; i != nil { |
||||
return i[ino.index] |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (m watchMap) set(ino *inode, watch *watch) { |
||||
i := m[ino.volume] |
||||
if i == nil { |
||||
i = make(indexMap) |
||||
m[ino.volume] = i |
||||
} |
||||
i[ino.index] = watch |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) addWatch(pathname string, flags uint64) error { |
||||
dir, err := getDir(pathname) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if flags&sys_FS_ONLYDIR != 0 && pathname != dir { |
||||
return nil |
||||
} |
||||
ino, err := getIno(dir) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
w.mu.Lock() |
||||
watchEntry := w.watches.get(ino) |
||||
w.mu.Unlock() |
||||
if watchEntry == nil { |
||||
if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { |
||||
syscall.CloseHandle(ino.handle) |
||||
return os.NewSyscallError("CreateIoCompletionPort", e) |
||||
} |
||||
watchEntry = &watch{ |
||||
ino: ino, |
||||
path: dir, |
||||
names: make(map[string]uint64), |
||||
} |
||||
w.mu.Lock() |
||||
w.watches.set(ino, watchEntry) |
||||
w.mu.Unlock() |
||||
flags |= provisional |
||||
} else { |
||||
syscall.CloseHandle(ino.handle) |
||||
} |
||||
if pathname == dir { |
||||
watchEntry.mask |= flags |
||||
} else { |
||||
watchEntry.names[filepath.Base(pathname)] |= flags |
||||
} |
||||
if err = w.startRead(watchEntry); err != nil { |
||||
return err |
||||
} |
||||
if pathname == dir { |
||||
watchEntry.mask &= ^provisional |
||||
} else { |
||||
watchEntry.names[filepath.Base(pathname)] &= ^provisional |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) remWatch(pathname string) error { |
||||
dir, err := getDir(pathname) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
ino, err := getIno(dir) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
w.mu.Lock() |
||||
watch := w.watches.get(ino) |
||||
w.mu.Unlock() |
||||
if watch == nil { |
||||
return fmt.Errorf("can't remove non-existent watch for: %s", pathname) |
||||
} |
||||
if pathname == dir { |
||||
w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) |
||||
watch.mask = 0 |
||||
} else { |
||||
name := filepath.Base(pathname) |
||||
w.sendEvent(watch.path+"\\"+name, watch.names[name]&sys_FS_IGNORED) |
||||
delete(watch.names, name) |
||||
} |
||||
return w.startRead(watch) |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) deleteWatch(watch *watch) { |
||||
for name, mask := range watch.names { |
||||
if mask&provisional == 0 { |
||||
w.sendEvent(watch.path+"\\"+name, mask&sys_FS_IGNORED) |
||||
} |
||||
delete(watch.names, name) |
||||
} |
||||
if watch.mask != 0 { |
||||
if watch.mask&provisional == 0 { |
||||
w.sendEvent(watch.path, watch.mask&sys_FS_IGNORED) |
||||
} |
||||
watch.mask = 0 |
||||
} |
||||
} |
||||
|
||||
// Must run within the I/O thread.
|
||||
func (w *Watcher) startRead(watch *watch) error { |
||||
if e := syscall.CancelIo(watch.ino.handle); e != nil { |
||||
w.Error <- os.NewSyscallError("CancelIo", e) |
||||
w.deleteWatch(watch) |
||||
} |
||||
mask := toWindowsFlags(watch.mask) |
||||
for _, m := range watch.names { |
||||
mask |= toWindowsFlags(m) |
||||
} |
||||
if mask == 0 { |
||||
if e := syscall.CloseHandle(watch.ino.handle); e != nil { |
||||
w.Error <- os.NewSyscallError("CloseHandle", e) |
||||
} |
||||
w.mu.Lock() |
||||
delete(w.watches[watch.ino.volume], watch.ino.index) |
||||
w.mu.Unlock() |
||||
return nil |
||||
} |
||||
e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], |
||||
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) |
||||
if e != nil { |
||||
err := os.NewSyscallError("ReadDirectoryChanges", e) |
||||
if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { |
||||
// Watched directory was probably removed
|
||||
if w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) { |
||||
if watch.mask&sys_FS_ONESHOT != 0 { |
||||
watch.mask = 0 |
||||
} |
||||
} |
||||
err = nil |
||||
} |
||||
w.deleteWatch(watch) |
||||
w.startRead(watch) |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// readEvents reads from the I/O completion port, converts the
|
||||
// received events into Event objects and sends them via the Event channel.
|
||||
// Entry point to the I/O thread.
|
||||
func (w *Watcher) readEvents() { |
||||
var ( |
||||
n, key uint32 |
||||
ov *syscall.Overlapped |
||||
) |
||||
runtime.LockOSThread() |
||||
|
||||
for { |
||||
e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) |
||||
watch := (*watch)(unsafe.Pointer(ov)) |
||||
|
||||
if watch == nil { |
||||
select { |
||||
case ch := <-w.quit: |
||||
w.mu.Lock() |
||||
var indexes []indexMap |
||||
for _, index := range w.watches { |
||||
indexes = append(indexes, index) |
||||
} |
||||
w.mu.Unlock() |
||||
for _, index := range indexes { |
||||
for _, watch := range index { |
||||
w.deleteWatch(watch) |
||||
w.startRead(watch) |
||||
} |
||||
} |
||||
var err error |
||||
if e := syscall.CloseHandle(w.port); e != nil { |
||||
err = os.NewSyscallError("CloseHandle", e) |
||||
} |
||||
close(w.internalEvent) |
||||
close(w.Error) |
||||
ch <- err |
||||
return |
||||
case in := <-w.input: |
||||
switch in.op { |
||||
case opAddWatch: |
||||
in.reply <- w.addWatch(in.path, uint64(in.flags)) |
||||
case opRemoveWatch: |
||||
in.reply <- w.remWatch(in.path) |
||||
} |
||||
default: |
||||
} |
||||
continue |
||||
} |
||||
|
||||
switch e { |
||||
case sys_ERROR_MORE_DATA: |
||||
if watch == nil { |
||||
w.Error <- errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer") |
||||
} else { |
||||
// The i/o succeeded but the buffer is full.
|
||||
// In theory we should be building up a full packet.
|
||||
// In practice we can get away with just carrying on.
|
||||
n = uint32(unsafe.Sizeof(watch.buf)) |
||||
} |
||||
case syscall.ERROR_ACCESS_DENIED: |
||||
// Watched directory was probably removed
|
||||
w.sendEvent(watch.path, watch.mask&sys_FS_DELETE_SELF) |
||||
w.deleteWatch(watch) |
||||
w.startRead(watch) |
||||
continue |
||||
case syscall.ERROR_OPERATION_ABORTED: |
||||
// CancelIo was called on this handle
|
||||
continue |
||||
default: |
||||
w.Error <- os.NewSyscallError("GetQueuedCompletionPort", e) |
||||
continue |
||||
case nil: |
||||
} |
||||
|
||||
var offset uint32 |
||||
for { |
||||
if n == 0 { |
||||
w.internalEvent <- &FileEvent{mask: sys_FS_Q_OVERFLOW} |
||||
w.Error <- errors.New("short read in readEvents()") |
||||
break |
||||
} |
||||
|
||||
// Point "raw" to the event in the buffer
|
||||
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) |
||||
buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) |
||||
name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) |
||||
fullname := watch.path + "\\" + name |
||||
|
||||
var mask uint64 |
||||
switch raw.Action { |
||||
case syscall.FILE_ACTION_REMOVED: |
||||
mask = sys_FS_DELETE_SELF |
||||
case syscall.FILE_ACTION_MODIFIED: |
||||
mask = sys_FS_MODIFY |
||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
||||
watch.rename = name |
||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
||||
if watch.names[watch.rename] != 0 { |
||||
watch.names[name] |= watch.names[watch.rename] |
||||
delete(watch.names, watch.rename) |
||||
mask = sys_FS_MOVE_SELF |
||||
} |
||||
} |
||||
|
||||
sendNameEvent := func() { |
||||
if w.sendEvent(fullname, watch.names[name]&mask) { |
||||
if watch.names[name]&sys_FS_ONESHOT != 0 { |
||||
delete(watch.names, name) |
||||
} |
||||
} |
||||
} |
||||
if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { |
||||
sendNameEvent() |
||||
} |
||||
if raw.Action == syscall.FILE_ACTION_REMOVED { |
||||
w.sendEvent(fullname, watch.names[name]&sys_FS_IGNORED) |
||||
delete(watch.names, name) |
||||
} |
||||
if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { |
||||
if watch.mask&sys_FS_ONESHOT != 0 { |
||||
watch.mask = 0 |
||||
} |
||||
} |
||||
if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { |
||||
fullname = watch.path + "\\" + watch.rename |
||||
sendNameEvent() |
||||
} |
||||
|
||||
// Move to the next event in the buffer
|
||||
if raw.NextEntryOffset == 0 { |
||||
break |
||||
} |
||||
offset += raw.NextEntryOffset |
||||
|
||||
// Error!
|
||||
if offset >= n { |
||||
w.Error <- errors.New("Windows system assumed buffer larger than it is, events have likely been missed.") |
||||
break |
||||
} |
||||
} |
||||
|
||||
if err := w.startRead(watch); err != nil { |
||||
w.Error <- err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (w *Watcher) sendEvent(name string, mask uint64) bool { |
||||
if mask == 0 { |
||||
return false |
||||
} |
||||
event := &FileEvent{mask: uint32(mask), Name: name} |
||||
if mask&sys_FS_MOVE != 0 { |
||||
if mask&sys_FS_MOVED_FROM != 0 { |
||||
w.cookie++ |
||||
} |
||||
event.cookie = w.cookie |
||||
} |
||||
select { |
||||
case ch := <-w.quit: |
||||
w.quit <- ch |
||||
case w.Event <- event: |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func toWindowsFlags(mask uint64) uint32 { |
||||
var m uint32 |
||||
if mask&sys_FS_ACCESS != 0 { |
||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS |
||||
} |
||||
if mask&sys_FS_MODIFY != 0 { |
||||
m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE |
||||
} |
||||
if mask&sys_FS_ATTRIB != 0 { |
||||
m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES |
||||
} |
||||
if mask&(sys_FS_MOVE|sys_FS_CREATE|sys_FS_DELETE) != 0 { |
||||
m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME |
||||
} |
||||
return m |
||||
} |
||||
|
||||
func toFSnotifyFlags(action uint32) uint64 { |
||||
switch action { |
||||
case syscall.FILE_ACTION_ADDED: |
||||
return sys_FS_CREATE |
||||
case syscall.FILE_ACTION_REMOVED: |
||||
return sys_FS_DELETE |
||||
case syscall.FILE_ACTION_MODIFIED: |
||||
return sys_FS_MODIFY |
||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME: |
||||
return sys_FS_MOVED_FROM |
||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME: |
||||
return sys_FS_MOVED_TO |
||||
} |
||||
return 0 |
||||
} |
@ -0,0 +1,9 @@ |
||||
language: go |
||||
go: |
||||
- tip |
||||
before_install: |
||||
- go get github.com/axw/gocov/gocov |
||||
- go get github.com/mattn/goveralls |
||||
- go get golang.org/x/tools/cmd/cover |
||||
script: |
||||
- $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL |
@ -0,0 +1,25 @@ |
||||
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) |
||||
|
||||
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 |
@ -0,0 +1,404 @@ |
||||
package runewidth |
||||
|
||||
var EastAsianWidth = IsEastAsian() |
||||
var DefaultCondition = &Condition{EastAsianWidth} |
||||
|
||||
type interval struct { |
||||
first rune |
||||
last rune |
||||
} |
||||
|
||||
var combining = []interval{ |
||||
{0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489}, |
||||
{0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, |
||||
{0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603}, |
||||
{0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670}, |
||||
{0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED}, |
||||
{0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A}, |
||||
{0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902}, |
||||
{0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D}, |
||||
{0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981}, |
||||
{0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD}, |
||||
{0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, |
||||
{0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, |
||||
{0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC}, |
||||
{0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD}, |
||||
{0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, |
||||
{0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D}, |
||||
{0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, |
||||
{0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, |
||||
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC}, |
||||
{0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, |
||||
{0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D}, |
||||
{0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, |
||||
{0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, |
||||
{0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC}, |
||||
{0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, |
||||
{0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, |
||||
{0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97}, |
||||
{0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, |
||||
{0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039}, |
||||
{0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F}, |
||||
{0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753}, |
||||
{0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, |
||||
{0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, |
||||
{0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922}, |
||||
{0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B}, |
||||
{0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, |
||||
{0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, |
||||
{0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF}, |
||||
{0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063}, |
||||
{0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F}, |
||||
{0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B}, |
||||
{0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, |
||||
{0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, |
||||
{0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, |
||||
{0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169}, |
||||
{0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, |
||||
{0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, |
||||
{0xE0100, 0xE01EF}, |
||||
} |
||||
|
||||
type ctype int |
||||
|
||||
const ( |
||||
narrow ctype = iota |
||||
ambiguous |
||||
wide |
||||
halfwidth |
||||
fullwidth |
||||
neutral |
||||
) |
||||
|
||||
type intervalType struct { |
||||
first rune |
||||
last rune |
||||
ctype ctype |
||||
} |
||||
|
||||
var ctypes = []intervalType{ |
||||
{0x0020, 0x007E, narrow}, |
||||
{0x00A1, 0x00A1, ambiguous}, |
||||
{0x00A2, 0x00A3, narrow}, |
||||
{0x00A4, 0x00A4, ambiguous}, |
||||
{0x00A5, 0x00A6, narrow}, |
||||
{0x00A7, 0x00A8, ambiguous}, |
||||
{0x00AA, 0x00AA, ambiguous}, |
||||
{0x00AC, 0x00AC, narrow}, |
||||
{0x00AD, 0x00AE, ambiguous}, |
||||
{0x00AF, 0x00AF, narrow}, |
||||
{0x00B0, 0x00B4, ambiguous}, |
||||
{0x00B6, 0x00BA, ambiguous}, |
||||
{0x00BC, 0x00BF, ambiguous}, |
||||
{0x00C6, 0x00C6, ambiguous}, |
||||
{0x00D0, 0x00D0, ambiguous}, |
||||
{0x00D7, 0x00D8, ambiguous}, |
||||
{0x00DE, 0x00E1, ambiguous}, |
||||
{0x00E6, 0x00E6, ambiguous}, |
||||
{0x00E8, 0x00EA, ambiguous}, |
||||
{0x00EC, 0x00ED, ambiguous}, |
||||
{0x00F0, 0x00F0, ambiguous}, |
||||
{0x00F2, 0x00F3, ambiguous}, |
||||
{0x00F7, 0x00FA, ambiguous}, |
||||
{0x00FC, 0x00FC, ambiguous}, |
||||
{0x00FE, 0x00FE, ambiguous}, |
||||
{0x0101, 0x0101, ambiguous}, |
||||
{0x0111, 0x0111, ambiguous}, |
||||
{0x0113, 0x0113, ambiguous}, |
||||
{0x011B, 0x011B, ambiguous}, |
||||
{0x0126, 0x0127, ambiguous}, |
||||
{0x012B, 0x012B, ambiguous}, |
||||
{0x0131, 0x0133, ambiguous}, |
||||
{0x0138, 0x0138, ambiguous}, |
||||
{0x013F, 0x0142, ambiguous}, |
||||
{0x0144, 0x0144, ambiguous}, |
||||
{0x0148, 0x014B, ambiguous}, |
||||
{0x014D, 0x014D, ambiguous}, |
||||
{0x0152, 0x0153, ambiguous}, |
||||
{0x0166, 0x0167, ambiguous}, |
||||
{0x016B, 0x016B, ambiguous}, |
||||
{0x01CE, 0x01CE, ambiguous}, |
||||
{0x01D0, 0x01D0, ambiguous}, |
||||
{0x01D2, 0x01D2, ambiguous}, |
||||
{0x01D4, 0x01D4, ambiguous}, |
||||
{0x01D6, 0x01D6, ambiguous}, |
||||
{0x01D8, 0x01D8, ambiguous}, |
||||
{0x01DA, 0x01DA, ambiguous}, |
||||
{0x01DC, 0x01DC, ambiguous}, |
||||
{0x0251, 0x0251, ambiguous}, |
||||
{0x0261, 0x0261, ambiguous}, |
||||
{0x02C4, 0x02C4, ambiguous}, |
||||
{0x02C7, 0x02C7, ambiguous}, |
||||
{0x02C9, 0x02CB, ambiguous}, |
||||
{0x02CD, 0x02CD, ambiguous}, |
||||
{0x02D0, 0x02D0, ambiguous}, |
||||
{0x02D8, 0x02DB, ambiguous}, |
||||
{0x02DD, 0x02DD, ambiguous}, |
||||
{0x02DF, 0x02DF, ambiguous}, |
||||
{0x0300, 0x036F, ambiguous}, |
||||
{0x0391, 0x03A2, ambiguous}, |
||||
{0x03A3, 0x03A9, ambiguous}, |
||||
{0x03B1, 0x03C1, ambiguous}, |
||||
{0x03C3, 0x03C9, ambiguous}, |
||||
{0x0401, 0x0401, ambiguous}, |
||||
{0x0410, 0x044F, ambiguous}, |
||||
{0x0451, 0x0451, ambiguous}, |
||||
{0x1100, 0x115F, wide}, |
||||
{0x2010, 0x2010, ambiguous}, |
||||
{0x2013, 0x2016, ambiguous}, |
||||
{0x2018, 0x2019, ambiguous}, |
||||
{0x201C, 0x201D, ambiguous}, |
||||
{0x2020, 0x2022, ambiguous}, |
||||
{0x2024, 0x2027, ambiguous}, |
||||
{0x2030, 0x2030, ambiguous}, |
||||
{0x2032, 0x2033, ambiguous}, |
||||
{0x2035, 0x2035, ambiguous}, |
||||
{0x203B, 0x203B, ambiguous}, |
||||
{0x203E, 0x203E, ambiguous}, |
||||
{0x2074, 0x2074, ambiguous}, |
||||
{0x207F, 0x207F, ambiguous}, |
||||
{0x2081, 0x2084, ambiguous}, |
||||
{0x20A9, 0x20A9, halfwidth}, |
||||
{0x20AC, 0x20AC, ambiguous}, |
||||
{0x2103, 0x2103, ambiguous}, |
||||
{0x2105, 0x2105, ambiguous}, |
||||
{0x2109, 0x2109, ambiguous}, |
||||
{0x2113, 0x2113, ambiguous}, |
||||
{0x2116, 0x2116, ambiguous}, |
||||
{0x2121, 0x2122, ambiguous}, |
||||
{0x2126, 0x2126, ambiguous}, |
||||
{0x212B, 0x212B, ambiguous}, |
||||
{0x2153, 0x2154, ambiguous}, |
||||
{0x215B, 0x215E, ambiguous}, |
||||
{0x2160, 0x216B, ambiguous}, |
||||
{0x2170, 0x2179, ambiguous}, |
||||
{0x2189, 0x218A, ambiguous}, |
||||
{0x2190, 0x2199, ambiguous}, |
||||
{0x21B8, 0x21B9, ambiguous}, |
||||
{0x21D2, 0x21D2, ambiguous}, |
||||
{0x21D4, 0x21D4, ambiguous}, |
||||
{0x21E7, 0x21E7, ambiguous}, |
||||
{0x2200, 0x2200, ambiguous}, |
||||
{0x2202, 0x2203, ambiguous}, |
||||
{0x2207, 0x2208, ambiguous}, |
||||
{0x220B, 0x220B, ambiguous}, |
||||
{0x220F, 0x220F, ambiguous}, |
||||
{0x2211, 0x2211, ambiguous}, |
||||
{0x2215, 0x2215, ambiguous}, |
||||
{0x221A, 0x221A, ambiguous}, |
||||
{0x221D, 0x2220, ambiguous}, |
||||
{0x2223, 0x2223, ambiguous}, |
||||
{0x2225, 0x2225, ambiguous}, |
||||
{0x2227, 0x222C, ambiguous}, |
||||
{0x222E, 0x222E, ambiguous}, |
||||
{0x2234, 0x2237, ambiguous}, |
||||
{0x223C, 0x223D, ambiguous}, |
||||
{0x2248, 0x2248, ambiguous}, |
||||
{0x224C, 0x224C, ambiguous}, |
||||
{0x2252, 0x2252, ambiguous}, |
||||
{0x2260, 0x2261, ambiguous}, |
||||
{0x2264, 0x2267, ambiguous}, |
||||
{0x226A, 0x226B, ambiguous}, |
||||
{0x226E, 0x226F, ambiguous}, |
||||
{0x2282, 0x2283, ambiguous}, |
||||
{0x2286, 0x2287, ambiguous}, |
||||
{0x2295, 0x2295, ambiguous}, |
||||
{0x2299, 0x2299, ambiguous}, |
||||
{0x22A5, 0x22A5, ambiguous}, |
||||
{0x22BF, 0x22BF, ambiguous}, |
||||
{0x2312, 0x2312, ambiguous}, |
||||
{0x2329, 0x232A, wide}, |
||||
{0x2460, 0x24E9, ambiguous}, |
||||
{0x24EB, 0x254B, ambiguous}, |
||||
{0x2550, 0x2573, ambiguous}, |
||||
{0x2580, 0x258F, ambiguous}, |
||||
{0x2592, 0x2595, ambiguous}, |
||||
{0x25A0, 0x25A1, ambiguous}, |
||||
{0x25A3, 0x25A9, ambiguous}, |
||||
{0x25B2, 0x25B3, ambiguous}, |
||||
{0x25B6, 0x25B7, ambiguous}, |
||||
{0x25BC, 0x25BD, ambiguous}, |
||||
{0x25C0, 0x25C1, ambiguous}, |
||||
{0x25C6, 0x25C8, ambiguous}, |
||||
{0x25CB, 0x25CB, ambiguous}, |
||||
{0x25CE, 0x25D1, ambiguous}, |
||||
{0x25E2, 0x25E5, ambiguous}, |
||||
{0x25EF, 0x25EF, ambiguous}, |
||||
{0x2605, 0x2606, ambiguous}, |
||||
{0x2609, 0x2609, ambiguous}, |
||||
{0x260E, 0x260F, ambiguous}, |
||||
{0x2614, 0x2615, ambiguous}, |
||||
{0x261C, 0x261C, ambiguous}, |
||||
{0x261E, 0x261E, ambiguous}, |
||||
{0x2640, 0x2640, ambiguous}, |
||||
{0x2642, 0x2642, ambiguous}, |
||||
{0x2660, 0x2661, ambiguous}, |
||||
{0x2663, 0x2665, ambiguous}, |
||||
{0x2667, 0x266A, ambiguous}, |
||||
{0x266C, 0x266D, ambiguous}, |
||||
{0x266F, 0x266F, ambiguous}, |
||||
{0x269E, 0x269F, ambiguous}, |
||||
{0x26BE, 0x26BF, ambiguous}, |
||||
{0x26C4, 0x26CD, ambiguous}, |
||||
{0x26CF, 0x26E1, ambiguous}, |
||||
{0x26E3, 0x26E3, ambiguous}, |
||||
{0x26E8, 0x26FF, ambiguous}, |
||||
{0x273D, 0x273D, ambiguous}, |
||||
{0x2757, 0x2757, ambiguous}, |
||||
{0x2776, 0x277F, ambiguous}, |
||||
{0x27E6, 0x27ED, narrow}, |
||||
{0x2985, 0x2986, narrow}, |
||||
{0x2B55, 0x2B59, ambiguous}, |
||||
{0x2E80, 0x2E9A, wide}, |
||||
{0x2E9B, 0x2EF4, wide}, |
||||
{0x2F00, 0x2FD6, wide}, |
||||
{0x2FF0, 0x2FFC, wide}, |
||||
{0x3000, 0x3000, fullwidth}, |
||||
{0x3001, 0x303E, wide}, |
||||
{0x3041, 0x3097, wide}, |
||||
{0x3099, 0x3100, wide}, |
||||
{0x3105, 0x312E, wide}, |
||||
{0x3131, 0x318F, wide}, |
||||
{0x3190, 0x31BB, wide}, |
||||
{0x31C0, 0x31E4, wide}, |
||||
{0x31F0, 0x321F, wide}, |
||||
{0x3220, 0x3247, wide}, |
||||
{0x3248, 0x324F, ambiguous}, |
||||
{0x3250, 0x32FF, wide}, |
||||
{0x3300, 0x4DBF, wide}, |
||||
{0x4E00, 0xA48D, wide}, |
||||
{0xA490, 0xA4C7, wide}, |
||||
{0xA960, 0xA97D, wide}, |
||||
{0xAC00, 0xD7A4, wide}, |
||||
{0xE000, 0xF8FF, ambiguous}, |
||||
{0xF900, 0xFAFF, wide}, |
||||
{0xFE00, 0xFE0F, ambiguous}, |
||||
{0xFE10, 0xFE1A, wide}, |
||||
{0xFE30, 0xFE53, wide}, |
||||
{0xFE54, 0xFE67, wide}, |
||||
{0xFE68, 0xFE6C, wide}, |
||||
{0xFF01, 0xFF60, fullwidth}, |
||||
{0xFF61, 0xFFBF, halfwidth}, |
||||
{0xFFC2, 0xFFC8, halfwidth}, |
||||
{0xFFCA, 0xFFD0, halfwidth}, |
||||
{0xFFD2, 0xFFD8, halfwidth}, |
||||
{0xFFDA, 0xFFDD, halfwidth}, |
||||
{0xFFE0, 0xFFE7, fullwidth}, |
||||
{0xFFE8, 0xFFEF, halfwidth}, |
||||
{0xFFFD, 0xFFFE, ambiguous}, |
||||
{0x1B000, 0x1B002, wide}, |
||||
{0x1F100, 0x1F10A, ambiguous}, |
||||
{0x1F110, 0x1F12D, ambiguous}, |
||||
{0x1F130, 0x1F169, ambiguous}, |
||||
{0x1F170, 0x1F19B, ambiguous}, |
||||
{0x1F200, 0x1F203, wide}, |
||||
{0x1F210, 0x1F23B, wide}, |
||||
{0x1F240, 0x1F249, wide}, |
||||
{0x1F250, 0x1F252, wide}, |
||||
{0x20000, 0x2FFFE, wide}, |
||||
{0x30000, 0x3FFFE, wide}, |
||||
{0xE0100, 0xE01F0, ambiguous}, |
||||
{0xF0000, 0xFFFFD, ambiguous}, |
||||
{0x100000, 0x10FFFE, ambiguous}, |
||||
} |
||||
|
||||
type Condition struct { |
||||
EastAsianWidth bool |
||||
} |
||||
|
||||
func NewCondition() *Condition { |
||||
return &Condition{EastAsianWidth} |
||||
} |
||||
|
||||
// RuneWidth returns the number of cells in r.
|
||||
// See http://www.unicode.org/reports/tr11/
|
||||
func (c *Condition) RuneWidth(r rune) int { |
||||
if r == 0 { |
||||
return 0 |
||||
} |
||||
if r < 32 || (r >= 0x7f && r < 0xa0) { |
||||
return 1 |
||||
} |
||||
for _, iv := range combining { |
||||
if iv.first <= r && r <= iv.last { |
||||
return 0 |
||||
} |
||||
} |
||||
|
||||
if c.EastAsianWidth && IsAmbiguousWidth(r) { |
||||
return 2 |
||||
} |
||||
|
||||
if r >= 0x1100 && |
||||
(r <= 0x115f || r == 0x2329 || r == 0x232a || |
||||
(r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || |
||||
(r >= 0xac00 && r <= 0xd7a3) || |
||||
(r >= 0xf900 && r <= 0xfaff) || |
||||
(r >= 0xfe30 && r <= 0xfe6f) || |
||||
(r >= 0xff00 && r <= 0xff60) || |
||||
(r >= 0xffe0 && r <= 0xffe6) || |
||||
(r >= 0x20000 && r <= 0x2fffd) || |
||||
(r >= 0x30000 && r <= 0x3fffd)) { |
||||
return 2 |
||||
} |
||||
return 1 |
||||
} |
||||
|
||||
func (c *Condition) StringWidth(s string) (width int) { |
||||
for _, r := range []rune(s) { |
||||
width += c.RuneWidth(r) |
||||
} |
||||
return width |
||||
} |
||||
|
||||
func (c *Condition) Truncate(s string, w int, tail string) string { |
||||
r := []rune(s) |
||||
tw := StringWidth(tail) |
||||
w -= tw |
||||
width := 0 |
||||
i := 0 |
||||
for ; i < len(r); i++ { |
||||
cw := RuneWidth(r[i]) |
||||
if width+cw > w { |
||||
break |
||||
} |
||||
width += cw |
||||
} |
||||
if i == len(r) { |
||||
return string(r[0:i]) |
||||
} |
||||
return string(r[0:i]) + tail |
||||
} |
||||
|
||||
// RuneWidth returns the number of cells in r.
|
||||
// See http://www.unicode.org/reports/tr11/
|
||||
func RuneWidth(r rune) int { |
||||
return DefaultCondition.RuneWidth(r) |
||||
} |
||||
|
||||
func ct(r rune) ctype { |
||||
for _, iv := range ctypes { |
||||
if iv.first <= r && r <= iv.last { |
||||
return iv.ctype |
||||
} |
||||
} |
||||
return neutral |
||||
} |
||||
|
||||
// IsAmbiguousWidth returns whether is ambiguous width or not.
|
||||
func IsAmbiguousWidth(r rune) bool { |
||||
return ct(r) == ambiguous |
||||
} |
||||
|
||||
// IsAmbiguousWidth returns whether is ambiguous width or not.
|
||||
func IsNeutralWidth(r rune) bool { |
||||
return ct(r) == neutral |
||||
} |
||||
|
||||
func StringWidth(s string) (width int) { |
||||
return DefaultCondition.StringWidth(s) |
||||
} |
||||
|
||||
func Truncate(s string, w int, tail string) string { |
||||
return DefaultCondition.Truncate(s, w, tail) |
||||
} |
@ -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,69 @@ |
||||
// +build !windows,!js
|
||||
|
||||
package runewidth |
||||
|
||||
import ( |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`) |
||||
|
||||
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 |
||||
} |
||||
|
||||
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 |
||||
} |
||||
} |
||||
|
||||
mbc_max := 1 |
||||
switch charset { |
||||
case "utf-8", "utf8": |
||||
mbc_max = 6 |
||||
case "jis": |
||||
mbc_max = 8 |
||||
case "eucjp": |
||||
mbc_max = 3 |
||||
case "euckr", "euccn": |
||||
mbc_max = 2 |
||||
case "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950": |
||||
mbc_max = 2 |
||||
case "big5": |
||||
mbc_max = 2 |
||||
case "gbk", "gb2312": |
||||
mbc_max = 2 |
||||
} |
||||
|
||||
if mbc_max > 1 && (charset[0] != 'u' || |
||||
strings.HasPrefix(locale, "ja") || |
||||
strings.HasPrefix(locale, "ko") || |
||||
strings.HasPrefix(locale, "zh")) { |
||||
return true |
||||
} |
||||
return false |
||||
} |
@ -0,0 +1,134 @@ |
||||
package runewidth |
||||
|
||||
import ( |
||||
"testing" |
||||
) |
||||
|
||||
var runewidthtests = []struct { |
||||
in rune |
||||
out int |
||||
}{ |
||||
{'世', 2}, |
||||
{'界', 2}, |
||||
{'セ', 1}, |
||||
{'カ', 1}, |
||||
{'イ', 1}, |
||||
{'☆', 2}, // double width in ambiguous
|
||||
{'\x00', 0}, |
||||
{'\x01', 1}, |
||||
{'\u0300', 0}, |
||||
} |
||||
|
||||
func TestRuneWidth(t *testing.T) { |
||||
c := NewCondition() |
||||
c.EastAsianWidth = true |
||||
for _, tt := range runewidthtests { |
||||
if out := c.RuneWidth(tt.in); out != tt.out { |
||||
t.Errorf("Width(%q) = %v, want %v", tt.in, out, tt.out) |
||||
} |
||||
} |
||||
} |
||||
|
||||
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 |
||||
}{ |
||||
{"■㈱の世界①", 12}, |
||||
{"スター☆", 8}, |
||||
} |
||||
|
||||
func TestStringWidth(t *testing.T) { |
||||
c := NewCondition() |
||||
c.EastAsianWidth = true |
||||
for _, tt := range stringwidthtests { |
||||
if out := c.StringWidth(tt.in); out != tt.out { |
||||
t.Errorf("StringWidth(%q) = %v, want %v", tt.in, out, tt.out) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestStringWidthInvalid(t *testing.T) { |
||||
s := "こんにちわ\x00世界" |
||||
if out := StringWidth(s); out != 14 { |
||||
t.Errorf("StringWidth(%q) = %v, want %v", s, out, 14) |
||||
} |
||||
} |
||||
|
||||
func TestTruncate(t *testing.T) { |
||||
s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお" |
||||
expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..." |
||||
|
||||
if out := Truncate(s, 80, "..."); out != expected { |
||||
t.Errorf("Truncate(%q) = %v, want %v", s, out, expected) |
||||
} |
||||
} |
||||
|
||||
func TestTruncateNoNeeded(t *testing.T) { |
||||
s := "あいうえおあい" |
||||
expected := "あいうえおあい" |
||||
|
||||
if out := Truncate(s, 80, "..."); out != expected { |
||||
t.Errorf("Truncate(%q) = %v, want %v", 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) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
package runewidth |
||||
|
||||
import ( |
||||
"syscall" |
||||
) |
||||
|
||||
var ( |
||||
kernel32 = syscall.NewLazyDLL("kernel32") |
||||
procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP") |
||||
) |
||||
|
||||
func IsEastAsian() bool { |
||||
r1, _, _ := procGetConsoleOutputCP.Call() |
||||
if r1 == 0 { |
||||
return false |
||||
} |
||||
|
||||
switch int(r1) { |
||||
case 932, 51932, 936, 949, 950: |
||||
return true |
||||
} |
||||
|
||||
return false |
||||
} |
@ -0,0 +1,4 @@ |
||||
# Please keep this file sorted. |
||||
|
||||
Georg Reinke <guelfey@googlemail.com> |
||||
nsf <no.smile.face@gmail.com> |
@ -0,0 +1,19 @@ |
||||
Copyright (C) 2012 termbox-go authors |
||||
|
||||
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,21 @@ |
||||
## Termbox |
||||
Termbox is a library that provides a minimalistic API which allows the programmer to write text-based user interfaces. The library is crossplatform and has both terminal-based implementations on *nix operating systems and a winapi console based implementation for windows operating systems. The basic idea is an abstraction of the greatest common subset of features available on all major terminals and other terminal-like APIs in a minimalistic fashion. Small API means it is easy to implement, test, maintain and learn it, that's what makes the termbox a distinct library in its area. |
||||
|
||||
### Installation |
||||
Install and update this go package with `go get -u github.com/nsf/termbox-go` |
||||
|
||||
### Examples |
||||
For examples of what can be done take a look at demos in the _demos directory. You can try them with go run: `go run _demos/keyboard.go` |
||||
|
||||
There are also some interesting projects using termbox-go: |
||||
- [godit](https://github.com/nsf/godit) is an emacsish lightweight text editor written using termbox. |
||||
- [gomatrix](https://github.com/GeertJohan/gomatrix) connects to The Matrix and displays its data streams in your terminal. |
||||
- [gotetris](https://github.com/jjinux/gotetris) is an implementation of Tetris. |
||||
- [sokoban-go](https://github.com/rn2dy/sokoban-go) is an implementation of sokoban game. |
||||
- [hecate](https://github.com/evanmiller/hecate) is a hex editor designed by Satan. |
||||
- [httopd](https://github.com/verdverm/httopd) is top for httpd logs. |
||||
- [mop](https://github.com/michaeldv/mop) is stock market tracker for hackers. |
||||
- [termui](https://github.com/gizak/termui) is a terminal dashboard. |
||||
|
||||
### API reference |
||||
[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go) |
@ -0,0 +1,451 @@ |
||||
// +build !windows
|
||||
|
||||
package termbox |
||||
|
||||
import "github.com/mattn/go-runewidth" |
||||
import "fmt" |
||||
import "os" |
||||
import "os/signal" |
||||
import "syscall" |
||||
import "runtime" |
||||
|
||||
// public API
|
||||
|
||||
// Initializes termbox library. This function should be called before any other functions.
|
||||
// After successful initialization, the library must be finalized using 'Close' function.
|
||||
//
|
||||
// Example usage:
|
||||
// err := termbox.Init()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer termbox.Close()
|
||||
func Init() error { |
||||
var err error |
||||
|
||||
out, err = os.OpenFile("/dev/tty", syscall.O_WRONLY, 0) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = setup_term() |
||||
if err != nil { |
||||
return fmt.Errorf("termbox: error while reading terminfo data: %v", err) |
||||
} |
||||
|
||||
signal.Notify(sigwinch, syscall.SIGWINCH) |
||||
signal.Notify(sigio, syscall.SIGIO) |
||||
|
||||
_, err = fcntl(in, syscall.F_SETFL, syscall.O_ASYNC|syscall.O_NONBLOCK) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_, err = fcntl(in, syscall.F_SETOWN, syscall.Getpid()) |
||||
if runtime.GOOS != "darwin" && err != nil { |
||||
return err |
||||
} |
||||
err = tcgetattr(out.Fd(), &orig_tios) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
tios := orig_tios |
||||
tios.Iflag &^= syscall_IGNBRK | syscall_BRKINT | syscall_PARMRK | |
||||
syscall_ISTRIP | syscall_INLCR | syscall_IGNCR | |
||||
syscall_ICRNL | syscall_IXON |
||||
tios.Oflag &^= syscall_OPOST |
||||
tios.Lflag &^= syscall_ECHO | syscall_ECHONL | syscall_ICANON | |
||||
syscall_ISIG | syscall_IEXTEN |
||||
tios.Cflag &^= syscall_CSIZE | syscall_PARENB |
||||
tios.Cflag |= syscall_CS8 |
||||
tios.Cc[syscall_VMIN] = 1 |
||||
tios.Cc[syscall_VTIME] = 0 |
||||
|
||||
err = tcsetattr(out.Fd(), &tios) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
out.WriteString(funcs[t_enter_ca]) |
||||
out.WriteString(funcs[t_enter_keypad]) |
||||
out.WriteString(funcs[t_hide_cursor]) |
||||
out.WriteString(funcs[t_clear_screen]) |
||||
|
||||
termw, termh = get_term_size(out.Fd()) |
||||
back_buffer.init(termw, termh) |
||||
front_buffer.init(termw, termh) |
||||
back_buffer.clear() |
||||
front_buffer.clear() |
||||
|
||||
go func() { |
||||
buf := make([]byte, 128) |
||||
for { |
||||
select { |
||||
case <-sigio: |
||||
for { |
||||
n, err := syscall.Read(in, buf) |
||||
if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { |
||||
break |
||||
} |
||||
select { |
||||
case input_comm <- input_event{buf[:n], err}: |
||||
ie := <-input_comm |
||||
buf = ie.data[:128] |
||||
case <-quit: |
||||
return |
||||
} |
||||
} |
||||
case <-quit: |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
|
||||
IsInit = true |
||||
return nil |
||||
} |
||||
|
||||
// Interrupt an in-progress call to PollEvent by causing it to return
|
||||
// EventInterrupt. Note that this function will block until the PollEvent
|
||||
// function has successfully been interrupted.
|
||||
func Interrupt() { |
||||
interrupt_comm <- struct{}{} |
||||
} |
||||
|
||||
// Finalizes termbox library, should be called after successful initialization
|
||||
// when termbox's functionality isn't required anymore.
|
||||
func Close() { |
||||
quit <- 1 |
||||
out.WriteString(funcs[t_show_cursor]) |
||||
out.WriteString(funcs[t_sgr0]) |
||||
out.WriteString(funcs[t_clear_screen]) |
||||
out.WriteString(funcs[t_exit_ca]) |
||||
out.WriteString(funcs[t_exit_keypad]) |
||||
out.WriteString(funcs[t_exit_mouse]) |
||||
tcsetattr(out.Fd(), &orig_tios) |
||||
|
||||
out.Close() |
||||
syscall.Close(in) |
||||
|
||||
// reset the state, so that on next Init() it will work again
|
||||
termw = 0 |
||||
termh = 0 |
||||
input_mode = InputEsc |
||||
out = nil |
||||
in = 0 |
||||
lastfg = attr_invalid |
||||
lastbg = attr_invalid |
||||
lastx = coord_invalid |
||||
lasty = coord_invalid |
||||
cursor_x = cursor_hidden |
||||
cursor_y = cursor_hidden |
||||
foreground = ColorDefault |
||||
background = ColorDefault |
||||
IsInit = false |
||||
} |
||||
|
||||
// Synchronizes the internal back buffer with the terminal.
|
||||
func Flush() error { |
||||
// invalidate cursor position
|
||||
lastx = coord_invalid |
||||
lasty = coord_invalid |
||||
|
||||
update_size_maybe() |
||||
|
||||
for y := 0; y < front_buffer.height; y++ { |
||||
line_offset := y * front_buffer.width |
||||
for x := 0; x < front_buffer.width; { |
||||
cell_offset := line_offset + x |
||||
back := &back_buffer.cells[cell_offset] |
||||
front := &front_buffer.cells[cell_offset] |
||||
if back.Ch < ' ' { |
||||
back.Ch = ' ' |
||||
} |
||||
w := runewidth.RuneWidth(back.Ch) |
||||
if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { |
||||
w = 1 |
||||
} |
||||
if *back == *front { |
||||
x += w |
||||
continue |
||||
} |
||||
*front = *back |
||||
send_attr(back.Fg, back.Bg) |
||||
|
||||
if w == 2 && x == front_buffer.width-1 { |
||||
// there's not enough space for 2-cells rune,
|
||||
// let's just put a space in there
|
||||
send_char(x, y, ' ') |
||||
} else { |
||||
send_char(x, y, back.Ch) |
||||
if w == 2 { |
||||
next := cell_offset + 1 |
||||
front_buffer.cells[next] = Cell{ |
||||
Ch: 0, |
||||
Fg: back.Fg, |
||||
Bg: back.Bg, |
||||
} |
||||
} |
||||
} |
||||
x += w |
||||
} |
||||
} |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
write_cursor(cursor_x, cursor_y) |
||||
} |
||||
return flush() |
||||
} |
||||
|
||||
// Sets the position of the cursor. See also HideCursor().
|
||||
func SetCursor(x, y int) { |
||||
if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { |
||||
outbuf.WriteString(funcs[t_show_cursor]) |
||||
} |
||||
|
||||
if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { |
||||
outbuf.WriteString(funcs[t_hide_cursor]) |
||||
} |
||||
|
||||
cursor_x, cursor_y = x, y |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
write_cursor(cursor_x, cursor_y) |
||||
} |
||||
} |
||||
|
||||
// The shortcut for SetCursor(-1, -1).
|
||||
func HideCursor() { |
||||
SetCursor(cursor_hidden, cursor_hidden) |
||||
} |
||||
|
||||
// Changes cell's parameters in the internal back buffer at the specified
|
||||
// position.
|
||||
func SetCell(x, y int, ch rune, fg, bg Attribute) { |
||||
if x < 0 || x >= back_buffer.width { |
||||
return |
||||
} |
||||
if y < 0 || y >= back_buffer.height { |
||||
return |
||||
} |
||||
|
||||
back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} |
||||
} |
||||
|
||||
// Returns a slice into the termbox's back buffer. You can get its dimensions
|
||||
// using 'Size' function. The slice remains valid as long as no 'Clear' or
|
||||
// 'Flush' function calls were made after call to this function.
|
||||
func CellBuffer() []Cell { |
||||
return back_buffer.cells |
||||
} |
||||
|
||||
// After getting a raw event from PollRawEvent function call, you can parse it
|
||||
// again into an ordinary one using termbox logic. That is parse an event as
|
||||
// termbox would do it. Returned event in addition to usual Event struct fields
|
||||
// sets N field to the amount of bytes used within 'data' slice. If the length
|
||||
// of 'data' slice is zero or event cannot be parsed for some other reason, the
|
||||
// function will return a special event type: EventNone.
|
||||
//
|
||||
// IMPORTANT: EventNone may contain a non-zero N, which means you should skip
|
||||
// these bytes, because termbox cannot recognize them.
|
||||
//
|
||||
// NOTE: This API is experimental and may change in future.
|
||||
func ParseEvent(data []byte) Event { |
||||
event := Event{Type: EventKey} |
||||
ok := extract_event(data, &event) |
||||
if !ok { |
||||
return Event{Type: EventNone, N: event.N} |
||||
} |
||||
return event |
||||
} |
||||
|
||||
// Wait for an event and return it. This is a blocking function call. Instead
|
||||
// of EventKey and EventMouse it returns EventRaw events. Raw event is written
|
||||
// into `data` slice and Event's N field is set to the amount of bytes written.
|
||||
// The minimum required length of the 'data' slice is 1. This requirement may
|
||||
// vary on different platforms.
|
||||
//
|
||||
// NOTE: This API is experimental and may change in future.
|
||||
func PollRawEvent(data []byte) Event { |
||||
if len(data) == 0 { |
||||
panic("len(data) >= 1 is a requirement") |
||||
} |
||||
|
||||
var event Event |
||||
if extract_raw_event(data, &event) { |
||||
return event |
||||
} |
||||
|
||||
for { |
||||
select { |
||||
case ev := <-input_comm: |
||||
if ev.err != nil { |
||||
return Event{Type: EventError, Err: ev.err} |
||||
} |
||||
|
||||
inbuf = append(inbuf, ev.data...) |
||||
input_comm <- ev |
||||
if extract_raw_event(data, &event) { |
||||
return event |
||||
} |
||||
case <-interrupt_comm: |
||||
event.Type = EventInterrupt |
||||
return event |
||||
|
||||
case <-sigwinch: |
||||
event.Type = EventResize |
||||
event.Width, event.Height = get_term_size(out.Fd()) |
||||
return event |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Wait for an event and return it. This is a blocking function call.
|
||||
func PollEvent() Event { |
||||
var event Event |
||||
|
||||
// try to extract event from input buffer, return on success
|
||||
event.Type = EventKey |
||||
ok := extract_event(inbuf, &event) |
||||
if event.N != 0 { |
||||
copy(inbuf, inbuf[event.N:]) |
||||
inbuf = inbuf[:len(inbuf)-event.N] |
||||
} |
||||
if ok { |
||||
return event |
||||
} |
||||
|
||||
for { |
||||
select { |
||||
case ev := <-input_comm: |
||||
if ev.err != nil { |
||||
return Event{Type: EventError, Err: ev.err} |
||||
} |
||||
|
||||
inbuf = append(inbuf, ev.data...) |
||||
input_comm <- ev |
||||
ok := extract_event(inbuf, &event) |
||||
if event.N != 0 { |
||||
copy(inbuf, inbuf[event.N:]) |
||||
inbuf = inbuf[:len(inbuf)-event.N] |
||||
} |
||||
if ok { |
||||
return event |
||||
} |
||||
case <-interrupt_comm: |
||||
event.Type = EventInterrupt |
||||
return event |
||||
|
||||
case <-sigwinch: |
||||
event.Type = EventResize |
||||
event.Width, event.Height = get_term_size(out.Fd()) |
||||
return event |
||||
} |
||||
} |
||||
panic("unreachable") |
||||
} |
||||
|
||||
// Returns the size of the internal back buffer (which is mostly the same as
|
||||
// terminal's window size in characters). But it doesn't always match the size
|
||||
// of the terminal window, after the terminal size has changed, the internal
|
||||
// back buffer will get in sync only after Clear or Flush function calls.
|
||||
func Size() (int, int) { |
||||
return termw, termh |
||||
} |
||||
|
||||
// Clears the internal back buffer.
|
||||
func Clear(fg, bg Attribute) error { |
||||
foreground, background = fg, bg |
||||
err := update_size_maybe() |
||||
back_buffer.clear() |
||||
return err |
||||
} |
||||
|
||||
// Sets termbox input mode. Termbox has two input modes:
|
||||
//
|
||||
// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match
|
||||
// any known sequence. ESC means KeyEsc. This is the default input mode.
|
||||
//
|
||||
// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match
|
||||
// any known sequence. ESC enables ModAlt modifier for the next keyboard event.
|
||||
//
|
||||
// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will
|
||||
// enable mouse button click events.
|
||||
//
|
||||
// If 'mode' is InputCurrent, returns the current input mode. See also Input*
|
||||
// constants.
|
||||
func SetInputMode(mode InputMode) InputMode { |
||||
if mode == InputCurrent { |
||||
return input_mode |
||||
} |
||||
if mode&InputMouse != 0 { |
||||
out.WriteString(funcs[t_enter_mouse]) |
||||
} else { |
||||
out.WriteString(funcs[t_exit_mouse]) |
||||
} |
||||
|
||||
input_mode = mode |
||||
return input_mode |
||||
} |
||||
|
||||
// Sets the termbox output mode. Termbox has four output options:
|
||||
// 1. OutputNormal => [1..8]
|
||||
// This mode provides 8 different colors:
|
||||
// black, red, green, yellow, blue, magenta, cyan, white
|
||||
// Shortcut: ColorBlack, ColorRed, ...
|
||||
// Attributes: AttrBold, AttrUnderline, AttrReverse
|
||||
//
|
||||
// Example usage:
|
||||
// SetCell(x, y, '@', ColorBlack | AttrBold, ColorRed);
|
||||
//
|
||||
// 2. Output256 => [1..256]
|
||||
// In this mode you can leverage the 256 terminal mode:
|
||||
// 0x00 - 0x07: the 8 colors as in OutputNormal
|
||||
// 0x08 - 0x0f: Color* | AttrBold
|
||||
// 0x10 - 0xe7: 216 different colors
|
||||
// 0xe8 - 0xff: 24 different shades of grey
|
||||
//
|
||||
// Example usage:
|
||||
// SetCell(x, y, '@', 184, 240);
|
||||
// SetCell(x, y, '@', 0xb8, 0xf0);
|
||||
//
|
||||
// 3. Output216 => [1..216]
|
||||
// This mode supports the 3rd range of the 256 mode only.
|
||||
// But you dont need to provide an offset.
|
||||
//
|
||||
// 4. OutputGrayscale => [1..24]
|
||||
// This mode supports the 4th range of the 256 mode only.
|
||||
// But you dont need to provide an offset.
|
||||
//
|
||||
// In all modes, 0 represents the default color.
|
||||
//
|
||||
// `go run _demos/output.go` to see its impact on your terminal.
|
||||
//
|
||||
// If 'mode' is OutputCurrent, it returns the current output mode.
|
||||
//
|
||||
// Note that this may return a different OutputMode than the one requested,
|
||||
// as the requested mode may not be available on the target platform.
|
||||
func SetOutputMode(mode OutputMode) OutputMode { |
||||
if mode == OutputCurrent { |
||||
return output_mode |
||||
} |
||||
|
||||
output_mode = mode |
||||
return output_mode |
||||
} |
||||
|
||||
// Sync comes handy when something causes desync between termbox's understanding
|
||||
// of a terminal buffer and the reality. Such as a third party process. Sync
|
||||
// forces a complete resync between the termbox and a terminal, it may not be
|
||||
// visually pretty though.
|
||||
func Sync() error { |
||||
front_buffer.clear() |
||||
err := send_clear() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return Flush() |
||||
} |
@ -0,0 +1,183 @@ |
||||
// termbox is a library for creating cross-platform text-based interfaces
|
||||
package termbox |
||||
|
||||
// public API, common OS agnostic part
|
||||
|
||||
type ( |
||||
InputMode int |
||||
OutputMode int |
||||
EventType uint8 |
||||
Modifier uint8 |
||||
Key uint16 |
||||
Attribute uint16 |
||||
) |
||||
|
||||
// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are
|
||||
// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if
|
||||
// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError.
|
||||
type Event struct { |
||||
Type EventType // one of Event* constants
|
||||
Mod Modifier // one of Mod* constants or 0
|
||||
Key Key // one of Key* constants, invalid if 'Ch' is not 0
|
||||
Ch rune // a unicode character
|
||||
Width int // width of the screen
|
||||
Height int // height of the screen
|
||||
Err error // error in case if input failed
|
||||
MouseX int // x coord of mouse
|
||||
MouseY int // y coord of mouse
|
||||
N int // number of bytes written when getting a raw event
|
||||
} |
||||
|
||||
// A cell, single conceptual entity on the screen. The screen is basically a 2d
|
||||
// array of cells. 'Ch' is a unicode character, 'Fg' and 'Bg' are foreground
|
||||
// and background attributes respectively.
|
||||
type Cell struct { |
||||
Ch rune |
||||
Fg Attribute |
||||
Bg Attribute |
||||
} |
||||
|
||||
// To know if termbox has been initialized or not
|
||||
var ( |
||||
IsInit bool = false |
||||
) |
||||
|
||||
// Key constants, see Event.Key field.
|
||||
const ( |
||||
KeyF1 Key = 0xFFFF - iota |
||||
KeyF2 |
||||
KeyF3 |
||||
KeyF4 |
||||
KeyF5 |
||||
KeyF6 |
||||
KeyF7 |
||||
KeyF8 |
||||
KeyF9 |
||||
KeyF10 |
||||
KeyF11 |
||||
KeyF12 |
||||
KeyInsert |
||||
KeyDelete |
||||
KeyHome |
||||
KeyEnd |
||||
KeyPgup |
||||
KeyPgdn |
||||
KeyArrowUp |
||||
KeyArrowDown |
||||
KeyArrowLeft |
||||
KeyArrowRight |
||||
key_min // see terminfo
|
||||
MouseLeft |
||||
MouseMiddle |
||||
MouseRight |
||||
) |
||||
|
||||
const ( |
||||
KeyCtrlTilde Key = 0x00 |
||||
KeyCtrl2 Key = 0x00 |
||||
KeyCtrlSpace Key = 0x00 |
||||
KeyCtrlA Key = 0x01 |
||||
KeyCtrlB Key = 0x02 |
||||
KeyCtrlC Key = 0x03 |
||||
KeyCtrlD Key = 0x04 |
||||
KeyCtrlE Key = 0x05 |
||||
KeyCtrlF Key = 0x06 |
||||
KeyCtrlG Key = 0x07 |
||||
KeyBackspace Key = 0x08 |
||||
KeyCtrlH Key = 0x08 |
||||
KeyTab Key = 0x09 |
||||
KeyCtrlI Key = 0x09 |
||||
KeyCtrlJ Key = 0x0A |
||||
KeyCtrlK Key = 0x0B |
||||
KeyCtrlL Key = 0x0C |
||||
KeyEnter Key = 0x0D |
||||
KeyCtrlM Key = 0x0D |
||||
KeyCtrlN Key = 0x0E |
||||
KeyCtrlO Key = 0x0F |
||||
KeyCtrlP Key = 0x10 |
||||
KeyCtrlQ Key = 0x11 |
||||
KeyCtrlR Key = 0x12 |
||||
KeyCtrlS Key = 0x13 |
||||
KeyCtrlT Key = 0x14 |
||||
KeyCtrlU Key = 0x15 |
||||
KeyCtrlV Key = 0x16 |
||||
KeyCtrlW Key = 0x17 |
||||
KeyCtrlX Key = 0x18 |
||||
KeyCtrlY Key = 0x19 |
||||
KeyCtrlZ Key = 0x1A |
||||
KeyEsc Key = 0x1B |
||||
KeyCtrlLsqBracket Key = 0x1B |
||||
KeyCtrl3 Key = 0x1B |
||||
KeyCtrl4 Key = 0x1C |
||||
KeyCtrlBackslash Key = 0x1C |
||||
KeyCtrl5 Key = 0x1D |
||||
KeyCtrlRsqBracket Key = 0x1D |
||||
KeyCtrl6 Key = 0x1E |
||||
KeyCtrl7 Key = 0x1F |
||||
KeyCtrlSlash Key = 0x1F |
||||
KeyCtrlUnderscore Key = 0x1F |
||||
KeySpace Key = 0x20 |
||||
KeyBackspace2 Key = 0x7F |
||||
KeyCtrl8 Key = 0x7F |
||||
) |
||||
|
||||
// Alt modifier constant, see Event.Mod field and SetInputMode function.
|
||||
const ( |
||||
ModAlt Modifier = 0x01 |
||||
) |
||||
|
||||
// Cell colors, you can combine a color with multiple attributes using bitwise
|
||||
// OR ('|').
|
||||
const ( |
||||
ColorDefault Attribute = iota |
||||
ColorBlack |
||||
ColorRed |
||||
ColorGreen |
||||
ColorYellow |
||||
ColorBlue |
||||
ColorMagenta |
||||
ColorCyan |
||||
ColorWhite |
||||
) |
||||
|
||||
// Cell attributes, it is possible to use multiple attributes by combining them
|
||||
// using bitwise OR ('|'). Although, colors cannot be combined. But you can
|
||||
// combine attributes and a single color.
|
||||
//
|
||||
// It's worth mentioning that some platforms don't support certain attibutes.
|
||||
// For example windows console doesn't support AttrUnderline. And on some
|
||||
// terminals applying AttrBold to background may result in blinking text. Use
|
||||
// them with caution and test your code on various terminals.
|
||||
const ( |
||||
AttrBold Attribute = 1 << (iota + 9) |
||||
AttrUnderline |
||||
AttrReverse |
||||
) |
||||
|
||||
// Input mode. See SetInputMode function.
|
||||
const ( |
||||
InputEsc InputMode = 1 << iota |
||||
InputAlt |
||||
InputMouse |
||||
InputCurrent InputMode = 0 |
||||
) |
||||
|
||||
// Output mode. See SetOutputMode function.
|
||||
const ( |
||||
OutputCurrent OutputMode = iota |
||||
OutputNormal |
||||
Output256 |
||||
Output216 |
||||
OutputGrayscale |
||||
) |
||||
|
||||
// Event type. See Event.Type field.
|
||||
const ( |
||||
EventKey EventType = iota |
||||
EventResize |
||||
EventMouse |
||||
EventError |
||||
EventInterrupt |
||||
EventRaw |
||||
EventNone |
||||
) |
@ -0,0 +1,235 @@ |
||||
package termbox |
||||
|
||||
import ( |
||||
"syscall" |
||||
) |
||||
|
||||
// public API
|
||||
|
||||
// Initializes termbox library. This function should be called before any other functions.
|
||||
// After successful initialization, the library must be finalized using 'Close' function.
|
||||
//
|
||||
// Example usage:
|
||||
// err := termbox.Init()
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer termbox.Close()
|
||||
func Init() error { |
||||
var err error |
||||
|
||||
interrupt, err = create_event() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
in, err = syscall.Open("CONIN$", syscall.O_RDWR, 0) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
out, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = get_console_mode(in, &orig_mode) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = set_console_mode(in, enable_window_input) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
orig_size = get_term_size(out) |
||||
win_size := get_win_size(out) |
||||
|
||||
err = set_console_screen_buffer_size(out, win_size) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = get_console_cursor_info(out, &orig_cursor_info) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
show_cursor(false) |
||||
term_size = get_term_size(out) |
||||
back_buffer.init(int(term_size.x), int(term_size.y)) |
||||
front_buffer.init(int(term_size.x), int(term_size.y)) |
||||
back_buffer.clear() |
||||
front_buffer.clear() |
||||
clear() |
||||
|
||||
diffbuf = make([]diff_msg, 0, 32) |
||||
|
||||
go input_event_producer() |
||||
IsInit = true |
||||
return nil |
||||
} |
||||
|
||||
// Finalizes termbox library, should be called after successful initialization
|
||||
// when termbox's functionality isn't required anymore.
|
||||
func Close() { |
||||
// we ignore errors here, because we can't really do anything about them
|
||||
Clear(0, 0) |
||||
Flush() |
||||
|
||||
// stop event producer
|
||||
cancel_comm <- true |
||||
set_event(interrupt) |
||||
<-cancel_done_comm |
||||
|
||||
set_console_cursor_info(out, &orig_cursor_info) |
||||
set_console_cursor_position(out, coord{}) |
||||
set_console_screen_buffer_size(out, orig_size) |
||||
set_console_mode(in, orig_mode) |
||||
syscall.Close(in) |
||||
syscall.Close(out) |
||||
syscall.Close(interrupt) |
||||
IsInit = false |
||||
} |
||||
|
||||
// Interrupt an in-progress call to PollEvent by causing it to return
|
||||
// EventInterrupt. Note that this function will block until the PollEvent
|
||||
// function has successfully been interrupted.
|
||||
func Interrupt() { |
||||
interrupt_comm <- struct{}{} |
||||
} |
||||
|
||||
// Synchronizes the internal back buffer with the terminal.
|
||||
func Flush() error { |
||||
update_size_maybe() |
||||
prepare_diff_messages() |
||||
for _, diff := range diffbuf { |
||||
r := small_rect{ |
||||
left: 0, |
||||
top: diff.pos, |
||||
right: term_size.x - 1, |
||||
bottom: diff.pos + diff.lines - 1, |
||||
} |
||||
write_console_output(out, diff.chars, r) |
||||
} |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
move_cursor(cursor_x, cursor_y) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// Sets the position of the cursor. See also HideCursor().
|
||||
func SetCursor(x, y int) { |
||||
if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { |
||||
show_cursor(true) |
||||
} |
||||
|
||||
if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { |
||||
show_cursor(false) |
||||
} |
||||
|
||||
cursor_x, cursor_y = x, y |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
move_cursor(cursor_x, cursor_y) |
||||
} |
||||
} |
||||
|
||||
// The shortcut for SetCursor(-1, -1).
|
||||
func HideCursor() { |
||||
SetCursor(cursor_hidden, cursor_hidden) |
||||
} |
||||
|
||||
// Changes cell's parameters in the internal back buffer at the specified
|
||||
// position.
|
||||
func SetCell(x, y int, ch rune, fg, bg Attribute) { |
||||
if x < 0 || x >= back_buffer.width { |
||||
return |
||||
} |
||||
if y < 0 || y >= back_buffer.height { |
||||
return |
||||
} |
||||
|
||||
back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} |
||||
} |
||||
|
||||
// Returns a slice into the termbox's back buffer. You can get its dimensions
|
||||
// using 'Size' function. The slice remains valid as long as no 'Clear' or
|
||||
// 'Flush' function calls were made after call to this function.
|
||||
func CellBuffer() []Cell { |
||||
return back_buffer.cells |
||||
} |
||||
|
||||
// Wait for an event and return it. This is a blocking function call.
|
||||
func PollEvent() Event { |
||||
select { |
||||
case ev := <-input_comm: |
||||
return ev |
||||
case <-interrupt_comm: |
||||
return Event{Type: EventInterrupt} |
||||
} |
||||
} |
||||
|
||||
// Returns the size of the internal back buffer (which is mostly the same as
|
||||
// console's window size in characters). But it doesn't always match the size
|
||||
// of the console window, after the console size has changed, the internal back
|
||||
// buffer will get in sync only after Clear or Flush function calls.
|
||||
func Size() (int, int) { |
||||
return int(term_size.x), int(term_size.y) |
||||
} |
||||
|
||||
// Clears the internal back buffer.
|
||||
func Clear(fg, bg Attribute) error { |
||||
foreground, background = fg, bg |
||||
update_size_maybe() |
||||
back_buffer.clear() |
||||
return nil |
||||
} |
||||
|
||||
// Sets termbox input mode. Termbox has two input modes:
|
||||
//
|
||||
// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match
|
||||
// any known sequence. ESC means KeyEsc. This is the default input mode.
|
||||
//
|
||||
// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match
|
||||
// any known sequence. ESC enables ModAlt modifier for the next keyboard event.
|
||||
//
|
||||
// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will
|
||||
// enable mouse button click events.
|
||||
//
|
||||
// If 'mode' is InputCurrent, returns the current input mode. See also Input*
|
||||
// constants.
|
||||
func SetInputMode(mode InputMode) InputMode { |
||||
if mode == InputCurrent { |
||||
return input_mode |
||||
} |
||||
if mode&InputMouse != 0 { |
||||
err := set_console_mode(in, enable_window_input|enable_mouse_input|enable_extended_flags) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} else { |
||||
err := set_console_mode(in, enable_window_input) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
input_mode = mode |
||||
return input_mode |
||||
} |
||||
|
||||
// Sets the termbox output mode.
|
||||
//
|
||||
// Windows console does not support extra colour modes,
|
||||
// so this will always set and return OutputNormal.
|
||||
func SetOutputMode(mode OutputMode) OutputMode { |
||||
return OutputNormal |
||||
} |
||||
|
||||
// Sync comes handy when something causes desync between termbox's understanding
|
||||
// of a terminal buffer and the reality. Such as a third party process. Sync
|
||||
// forces a complete resync between the termbox and a terminal, it may not be
|
||||
// visually pretty though. At the moment on Windows it does nothing.
|
||||
func Sync() error { |
||||
return nil |
||||
} |
@ -0,0 +1,110 @@ |
||||
#!/usr/bin/env python |
||||
|
||||
import sys, os, subprocess |
||||
|
||||
def escaped(s): |
||||
return repr(s)[1:-1] |
||||
|
||||
def tput(term, name): |
||||
try: |
||||
return subprocess.check_output(['tput', '-T%s' % term, name]).decode() |
||||
except subprocess.CalledProcessError as e: |
||||
return e.output.decode() |
||||
|
||||
|
||||
def w(s): |
||||
if s == None: |
||||
return |
||||
sys.stdout.write(s) |
||||
|
||||
terminals = { |
||||
'xterm' : 'xterm', |
||||
'rxvt-256color' : 'rxvt_256color', |
||||
'rxvt-unicode' : 'rxvt_unicode', |
||||
'linux' : 'linux', |
||||
'Eterm' : 'eterm', |
||||
'screen' : 'screen' |
||||
} |
||||
|
||||
keys = [ |
||||
"F1", "kf1", |
||||
"F2", "kf2", |
||||
"F3", "kf3", |
||||
"F4", "kf4", |
||||
"F5", "kf5", |
||||
"F6", "kf6", |
||||
"F7", "kf7", |
||||
"F8", "kf8", |
||||
"F9", "kf9", |
||||
"F10", "kf10", |
||||
"F11", "kf11", |
||||
"F12", "kf12", |
||||
"INSERT", "kich1", |
||||
"DELETE", "kdch1", |
||||
"HOME", "khome", |
||||
"END", "kend", |
||||
"PGUP", "kpp", |
||||
"PGDN", "knp", |
||||
"KEY_UP", "kcuu1", |
||||
"KEY_DOWN", "kcud1", |
||||
"KEY_LEFT", "kcub1", |
||||
"KEY_RIGHT", "kcuf1" |
||||
] |
||||
|
||||
funcs = [ |
||||
"T_ENTER_CA", "smcup", |
||||
"T_EXIT_CA", "rmcup", |
||||
"T_SHOW_CURSOR", "cnorm", |
||||
"T_HIDE_CURSOR", "civis", |
||||
"T_CLEAR_SCREEN", "clear", |
||||
"T_SGR0", "sgr0", |
||||
"T_UNDERLINE", "smul", |
||||
"T_BOLD", "bold", |
||||
"T_BLINK", "blink", |
||||
"T_REVERSE", "rev", |
||||
"T_ENTER_KEYPAD", "smkx", |
||||
"T_EXIT_KEYPAD", "rmkx" |
||||
] |
||||
|
||||
def iter_pairs(iterable): |
||||
iterable = iter(iterable) |
||||
while True: |
||||
yield (next(iterable), next(iterable)) |
||||
|
||||
def do_term(term, nick): |
||||
w("// %s\n" % term) |
||||
w("var %s_keys = []string{\n\t" % nick) |
||||
for k, v in iter_pairs(keys): |
||||
w('"') |
||||
w(escaped(tput(term, v))) |
||||
w('",') |
||||
w("\n}\n") |
||||
w("var %s_funcs = []string{\n\t" % nick) |
||||
for k,v in iter_pairs(funcs): |
||||
w('"') |
||||
if v == "sgr": |
||||
w("\\033[3%d;4%dm") |
||||
elif v == "cup": |
||||
w("\\033[%d;%dH") |
||||
else: |
||||
w(escaped(tput(term, v))) |
||||
w('", ') |
||||
w("\n}\n\n") |
||||
|
||||
def do_terms(d): |
||||
w("var terms = []struct {\n") |
||||
w("\tname string\n") |
||||
w("\tkeys []string\n") |
||||
w("\tfuncs []string\n") |
||||
w("}{\n") |
||||
for k, v in d.items(): |
||||
w('\t{"%s", %s_keys, %s_funcs},\n' % (k, v, v)) |
||||
w("}\n\n") |
||||
|
||||
w("// +build !windows\n\npackage termbox\n\n") |
||||
|
||||
for k,v in terminals.items(): |
||||
do_term(k, v) |
||||
|
||||
do_terms(terminals) |
||||
|
@ -0,0 +1,39 @@ |
||||
// +build ignore
|
||||
|
||||
package termbox |
||||
|
||||
/* |
||||
#include <termios.h> |
||||
#include <sys/ioctl.h> |
||||
*/ |
||||
import "C" |
||||
|
||||
type syscall_Termios C.struct_termios |
||||
|
||||
const ( |
||||
syscall_IGNBRK = C.IGNBRK |
||||
syscall_BRKINT = C.BRKINT |
||||
syscall_PARMRK = C.PARMRK |
||||
syscall_ISTRIP = C.ISTRIP |
||||
syscall_INLCR = C.INLCR |
||||
syscall_IGNCR = C.IGNCR |
||||
syscall_ICRNL = C.ICRNL |
||||
syscall_IXON = C.IXON |
||||
syscall_OPOST = C.OPOST |
||||
syscall_ECHO = C.ECHO |
||||
syscall_ECHONL = C.ECHONL |
||||
syscall_ICANON = C.ICANON |
||||
syscall_ISIG = C.ISIG |
||||
syscall_IEXTEN = C.IEXTEN |
||||
syscall_CSIZE = C.CSIZE |
||||
syscall_PARENB = C.PARENB |
||||
syscall_CS8 = C.CS8 |
||||
syscall_VMIN = C.VMIN |
||||
syscall_VTIME = C.VTIME |
||||
|
||||
// on darwin change these to (on *bsd too?):
|
||||
// C.TIOCGETA
|
||||
// C.TIOCSETA
|
||||
syscall_TCGETS = C.TCGETS |
||||
syscall_TCSETS = C.TCSETS |
||||
) |
@ -0,0 +1,39 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
type syscall_Termios struct { |
||||
Iflag uint32 |
||||
Oflag uint32 |
||||
Cflag uint32 |
||||
Lflag uint32 |
||||
Cc [20]uint8 |
||||
Ispeed uint32 |
||||
Ospeed uint32 |
||||
} |
||||
|
||||
const ( |
||||
syscall_IGNBRK = 0x1 |
||||
syscall_BRKINT = 0x2 |
||||
syscall_PARMRK = 0x8 |
||||
syscall_ISTRIP = 0x20 |
||||
syscall_INLCR = 0x40 |
||||
syscall_IGNCR = 0x80 |
||||
syscall_ICRNL = 0x100 |
||||
syscall_IXON = 0x200 |
||||
syscall_OPOST = 0x1 |
||||
syscall_ECHO = 0x8 |
||||
syscall_ECHONL = 0x10 |
||||
syscall_ICANON = 0x100 |
||||
syscall_ISIG = 0x80 |
||||
syscall_IEXTEN = 0x400 |
||||
syscall_CSIZE = 0x300 |
||||
syscall_PARENB = 0x1000 |
||||
syscall_CS8 = 0x300 |
||||
syscall_VMIN = 0x10 |
||||
syscall_VTIME = 0x11 |
||||
|
||||
syscall_TCGETS = 0x402c7413 |
||||
syscall_TCSETS = 0x802c7414 |
||||
) |
@ -0,0 +1,40 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
type syscall_Termios struct { |
||||
Iflag uint64 |
||||
Oflag uint64 |
||||
Cflag uint64 |
||||
Lflag uint64 |
||||
Cc [20]uint8 |
||||
Pad_cgo_0 [4]byte |
||||
Ispeed uint64 |
||||
Ospeed uint64 |
||||
} |
||||
|
||||
const ( |
||||
syscall_IGNBRK = 0x1 |
||||
syscall_BRKINT = 0x2 |
||||
syscall_PARMRK = 0x8 |
||||
syscall_ISTRIP = 0x20 |
||||
syscall_INLCR = 0x40 |
||||
syscall_IGNCR = 0x80 |
||||
syscall_ICRNL = 0x100 |
||||
syscall_IXON = 0x200 |
||||
syscall_OPOST = 0x1 |
||||
syscall_ECHO = 0x8 |
||||
syscall_ECHONL = 0x10 |
||||
syscall_ICANON = 0x100 |
||||
syscall_ISIG = 0x80 |
||||
syscall_IEXTEN = 0x400 |
||||
syscall_CSIZE = 0x300 |
||||
syscall_PARENB = 0x1000 |
||||
syscall_CS8 = 0x300 |
||||
syscall_VMIN = 0x10 |
||||
syscall_VTIME = 0x11 |
||||
|
||||
syscall_TCGETS = 0x40487413 |
||||
syscall_TCSETS = 0x80487414 |
||||
) |
@ -0,0 +1,39 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
type syscall_Termios struct { |
||||
Iflag uint32 |
||||
Oflag uint32 |
||||
Cflag uint32 |
||||
Lflag uint32 |
||||
Cc [20]uint8 |
||||
Ispeed uint32 |
||||
Ospeed uint32 |
||||
} |
||||
|
||||
const ( |
||||
syscall_IGNBRK = 0x1 |
||||
syscall_BRKINT = 0x2 |
||||
syscall_PARMRK = 0x8 |
||||
syscall_ISTRIP = 0x20 |
||||
syscall_INLCR = 0x40 |
||||
syscall_IGNCR = 0x80 |
||||
syscall_ICRNL = 0x100 |
||||
syscall_IXON = 0x200 |
||||
syscall_OPOST = 0x1 |
||||
syscall_ECHO = 0x8 |
||||
syscall_ECHONL = 0x10 |
||||
syscall_ICANON = 0x100 |
||||
syscall_ISIG = 0x80 |
||||
syscall_IEXTEN = 0x400 |
||||
syscall_CSIZE = 0x300 |
||||
syscall_PARENB = 0x1000 |
||||
syscall_CS8 = 0x300 |
||||
syscall_VMIN = 0x10 |
||||
syscall_VTIME = 0x11 |
||||
|
||||
syscall_TCGETS = 0x402c7413 |
||||
syscall_TCSETS = 0x802c7414 |
||||
) |
@ -0,0 +1,33 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
import "syscall" |
||||
|
||||
type syscall_Termios syscall.Termios |
||||
|
||||
const ( |
||||
syscall_IGNBRK = syscall.IGNBRK |
||||
syscall_BRKINT = syscall.BRKINT |
||||
syscall_PARMRK = syscall.PARMRK |
||||
syscall_ISTRIP = syscall.ISTRIP |
||||
syscall_INLCR = syscall.INLCR |
||||
syscall_IGNCR = syscall.IGNCR |
||||
syscall_ICRNL = syscall.ICRNL |
||||
syscall_IXON = syscall.IXON |
||||
syscall_OPOST = syscall.OPOST |
||||
syscall_ECHO = syscall.ECHO |
||||
syscall_ECHONL = syscall.ECHONL |
||||
syscall_ICANON = syscall.ICANON |
||||
syscall_ISIG = syscall.ISIG |
||||
syscall_IEXTEN = syscall.IEXTEN |
||||
syscall_CSIZE = syscall.CSIZE |
||||
syscall_PARENB = syscall.PARENB |
||||
syscall_CS8 = syscall.CS8 |
||||
syscall_VMIN = syscall.VMIN |
||||
syscall_VTIME = syscall.VTIME |
||||
|
||||
syscall_TCGETS = syscall.TCGETS |
||||
syscall_TCSETS = syscall.TCSETS |
||||
) |
@ -0,0 +1,39 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
type syscall_Termios struct { |
||||
Iflag uint32 |
||||
Oflag uint32 |
||||
Cflag uint32 |
||||
Lflag uint32 |
||||
Cc [20]uint8 |
||||
Ispeed int32 |
||||
Ospeed int32 |
||||
} |
||||
|
||||
const ( |
||||
syscall_IGNBRK = 0x1 |
||||
syscall_BRKINT = 0x2 |
||||
syscall_PARMRK = 0x8 |
||||
syscall_ISTRIP = 0x20 |
||||
syscall_INLCR = 0x40 |
||||
syscall_IGNCR = 0x80 |
||||
syscall_ICRNL = 0x100 |
||||
syscall_IXON = 0x200 |
||||
syscall_OPOST = 0x1 |
||||
syscall_ECHO = 0x8 |
||||
syscall_ECHONL = 0x10 |
||||
syscall_ICANON = 0x100 |
||||
syscall_ISIG = 0x80 |
||||
syscall_IEXTEN = 0x400 |
||||
syscall_CSIZE = 0x300 |
||||
syscall_PARENB = 0x1000 |
||||
syscall_CS8 = 0x300 |
||||
syscall_VMIN = 0x10 |
||||
syscall_VTIME = 0x11 |
||||
|
||||
syscall_TCGETS = 0x402c7413 |
||||
syscall_TCSETS = 0x802c7414 |
||||
) |
@ -0,0 +1,39 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
type syscall_Termios struct { |
||||
Iflag uint32 |
||||
Oflag uint32 |
||||
Cflag uint32 |
||||
Lflag uint32 |
||||
Cc [20]uint8 |
||||
Ispeed int32 |
||||
Ospeed int32 |
||||
} |
||||
|
||||
const ( |
||||
syscall_IGNBRK = 0x1 |
||||
syscall_BRKINT = 0x2 |
||||
syscall_PARMRK = 0x8 |
||||
syscall_ISTRIP = 0x20 |
||||
syscall_INLCR = 0x40 |
||||
syscall_IGNCR = 0x80 |
||||
syscall_ICRNL = 0x100 |
||||
syscall_IXON = 0x200 |
||||
syscall_OPOST = 0x1 |
||||
syscall_ECHO = 0x8 |
||||
syscall_ECHONL = 0x10 |
||||
syscall_ICANON = 0x100 |
||||
syscall_ISIG = 0x80 |
||||
syscall_IEXTEN = 0x400 |
||||
syscall_CSIZE = 0x300 |
||||
syscall_PARENB = 0x1000 |
||||
syscall_CS8 = 0x300 |
||||
syscall_VMIN = 0x10 |
||||
syscall_VTIME = 0x11 |
||||
|
||||
syscall_TCGETS = 0x402c7413 |
||||
syscall_TCSETS = 0x802c7414 |
||||
) |
@ -0,0 +1,61 @@ |
||||
// Created by cgo -godefs - DO NOT EDIT
|
||||
// cgo -godefs -- -DUNICODE syscalls.go
|
||||
|
||||
package termbox |
||||
|
||||
const ( |
||||
foreground_blue = 0x1 |
||||
foreground_green = 0x2 |
||||
foreground_red = 0x4 |
||||
foreground_intensity = 0x8 |
||||
background_blue = 0x10 |
||||
background_green = 0x20 |
||||
background_red = 0x40 |
||||
background_intensity = 0x80 |
||||
std_input_handle = -0xa |
||||
std_output_handle = -0xb |
||||
key_event = 0x1 |
||||
mouse_event = 0x2 |
||||
window_buffer_size_event = 0x4 |
||||
enable_window_input = 0x8 |
||||
enable_mouse_input = 0x10 |
||||
enable_extended_flags = 0x80 |
||||
|
||||
vk_f1 = 0x70 |
||||
vk_f2 = 0x71 |
||||
vk_f3 = 0x72 |
||||
vk_f4 = 0x73 |
||||
vk_f5 = 0x74 |
||||
vk_f6 = 0x75 |
||||
vk_f7 = 0x76 |
||||
vk_f8 = 0x77 |
||||
vk_f9 = 0x78 |
||||
vk_f10 = 0x79 |
||||
vk_f11 = 0x7a |
||||
vk_f12 = 0x7b |
||||
vk_insert = 0x2d |
||||
vk_delete = 0x2e |
||||
vk_home = 0x24 |
||||
vk_end = 0x23 |
||||
vk_pgup = 0x21 |
||||
vk_pgdn = 0x22 |
||||
vk_arrow_up = 0x26 |
||||
vk_arrow_down = 0x28 |
||||
vk_arrow_left = 0x25 |
||||
vk_arrow_right = 0x27 |
||||
vk_backspace = 0x8 |
||||
vk_tab = 0x9 |
||||
vk_enter = 0xd |
||||
vk_esc = 0x1b |
||||
vk_space = 0x20 |
||||
|
||||
left_alt_pressed = 0x2 |
||||
left_ctrl_pressed = 0x8 |
||||
right_alt_pressed = 0x1 |
||||
right_ctrl_pressed = 0x4 |
||||
shift_pressed = 0x10 |
||||
|
||||
generic_read = 0x80000000 |
||||
generic_write = 0x40000000 |
||||
console_textmode_buffer = 0x1 |
||||
) |
@ -0,0 +1,407 @@ |
||||
// +build !windows
|
||||
|
||||
package termbox |
||||
|
||||
import "unicode/utf8" |
||||
import "bytes" |
||||
import "syscall" |
||||
import "unsafe" |
||||
import "strings" |
||||
import "strconv" |
||||
import "os" |
||||
import "io" |
||||
|
||||
// private API
|
||||
|
||||
const ( |
||||
t_enter_ca = iota |
||||
t_exit_ca |
||||
t_show_cursor |
||||
t_hide_cursor |
||||
t_clear_screen |
||||
t_sgr0 |
||||
t_underline |
||||
t_bold |
||||
t_blink |
||||
t_reverse |
||||
t_enter_keypad |
||||
t_exit_keypad |
||||
t_enter_mouse |
||||
t_exit_mouse |
||||
t_max_funcs |
||||
) |
||||
|
||||
const ( |
||||
coord_invalid = -2 |
||||
attr_invalid = Attribute(0xFFFF) |
||||
) |
||||
|
||||
type input_event struct { |
||||
data []byte |
||||
err error |
||||
} |
||||
|
||||
var ( |
||||
// term specific sequences
|
||||
keys []string |
||||
funcs []string |
||||
|
||||
// termbox inner state
|
||||
orig_tios syscall_Termios |
||||
back_buffer cellbuf |
||||
front_buffer cellbuf |
||||
termw int |
||||
termh int |
||||
input_mode = InputEsc |
||||
output_mode = OutputNormal |
||||
out *os.File |
||||
in int |
||||
lastfg = attr_invalid |
||||
lastbg = attr_invalid |
||||
lastx = coord_invalid |
||||
lasty = coord_invalid |
||||
cursor_x = cursor_hidden |
||||
cursor_y = cursor_hidden |
||||
foreground = ColorDefault |
||||
background = ColorDefault |
||||
inbuf = make([]byte, 0, 64) |
||||
outbuf bytes.Buffer |
||||
sigwinch = make(chan os.Signal, 1) |
||||
sigio = make(chan os.Signal, 1) |
||||
quit = make(chan int) |
||||
input_comm = make(chan input_event) |
||||
interrupt_comm = make(chan struct{}) |
||||
intbuf = make([]byte, 0, 16) |
||||
) |
||||
|
||||
func write_cursor(x, y int) { |
||||
outbuf.WriteString("\033[") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10)) |
||||
outbuf.WriteString(";") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10)) |
||||
outbuf.WriteString("H") |
||||
} |
||||
|
||||
func write_sgr_fg(a Attribute) { |
||||
switch output_mode { |
||||
case Output256, Output216, OutputGrayscale: |
||||
outbuf.WriteString("\033[38;5;") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) |
||||
outbuf.WriteString("m") |
||||
default: |
||||
outbuf.WriteString("\033[3") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) |
||||
outbuf.WriteString("m") |
||||
} |
||||
} |
||||
|
||||
func write_sgr_bg(a Attribute) { |
||||
switch output_mode { |
||||
case Output256, Output216, OutputGrayscale: |
||||
outbuf.WriteString("\033[48;5;") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) |
||||
outbuf.WriteString("m") |
||||
default: |
||||
outbuf.WriteString("\033[4") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) |
||||
outbuf.WriteString("m") |
||||
} |
||||
} |
||||
|
||||
func write_sgr(fg, bg Attribute) { |
||||
switch output_mode { |
||||
case Output256, Output216, OutputGrayscale: |
||||
outbuf.WriteString("\033[38;5;") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10)) |
||||
outbuf.WriteString("m") |
||||
outbuf.WriteString("\033[48;5;") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10)) |
||||
outbuf.WriteString("m") |
||||
default: |
||||
outbuf.WriteString("\033[3") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10)) |
||||
outbuf.WriteString(";4") |
||||
outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10)) |
||||
outbuf.WriteString("m") |
||||
} |
||||
} |
||||
|
||||
type winsize struct { |
||||
rows uint16 |
||||
cols uint16 |
||||
xpixels uint16 |
||||
ypixels uint16 |
||||
} |
||||
|
||||
func get_term_size(fd uintptr) (int, int) { |
||||
var sz winsize |
||||
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, |
||||
fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) |
||||
return int(sz.cols), int(sz.rows) |
||||
} |
||||
|
||||
func send_attr(fg, bg Attribute) { |
||||
if fg == lastfg && bg == lastbg { |
||||
return |
||||
} |
||||
|
||||
outbuf.WriteString(funcs[t_sgr0]) |
||||
|
||||
var fgcol, bgcol Attribute |
||||
|
||||
switch output_mode { |
||||
case Output256: |
||||
fgcol = fg & 0x1FF |
||||
bgcol = bg & 0x1FF |
||||
case Output216: |
||||
fgcol = fg & 0xFF |
||||
bgcol = bg & 0xFF |
||||
if fgcol > 216 { |
||||
fgcol = ColorDefault |
||||
} |
||||
if bgcol > 216 { |
||||
bgcol = ColorDefault |
||||
} |
||||
if fgcol != ColorDefault { |
||||
fgcol += 0x10 |
||||
} |
||||
if bgcol != ColorDefault { |
||||
bgcol += 0x10 |
||||
} |
||||
case OutputGrayscale: |
||||
fgcol = fg & 0x1F |
||||
bgcol = bg & 0x1F |
||||
if fgcol > 24 { |
||||
fgcol = ColorDefault |
||||
} |
||||
if bgcol > 24 { |
||||
bgcol = ColorDefault |
||||
} |
||||
if fgcol != ColorDefault { |
||||
fgcol += 0xe8 |
||||
} |
||||
if bgcol != ColorDefault { |
||||
bgcol += 0xe8 |
||||
} |
||||
default: |
||||
fgcol = fg & 0x0F |
||||
bgcol = bg & 0x0F |
||||
} |
||||
|
||||
if fgcol != ColorDefault { |
||||
if bgcol != ColorDefault { |
||||
write_sgr(fgcol, bgcol) |
||||
} else { |
||||
write_sgr_fg(fgcol) |
||||
} |
||||
} else if bgcol != ColorDefault { |
||||
write_sgr_bg(bgcol) |
||||
} |
||||
|
||||
if fg&AttrBold != 0 { |
||||
outbuf.WriteString(funcs[t_bold]) |
||||
} |
||||
if bg&AttrBold != 0 { |
||||
outbuf.WriteString(funcs[t_blink]) |
||||
} |
||||
if fg&AttrUnderline != 0 { |
||||
outbuf.WriteString(funcs[t_underline]) |
||||
} |
||||
if fg&AttrReverse|bg&AttrReverse != 0 { |
||||
outbuf.WriteString(funcs[t_reverse]) |
||||
} |
||||
|
||||
lastfg, lastbg = fg, bg |
||||
} |
||||
|
||||
func send_char(x, y int, ch rune) { |
||||
var buf [8]byte |
||||
n := utf8.EncodeRune(buf[:], ch) |
||||
if x-1 != lastx || y != lasty { |
||||
write_cursor(x, y) |
||||
} |
||||
lastx, lasty = x, y |
||||
outbuf.Write(buf[:n]) |
||||
} |
||||
|
||||
func flush() error { |
||||
_, err := io.Copy(out, &outbuf) |
||||
outbuf.Reset() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func send_clear() error { |
||||
send_attr(foreground, background) |
||||
outbuf.WriteString(funcs[t_clear_screen]) |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
write_cursor(cursor_x, cursor_y) |
||||
} |
||||
|
||||
// we need to invalidate cursor position too and these two vars are
|
||||
// used only for simple cursor positioning optimization, cursor
|
||||
// actually may be in the correct place, but we simply discard
|
||||
// optimization once and it gives us simple solution for the case when
|
||||
// cursor moved
|
||||
lastx = coord_invalid |
||||
lasty = coord_invalid |
||||
|
||||
return flush() |
||||
} |
||||
|
||||
func update_size_maybe() error { |
||||
w, h := get_term_size(out.Fd()) |
||||
if w != termw || h != termh { |
||||
termw, termh = w, h |
||||
back_buffer.resize(termw, termh) |
||||
front_buffer.resize(termw, termh) |
||||
front_buffer.clear() |
||||
return send_clear() |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func tcsetattr(fd uintptr, termios *syscall_Termios) error { |
||||
r, _, e := syscall.Syscall(syscall.SYS_IOCTL, |
||||
fd, uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(termios))) |
||||
if r != 0 { |
||||
return os.NewSyscallError("SYS_IOCTL", e) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func tcgetattr(fd uintptr, termios *syscall_Termios) error { |
||||
r, _, e := syscall.Syscall(syscall.SYS_IOCTL, |
||||
fd, uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(termios))) |
||||
if r != 0 { |
||||
return os.NewSyscallError("SYS_IOCTL", e) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func parse_escape_sequence(event *Event, buf []byte) (int, bool) { |
||||
bufstr := string(buf) |
||||
// mouse
|
||||
if len(bufstr) >= 6 && strings.HasPrefix(bufstr, "\033[M") { |
||||
switch buf[3] & 3 { |
||||
case 0: |
||||
event.Key = MouseLeft |
||||
case 1: |
||||
event.Key = MouseMiddle |
||||
case 2: |
||||
event.Key = MouseRight |
||||
case 3: |
||||
return 6, false |
||||
} |
||||
event.Type = EventMouse // KeyEvent by default
|
||||
// wheel up outputs MouseLeft
|
||||
if buf[3] == 0x60 || buf[3] == 0x70 { |
||||
event.Key = MouseMiddle |
||||
} |
||||
// the coord is 1,1 for upper left
|
||||
event.MouseX = int(buf[4]) - 1 - 32 |
||||
event.MouseY = int(buf[5]) - 1 - 32 |
||||
return 6, true |
||||
} |
||||
|
||||
for i, key := range keys { |
||||
if strings.HasPrefix(bufstr, key) { |
||||
event.Ch = 0 |
||||
event.Key = Key(0xFFFF - i) |
||||
return len(key), true |
||||
} |
||||
} |
||||
return 0, true |
||||
} |
||||
|
||||
func extract_raw_event(data []byte, event *Event) bool { |
||||
if len(inbuf) == 0 { |
||||
return false |
||||
} |
||||
|
||||
n := len(data) |
||||
if n == 0 { |
||||
return false |
||||
} |
||||
|
||||
n = copy(data, inbuf) |
||||
copy(inbuf, inbuf[n:]) |
||||
inbuf = inbuf[:len(inbuf)-n] |
||||
|
||||
event.N = n |
||||
event.Type = EventRaw |
||||
return true |
||||
} |
||||
|
||||
func extract_event(inbuf []byte, event *Event) bool { |
||||
if len(inbuf) == 0 { |
||||
event.N = 0 |
||||
return false |
||||
} |
||||
|
||||
if inbuf[0] == '\033' { |
||||
// possible escape sequence
|
||||
n, ok := parse_escape_sequence(event, inbuf) |
||||
if n != 0 { |
||||
event.N = n |
||||
return ok |
||||
} |
||||
|
||||
// it's not escape sequence, then it's Alt or Esc, check input_mode
|
||||
switch { |
||||
case input_mode&InputEsc != 0: |
||||
// if we're in escape mode, fill Esc event, pop buffer, return success
|
||||
event.Ch = 0 |
||||
event.Key = KeyEsc |
||||
event.Mod = 0 |
||||
event.N = 1 |
||||
return true |
||||
case input_mode&InputAlt != 0: |
||||
// if we're in alt mode, set Alt modifier to event and redo parsing
|
||||
event.Mod = ModAlt |
||||
ok := extract_event(inbuf[1:], event) |
||||
if ok { |
||||
event.N++ |
||||
} else { |
||||
event.N = 0 |
||||
} |
||||
return ok |
||||
default: |
||||
panic("unreachable") |
||||
} |
||||
} |
||||
|
||||
// if we're here, this is not an escape sequence and not an alt sequence
|
||||
// so, it's a FUNCTIONAL KEY or a UNICODE character
|
||||
|
||||
// first of all check if it's a functional key
|
||||
if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 { |
||||
// fill event, pop buffer, return success
|
||||
event.Ch = 0 |
||||
event.Key = Key(inbuf[0]) |
||||
event.N = 1 |
||||
return true |
||||
} |
||||
|
||||
// the only possible option is utf8 rune
|
||||
if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError { |
||||
event.Ch = r |
||||
event.Key = 0 |
||||
event.N = n |
||||
return true |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func fcntl(fd int, cmd int, arg int) (val int, err error) { |
||||
r, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd), |
||||
uintptr(arg)) |
||||
val = int(r) |
||||
if e != 0 { |
||||
err = e |
||||
} |
||||
return |
||||
} |