Merge pull request #1321 from karalabe/cut-it-open-3000
Metrics collecting and reporting supportpull/1341/head
@ -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 |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -0,0 +1,59 @@ |
||||
package termbox |
||||
|
||||
// private API, common OS agnostic part
|
||||
|
||||
type cellbuf struct { |
||||
width int |
||||
height int |
||||
cells []Cell |
||||
} |
||||
|
||||
func (this *cellbuf) init(width, height int) { |
||||
this.width = width |
||||
this.height = height |
||||
this.cells = make([]Cell, width*height) |
||||
} |
||||
|
||||
func (this *cellbuf) resize(width, height int) { |
||||
if this.width == width && this.height == height { |
||||
return |
||||
} |
||||
|
||||
oldw := this.width |
||||
oldh := this.height |
||||
oldcells := this.cells |
||||
|
||||
this.init(width, height) |
||||
this.clear() |
||||
|
||||
minw, minh := oldw, oldh |
||||
|
||||
if width < minw { |
||||
minw = width |
||||
} |
||||
if height < minh { |
||||
minh = height |
||||
} |
||||
|
||||
for i := 0; i < minh; i++ { |
||||
srco, dsto := i*oldw, i*width |
||||
src := oldcells[srco : srco+minw] |
||||
dst := this.cells[dsto : dsto+minw] |
||||
copy(dst, src) |
||||
} |
||||
} |
||||
|
||||
func (this *cellbuf) clear() { |
||||
for i := range this.cells { |
||||
c := &this.cells[i] |
||||
c.Ch = ' ' |
||||
c.Fg = foreground |
||||
c.Bg = background |
||||
} |
||||
} |
||||
|
||||
const cursor_hidden = -1 |
||||
|
||||
func is_cursor_hidden(x, y int) bool { |
||||
return x == cursor_hidden || y == cursor_hidden |
||||
} |
@ -0,0 +1,813 @@ |
||||
package termbox |
||||
|
||||
import "syscall" |
||||
import "unsafe" |
||||
import "unicode/utf16" |
||||
import "github.com/mattn/go-runewidth" |
||||
|
||||
type ( |
||||
wchar uint16 |
||||
short int16 |
||||
dword uint32 |
||||
word uint16 |
||||
char_info struct { |
||||
char wchar |
||||
attr word |
||||
} |
||||
coord struct { |
||||
x short |
||||
y short |
||||
} |
||||
small_rect struct { |
||||
left short |
||||
top short |
||||
right short |
||||
bottom short |
||||
} |
||||
console_screen_buffer_info struct { |
||||
size coord |
||||
cursor_position coord |
||||
attributes word |
||||
window small_rect |
||||
maximum_window_size coord |
||||
} |
||||
console_cursor_info struct { |
||||
size dword |
||||
visible int32 |
||||
} |
||||
input_record struct { |
||||
event_type word |
||||
_ [2]byte |
||||
event [16]byte |
||||
} |
||||
key_event_record struct { |
||||
key_down int32 |
||||
repeat_count word |
||||
virtual_key_code word |
||||
virtual_scan_code word |
||||
unicode_char wchar |
||||
control_key_state dword |
||||
} |
||||
window_buffer_size_record struct { |
||||
size coord |
||||
} |
||||
mouse_event_record struct { |
||||
mouse_pos coord |
||||
button_state dword |
||||
control_key_state dword |
||||
event_flags dword |
||||
} |
||||
) |
||||
|
||||
const ( |
||||
mouse_lmb = 0x1 |
||||
mouse_rmb = 0x2 |
||||
mouse_mmb = 0x4 | 0x8 | 0x10 |
||||
) |
||||
|
||||
func (this coord) uintptr() uintptr { |
||||
return uintptr(*(*int32)(unsafe.Pointer(&this))) |
||||
} |
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll") |
||||
var is_cjk = runewidth.IsEastAsian() |
||||
|
||||
var ( |
||||
proc_set_console_active_screen_buffer = kernel32.NewProc("SetConsoleActiveScreenBuffer") |
||||
proc_set_console_screen_buffer_size = kernel32.NewProc("SetConsoleScreenBufferSize") |
||||
proc_create_console_screen_buffer = kernel32.NewProc("CreateConsoleScreenBuffer") |
||||
proc_get_console_screen_buffer_info = kernel32.NewProc("GetConsoleScreenBufferInfo") |
||||
proc_write_console_output = kernel32.NewProc("WriteConsoleOutputW") |
||||
proc_write_console_output_character = kernel32.NewProc("WriteConsoleOutputCharacterW") |
||||
proc_write_console_output_attribute = kernel32.NewProc("WriteConsoleOutputAttribute") |
||||
proc_set_console_cursor_info = kernel32.NewProc("SetConsoleCursorInfo") |
||||
proc_set_console_cursor_position = kernel32.NewProc("SetConsoleCursorPosition") |
||||
proc_get_console_cursor_info = kernel32.NewProc("GetConsoleCursorInfo") |
||||
proc_read_console_input = kernel32.NewProc("ReadConsoleInputW") |
||||
proc_get_console_mode = kernel32.NewProc("GetConsoleMode") |
||||
proc_set_console_mode = kernel32.NewProc("SetConsoleMode") |
||||
proc_fill_console_output_character = kernel32.NewProc("FillConsoleOutputCharacterW") |
||||
proc_fill_console_output_attribute = kernel32.NewProc("FillConsoleOutputAttribute") |
||||
proc_create_event = kernel32.NewProc("CreateEventW") |
||||
proc_wait_for_multiple_objects = kernel32.NewProc("WaitForMultipleObjects") |
||||
proc_set_event = kernel32.NewProc("SetEvent") |
||||
) |
||||
|
||||
func set_console_active_screen_buffer(h syscall.Handle) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_console_active_screen_buffer.Addr(), |
||||
1, uintptr(h), 0, 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func set_console_screen_buffer_size(h syscall.Handle, size coord) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_console_screen_buffer_size.Addr(), |
||||
2, uintptr(h), size.uintptr(), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func create_console_screen_buffer() (h syscall.Handle, err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_create_console_screen_buffer.Addr(), |
||||
5, uintptr(generic_read|generic_write), 0, 0, console_textmode_buffer, 0, 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return syscall.Handle(r0), nil |
||||
} |
||||
|
||||
func get_console_screen_buffer_info(h syscall.Handle, info *console_screen_buffer_info) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_get_console_screen_buffer_info.Addr(), |
||||
2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func write_console_output(h syscall.Handle, chars []char_info, dst small_rect) (err error) { |
||||
tmp_coord = coord{dst.right - dst.left + 1, dst.bottom - dst.top + 1} |
||||
tmp_rect = dst |
||||
r0, _, e1 := syscall.Syscall6(proc_write_console_output.Addr(), |
||||
5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), tmp_coord.uintptr(), |
||||
tmp_coord0.uintptr(), uintptr(unsafe.Pointer(&tmp_rect)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func write_console_output_character(h syscall.Handle, chars []wchar, pos coord) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_write_console_output_character.Addr(), |
||||
5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), uintptr(len(chars)), |
||||
pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func write_console_output_attribute(h syscall.Handle, attrs []word, pos coord) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_write_console_output_attribute.Addr(), |
||||
5, uintptr(h), uintptr(unsafe.Pointer(&attrs[0])), uintptr(len(attrs)), |
||||
pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func set_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_console_cursor_info.Addr(), |
||||
2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func get_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_get_console_cursor_info.Addr(), |
||||
2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func set_console_cursor_position(h syscall.Handle, pos coord) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_console_cursor_position.Addr(), |
||||
2, uintptr(h), pos.uintptr(), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func read_console_input(h syscall.Handle, record *input_record) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_read_console_input.Addr(), |
||||
4, uintptr(h), uintptr(unsafe.Pointer(record)), 1, uintptr(unsafe.Pointer(&tmp_arg)), 0, 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func get_console_mode(h syscall.Handle, mode *dword) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_get_console_mode.Addr(), |
||||
2, uintptr(h), uintptr(unsafe.Pointer(mode)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func set_console_mode(h syscall.Handle, mode dword) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_console_mode.Addr(), |
||||
2, uintptr(h), uintptr(mode), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func fill_console_output_character(h syscall.Handle, char wchar, n int) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_fill_console_output_character.Addr(), |
||||
5, uintptr(h), uintptr(char), uintptr(n), tmp_coord.uintptr(), |
||||
uintptr(unsafe.Pointer(&tmp_arg)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func fill_console_output_attribute(h syscall.Handle, attr word, n int) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_fill_console_output_attribute.Addr(), |
||||
5, uintptr(h), uintptr(attr), uintptr(n), tmp_coord.uintptr(), |
||||
uintptr(unsafe.Pointer(&tmp_arg)), 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func create_event() (out syscall.Handle, err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_create_event.Addr(), |
||||
4, 0, 0, 0, 0, 0, 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return syscall.Handle(r0), nil |
||||
} |
||||
|
||||
func wait_for_multiple_objects(objects []syscall.Handle) (err error) { |
||||
r0, _, e1 := syscall.Syscall6(proc_wait_for_multiple_objects.Addr(), |
||||
4, uintptr(len(objects)), uintptr(unsafe.Pointer(&objects[0])), |
||||
0, 0xFFFFFFFF, 0, 0) |
||||
if uint32(r0) == 0xFFFFFFFF { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func set_event(ev syscall.Handle) (err error) { |
||||
r0, _, e1 := syscall.Syscall(proc_set_event.Addr(), |
||||
1, uintptr(ev), 0, 0) |
||||
if int(r0) == 0 { |
||||
if e1 != 0 { |
||||
err = error(e1) |
||||
} else { |
||||
err = syscall.EINVAL |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
type diff_msg struct { |
||||
pos short |
||||
lines short |
||||
chars []char_info |
||||
} |
||||
|
||||
type input_event struct { |
||||
event Event |
||||
err error |
||||
} |
||||
|
||||
var ( |
||||
orig_cursor_info console_cursor_info |
||||
orig_size coord |
||||
orig_mode dword |
||||
orig_screen syscall.Handle |
||||
back_buffer cellbuf |
||||
front_buffer cellbuf |
||||
term_size coord |
||||
input_mode = InputEsc |
||||
cursor_x = cursor_hidden |
||||
cursor_y = cursor_hidden |
||||
foreground = ColorDefault |
||||
background = ColorDefault |
||||
in syscall.Handle |
||||
out syscall.Handle |
||||
interrupt syscall.Handle |
||||
charbuf []char_info |
||||
diffbuf []diff_msg |
||||
beg_x = -1 |
||||
beg_y = -1 |
||||
beg_i = -1 |
||||
input_comm = make(chan Event) |
||||
interrupt_comm = make(chan struct{}) |
||||
cancel_comm = make(chan bool, 1) |
||||
cancel_done_comm = make(chan bool) |
||||
alt_mode_esc = false |
||||
|
||||
// these ones just to prevent heap allocs at all costs
|
||||
tmp_info console_screen_buffer_info |
||||
tmp_arg dword |
||||
tmp_coord0 = coord{0, 0} |
||||
tmp_coord = coord{0, 0} |
||||
tmp_rect = small_rect{0, 0, 0, 0} |
||||
) |
||||
|
||||
func get_cursor_position(out syscall.Handle) coord { |
||||
err := get_console_screen_buffer_info(out, &tmp_info) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return tmp_info.cursor_position |
||||
} |
||||
|
||||
func get_term_size(out syscall.Handle) coord { |
||||
err := get_console_screen_buffer_info(out, &tmp_info) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return tmp_info.size |
||||
} |
||||
|
||||
func get_win_size(out syscall.Handle) coord { |
||||
err := get_console_screen_buffer_info(out, &tmp_info) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
return coord{ |
||||
x: tmp_info.window.right - tmp_info.window.left + 1, |
||||
y: tmp_info.window.bottom - tmp_info.window.top + 1, |
||||
} |
||||
} |
||||
|
||||
func update_size_maybe() { |
||||
size := get_term_size(out) |
||||
if size.x != term_size.x || size.y != term_size.y { |
||||
term_size = size |
||||
back_buffer.resize(int(size.x), int(size.y)) |
||||
front_buffer.resize(int(size.x), int(size.y)) |
||||
front_buffer.clear() |
||||
clear() |
||||
|
||||
area := int(size.x) * int(size.y) |
||||
if cap(charbuf) < area { |
||||
charbuf = make([]char_info, 0, area) |
||||
} |
||||
} |
||||
} |
||||
|
||||
var color_table_bg = []word{ |
||||
0, // default (black)
|
||||
0, // black
|
||||
background_red, |
||||
background_green, |
||||
background_red | background_green, // yellow
|
||||
background_blue, |
||||
background_red | background_blue, // magenta
|
||||
background_green | background_blue, // cyan
|
||||
background_red | background_blue | background_green, // white
|
||||
} |
||||
|
||||
var color_table_fg = []word{ |
||||
foreground_red | foreground_blue | foreground_green, // default (white)
|
||||
0, |
||||
foreground_red, |
||||
foreground_green, |
||||
foreground_red | foreground_green, // yellow
|
||||
foreground_blue, |
||||
foreground_red | foreground_blue, // magenta
|
||||
foreground_green | foreground_blue, // cyan
|
||||
foreground_red | foreground_blue | foreground_green, // white
|
||||
} |
||||
|
||||
const ( |
||||
replacement_char = '\uFFFD' |
||||
max_rune = '\U0010FFFF' |
||||
surr1 = 0xd800 |
||||
surr2 = 0xdc00 |
||||
surr3 = 0xe000 |
||||
surr_self = 0x10000 |
||||
) |
||||
|
||||
func append_diff_line(y int) int { |
||||
n := 0 |
||||
for x := 0; x < front_buffer.width; { |
||||
cell_offset := y*front_buffer.width + x |
||||
back := &back_buffer.cells[cell_offset] |
||||
front := &front_buffer.cells[cell_offset] |
||||
attr, char := cell_to_char_info(*back) |
||||
charbuf = append(charbuf, char_info{attr: attr, char: char[0]}) |
||||
*front = *back |
||||
n++ |
||||
w := runewidth.RuneWidth(back.Ch) |
||||
if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { |
||||
w = 1 |
||||
} |
||||
x += w |
||||
// If not CJK, fill trailing space with whitespace
|
||||
if !is_cjk && w == 2 { |
||||
charbuf = append(charbuf, char_info{attr: attr, char: ' '}) |
||||
} |
||||
} |
||||
return n |
||||
} |
||||
|
||||
// compares 'back_buffer' with 'front_buffer' and prepares all changes in the form of
|
||||
// 'diff_msg's in the 'diff_buf'
|
||||
func prepare_diff_messages() { |
||||
// clear buffers
|
||||
diffbuf = diffbuf[:0] |
||||
charbuf = charbuf[:0] |
||||
|
||||
var diff diff_msg |
||||
gbeg := 0 |
||||
for y := 0; y < front_buffer.height; y++ { |
||||
same := true |
||||
line_offset := y * front_buffer.width |
||||
for x := 0; x < front_buffer.width; x++ { |
||||
cell_offset := line_offset + x |
||||
back := &back_buffer.cells[cell_offset] |
||||
front := &front_buffer.cells[cell_offset] |
||||
if *back != *front { |
||||
same = false |
||||
break |
||||
} |
||||
} |
||||
if same && diff.lines > 0 { |
||||
diffbuf = append(diffbuf, diff) |
||||
diff = diff_msg{} |
||||
} |
||||
if !same { |
||||
beg := len(charbuf) |
||||
end := beg + append_diff_line(y) |
||||
if diff.lines == 0 { |
||||
diff.pos = short(y) |
||||
gbeg = beg |
||||
} |
||||
diff.lines++ |
||||
diff.chars = charbuf[gbeg:end] |
||||
} |
||||
} |
||||
if diff.lines > 0 { |
||||
diffbuf = append(diffbuf, diff) |
||||
diff = diff_msg{} |
||||
} |
||||
} |
||||
|
||||
func cell_to_char_info(c Cell) (attr word, wc [2]wchar) { |
||||
attr = color_table_fg[c.Fg&0x0F] | color_table_bg[c.Bg&0x0F] |
||||
if c.Fg&AttrReverse|c.Bg&AttrReverse != 0 { |
||||
attr = (attr&0xF0)>>4 | (attr&0x0F)<<4 |
||||
} |
||||
if c.Fg&AttrBold != 0 { |
||||
attr |= foreground_intensity |
||||
} |
||||
if c.Bg&AttrBold != 0 { |
||||
attr |= background_intensity |
||||
} |
||||
|
||||
r0, r1 := utf16.EncodeRune(c.Ch) |
||||
if r0 == 0xFFFD { |
||||
wc[0] = wchar(c.Ch) |
||||
wc[1] = ' ' |
||||
} else { |
||||
wc[0] = wchar(r0) |
||||
wc[1] = wchar(r1) |
||||
} |
||||
return |
||||
} |
||||
|
||||
func move_cursor(x, y int) { |
||||
err := set_console_cursor_position(out, coord{short(x), short(y)}) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func show_cursor(visible bool) { |
||||
var v int32 |
||||
if visible { |
||||
v = 1 |
||||
} |
||||
|
||||
var info console_cursor_info |
||||
info.size = 100 |
||||
info.visible = v |
||||
err := set_console_cursor_info(out, &info) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
func clear() { |
||||
var err error |
||||
attr, char := cell_to_char_info(Cell{ |
||||
' ', |
||||
foreground, |
||||
background, |
||||
}) |
||||
|
||||
area := int(term_size.x) * int(term_size.y) |
||||
err = fill_console_output_attribute(out, attr, area) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
err = fill_console_output_character(out, char[0], area) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
if !is_cursor_hidden(cursor_x, cursor_y) { |
||||
move_cursor(cursor_x, cursor_y) |
||||
} |
||||
} |
||||
|
||||
func key_event_record_to_event(r *key_event_record) (Event, bool) { |
||||
if r.key_down == 0 { |
||||
return Event{}, false |
||||
} |
||||
|
||||
e := Event{Type: EventKey} |
||||
if input_mode&InputAlt != 0 { |
||||
if alt_mode_esc { |
||||
e.Mod = ModAlt |
||||
alt_mode_esc = false |
||||
} |
||||
if r.control_key_state&(left_alt_pressed|right_alt_pressed) != 0 { |
||||
e.Mod = ModAlt |
||||
} |
||||
} |
||||
|
||||
ctrlpressed := r.control_key_state&(left_ctrl_pressed|right_ctrl_pressed) != 0 |
||||
|
||||
if r.virtual_key_code >= vk_f1 && r.virtual_key_code <= vk_f12 { |
||||
switch r.virtual_key_code { |
||||
case vk_f1: |
||||
e.Key = KeyF1 |
||||
case vk_f2: |
||||
e.Key = KeyF2 |
||||
case vk_f3: |
||||
e.Key = KeyF3 |
||||
case vk_f4: |
||||
e.Key = KeyF4 |
||||
case vk_f5: |
||||
e.Key = KeyF5 |
||||
case vk_f6: |
||||
e.Key = KeyF6 |
||||
case vk_f7: |
||||
e.Key = KeyF7 |
||||
case vk_f8: |
||||
e.Key = KeyF8 |
||||
case vk_f9: |
||||
e.Key = KeyF9 |
||||
case vk_f10: |
||||
e.Key = KeyF10 |
||||
case vk_f11: |
||||
e.Key = KeyF11 |
||||
case vk_f12: |
||||
e.Key = KeyF12 |
||||
default: |
||||
panic("unreachable") |
||||
} |
||||
|
||||
return e, true |
||||
} |
||||
|
||||
if r.virtual_key_code <= vk_delete { |
||||
switch r.virtual_key_code { |
||||
case vk_insert: |
||||
e.Key = KeyInsert |
||||
case vk_delete: |
||||
e.Key = KeyDelete |
||||
case vk_home: |
||||
e.Key = KeyHome |
||||
case vk_end: |
||||
e.Key = KeyEnd |
||||
case vk_pgup: |
||||
e.Key = KeyPgup |
||||
case vk_pgdn: |
||||
e.Key = KeyPgdn |
||||
case vk_arrow_up: |
||||
e.Key = KeyArrowUp |
||||
case vk_arrow_down: |
||||
e.Key = KeyArrowDown |
||||
case vk_arrow_left: |
||||
e.Key = KeyArrowLeft |
||||
case vk_arrow_right: |
||||
e.Key = KeyArrowRight |
||||
case vk_backspace: |
||||
if ctrlpressed { |
||||
e.Key = KeyBackspace2 |
||||
} else { |
||||
e.Key = KeyBackspace |
||||
} |
||||
case vk_tab: |
||||
e.Key = KeyTab |
||||
case vk_enter: |
||||
e.Key = KeyEnter |
||||
case vk_esc: |
||||
switch { |
||||
case input_mode&InputEsc != 0: |
||||
e.Key = KeyEsc |
||||
case input_mode&InputAlt != 0: |
||||
alt_mode_esc = true |
||||
return Event{}, false |
||||
} |
||||
case vk_space: |
||||
if ctrlpressed { |
||||
// manual return here, because KeyCtrlSpace is zero
|
||||
e.Key = KeyCtrlSpace |
||||
return e, true |
||||
} else { |
||||
e.Key = KeySpace |
||||
} |
||||
} |
||||
|
||||
if e.Key != 0 { |
||||
return e, true |
||||
} |
||||
} |
||||
|
||||
if ctrlpressed { |
||||
if Key(r.unicode_char) >= KeyCtrlA && Key(r.unicode_char) <= KeyCtrlRsqBracket { |
||||
e.Key = Key(r.unicode_char) |
||||
if input_mode&InputAlt != 0 && e.Key == KeyEsc { |
||||
alt_mode_esc = true |
||||
return Event{}, false |
||||
} |
||||
return e, true |
||||
} |
||||
switch r.virtual_key_code { |
||||
case 192, 50: |
||||
// manual return here, because KeyCtrl2 is zero
|
||||
e.Key = KeyCtrl2 |
||||
return e, true |
||||
case 51: |
||||
if input_mode&InputAlt != 0 { |
||||
alt_mode_esc = true |
||||
return Event{}, false |
||||
} |
||||
e.Key = KeyCtrl3 |
||||
case 52: |
||||
e.Key = KeyCtrl4 |
||||
case 53: |
||||
e.Key = KeyCtrl5 |
||||
case 54: |
||||
e.Key = KeyCtrl6 |
||||
case 189, 191, 55: |
||||
e.Key = KeyCtrl7 |
||||
case 8, 56: |
||||
e.Key = KeyCtrl8 |
||||
} |
||||
|
||||
if e.Key != 0 { |
||||
return e, true |
||||
} |
||||
} |
||||
|
||||
if r.unicode_char != 0 { |
||||
e.Ch = rune(r.unicode_char) |
||||
return e, true |
||||
} |
||||
|
||||
return Event{}, false |
||||
} |
||||
|
||||
func input_event_producer() { |
||||
var r input_record |
||||
var err error |
||||
var last_button Key |
||||
var last_state = dword(0) |
||||
handles := []syscall.Handle{in, interrupt} |
||||
for { |
||||
err = wait_for_multiple_objects(handles) |
||||
if err != nil { |
||||
input_comm <- Event{Type: EventError, Err: err} |
||||
} |
||||
|
||||
select { |
||||
case <-cancel_comm: |
||||
cancel_done_comm <- true |
||||
return |
||||
default: |
||||
} |
||||
|
||||
err = read_console_input(in, &r) |
||||
if err != nil { |
||||
input_comm <- Event{Type: EventError, Err: err} |
||||
} |
||||
|
||||
switch r.event_type { |
||||
case key_event: |
||||
kr := (*key_event_record)(unsafe.Pointer(&r.event)) |
||||
ev, ok := key_event_record_to_event(kr) |
||||
if ok { |
||||
for i := 0; i < int(kr.repeat_count); i++ { |
||||
input_comm <- ev |
||||
} |
||||
} |
||||
case window_buffer_size_event: |
||||
sr := *(*window_buffer_size_record)(unsafe.Pointer(&r.event)) |
||||
input_comm <- Event{ |
||||
Type: EventResize, |
||||
Width: int(sr.size.x), |
||||
Height: int(sr.size.y), |
||||
} |
||||
case mouse_event: |
||||
mr := *(*mouse_event_record)(unsafe.Pointer(&r.event)) |
||||
|
||||
// single or double click
|
||||
switch mr.event_flags { |
||||
case 0: |
||||
cur_state := mr.button_state |
||||
switch { |
||||
case last_state&mouse_lmb == 0 && cur_state&mouse_lmb != 0: |
||||
last_button = MouseLeft |
||||
case last_state&mouse_rmb == 0 && cur_state&mouse_rmb != 0: |
||||
last_button = MouseRight |
||||
case last_state&mouse_mmb == 0 && cur_state&mouse_mmb != 0: |
||||
last_button = MouseMiddle |
||||
default: |
||||
last_state = cur_state |
||||
continue |
||||
} |
||||
last_state = cur_state |
||||
fallthrough |
||||
case 2: |
||||
input_comm <- Event{ |
||||
Type: EventMouse, |
||||
Key: last_button, |
||||
MouseX: int(mr.mouse_pos.x), |
||||
MouseY: int(mr.mouse_pos.y), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,219 @@ |
||||
// +build !windows
|
||||
// This file contains a simple and incomplete implementation of the terminfo
|
||||
// database. Information was taken from the ncurses manpages term(5) and
|
||||
// terminfo(5). Currently, only the string capabilities for special keys and for
|
||||
// functions without parameters are actually used. Colors are still done with
|
||||
// ANSI escape sequences. Other special features that are not (yet?) supported
|
||||
// are reading from ~/.terminfo, the TERMINFO_DIRS variable, Berkeley database
|
||||
// format and extended capabilities.
|
||||
|
||||
package termbox |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/binary" |
||||
"encoding/hex" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"os" |
||||
"strings" |
||||
) |
||||
|
||||
const ( |
||||
ti_magic = 0432 |
||||
ti_header_length = 12 |
||||
) |
||||
|
||||
func load_terminfo() ([]byte, error) { |
||||
var data []byte |
||||
var err error |
||||
|
||||
term := os.Getenv("TERM") |
||||
if term == "" { |
||||
return nil, fmt.Errorf("termbox: TERM not set") |
||||
} |
||||
|
||||
// The following behaviour follows the one described in terminfo(5) as
|
||||
// distributed by ncurses.
|
||||
|
||||
terminfo := os.Getenv("TERMINFO") |
||||
if terminfo != "" { |
||||
// if TERMINFO is set, no other directory should be searched
|
||||
return ti_try_path(terminfo) |
||||
} |
||||
|
||||
// next, consider ~/.terminfo
|
||||
home := os.Getenv("HOME") |
||||
if home != "" { |
||||
data, err = ti_try_path(home + "/.terminfo") |
||||
if err == nil { |
||||
return data, nil |
||||
} |
||||
} |
||||
|
||||
// next, TERMINFO_DIRS
|
||||
dirs := os.Getenv("TERMINFO_DIRS") |
||||
if dirs != "" { |
||||
for _, dir := range strings.Split(dirs, ":") { |
||||
if dir == "" { |
||||
// "" -> "/usr/share/terminfo"
|
||||
dir = "/usr/share/terminfo" |
||||
} |
||||
data, err = ti_try_path(dir) |
||||
if err == nil { |
||||
return data, nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
// fall back to /usr/share/terminfo
|
||||
return ti_try_path("/usr/share/terminfo") |
||||
} |
||||
|
||||
func ti_try_path(path string) (data []byte, err error) { |
||||
// load_terminfo already made sure it is set
|
||||
term := os.Getenv("TERM") |
||||
|
||||
// first try, the typical *nix path
|
||||
terminfo := path + "/" + term[0:1] + "/" + term |
||||
data, err = ioutil.ReadFile(terminfo) |
||||
if err == nil { |
||||
return |
||||
} |
||||
|
||||
// fallback to darwin specific dirs structure
|
||||
terminfo = path + "/" + hex.EncodeToString([]byte(term[:1])) + "/" + term |
||||
data, err = ioutil.ReadFile(terminfo) |
||||
return |
||||
} |
||||
|
||||
func setup_term_builtin() error { |
||||
name := os.Getenv("TERM") |
||||
if name == "" { |
||||
return errors.New("termbox: TERM environment variable not set") |
||||
} |
||||
|
||||
for _, t := range terms { |
||||
if t.name == name { |
||||
keys = t.keys |
||||
funcs = t.funcs |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
compat_table := []struct { |
||||
partial string |
||||
keys []string |
||||
funcs []string |
||||
}{ |
||||
{"xterm", xterm_keys, xterm_funcs}, |
||||
{"rxvt", rxvt_unicode_keys, rxvt_unicode_funcs}, |
||||
{"linux", linux_keys, linux_funcs}, |
||||
{"Eterm", eterm_keys, eterm_funcs}, |
||||
{"screen", screen_keys, screen_funcs}, |
||||
// let's assume that 'cygwin' is xterm compatible
|
||||
{"cygwin", xterm_keys, xterm_funcs}, |
||||
{"st", xterm_keys, xterm_funcs}, |
||||
} |
||||
|
||||
// try compatibility variants
|
||||
for _, it := range compat_table { |
||||
if strings.Contains(name, it.partial) { |
||||
keys = it.keys |
||||
funcs = it.funcs |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
return errors.New("termbox: unsupported terminal") |
||||
} |
||||
|
||||
func setup_term() (err error) { |
||||
var data []byte |
||||
var header [6]int16 |
||||
var str_offset, table_offset int16 |
||||
|
||||
data, err = load_terminfo() |
||||
if err != nil { |
||||
return setup_term_builtin() |
||||
} |
||||
|
||||
rd := bytes.NewReader(data) |
||||
// 0: magic number, 1: size of names section, 2: size of boolean section, 3:
|
||||
// size of numbers section (in integers), 4: size of the strings section (in
|
||||
// integers), 5: size of the string table
|
||||
|
||||
err = binary.Read(rd, binary.LittleEndian, header[:]) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if (header[1]+header[2])%2 != 0 { |
||||
// old quirk to align everything on word boundaries
|
||||
header[2] += 1 |
||||
} |
||||
str_offset = ti_header_length + header[1] + header[2] + 2*header[3] |
||||
table_offset = str_offset + 2*header[4] |
||||
|
||||
keys = make([]string, 0xFFFF-key_min) |
||||
for i, _ := range keys { |
||||
keys[i], err = ti_read_string(rd, str_offset+2*ti_keys[i], table_offset) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
funcs = make([]string, t_max_funcs) |
||||
// the last two entries are reserved for mouse. because the table offset is
|
||||
// not there, the two entries have to fill in manually
|
||||
for i, _ := range funcs[:len(funcs)-2] { |
||||
funcs[i], err = ti_read_string(rd, str_offset+2*ti_funcs[i], table_offset) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
funcs[t_max_funcs-2] = "\x1b[?1000h" |
||||
funcs[t_max_funcs-1] = "\x1b[?1000l" |
||||
return nil |
||||
} |
||||
|
||||
func ti_read_string(rd *bytes.Reader, str_off, table int16) (string, error) { |
||||
var off int16 |
||||
|
||||
_, err := rd.Seek(int64(str_off), 0) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
err = binary.Read(rd, binary.LittleEndian, &off) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
_, err = rd.Seek(int64(table+off), 0) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
var bs []byte |
||||
for { |
||||
b, err := rd.ReadByte() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
if b == byte(0x00) { |
||||
break |
||||
} |
||||
bs = append(bs, b) |
||||
} |
||||
return string(bs), nil |
||||
} |
||||
|
||||
// "Maps" the function constants from termbox.go to the number of the respective
|
||||
// string capability in the terminfo file. Taken from (ncurses) term.h.
|
||||
var ti_funcs = []int16{ |
||||
28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88, |
||||
} |
||||
|
||||
// Same as above for the special keys.
|
||||
var ti_keys = []int16{ |
||||
66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69, 70, |
||||
71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61, 79, 83, |
||||
} |
@ -0,0 +1,64 @@ |
||||
// +build !windows
|
||||
|
||||
package termbox |
||||
|
||||
// Eterm
|
||||
var eterm_keys = []string{ |
||||
"\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", |
||||
} |
||||
var eterm_funcs = []string{ |
||||
"\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "", |
||||
} |
||||
|
||||
// screen
|
||||
var screen_keys = []string{ |
||||
"\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC", |
||||
} |
||||
var screen_funcs = []string{ |
||||
"\x1b[?1049h", "\x1b[?1049l", "\x1b[34h\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", "\x1b[?1000h", "\x1b[?1000l", |
||||
} |
||||
|
||||
// xterm
|
||||
var xterm_keys = []string{ |
||||
"\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1bOH", "\x1bOF", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC", |
||||
} |
||||
var xterm_funcs = []string{ |
||||
"\x1b[?1049h", "\x1b[?1049l", "\x1b[?12l\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b(B\x1b[m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", "\x1b[?1000h", "\x1b[?1000l", |
||||
} |
||||
|
||||
// rxvt-unicode
|
||||
var rxvt_unicode_keys = []string{ |
||||
"\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", |
||||
} |
||||
var rxvt_unicode_funcs = []string{ |
||||
"\x1b[?1049h", "\x1b[r\x1b[?1049l", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x1b(B", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", "\x1b[?1000h", "\x1b[?1000l", |
||||
} |
||||
|
||||
// linux
|
||||
var linux_keys = []string{ |
||||
"\x1b[[A", "\x1b[[B", "\x1b[[C", "\x1b[[D", "\x1b[[E", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", |
||||
} |
||||
var linux_funcs = []string{ |
||||
"", "", "\x1b[?25h\x1b[?0c", "\x1b[?25l\x1b[?1c", "\x1b[H\x1b[J", "\x1b[0;10m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "", |
||||
} |
||||
|
||||
// rxvt-256color
|
||||
var rxvt_256color_keys = []string{ |
||||
"\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", |
||||
} |
||||
var rxvt_256color_funcs = []string{ |
||||
"\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", "\x1b[?1000h", "\x1b[?1000l", |
||||
} |
||||
|
||||
var terms = []struct { |
||||
name string |
||||
keys []string |
||||
funcs []string |
||||
}{ |
||||
{"Eterm", eterm_keys, eterm_funcs}, |
||||
{"screen", screen_keys, screen_funcs}, |
||||
{"xterm", xterm_keys, xterm_funcs}, |
||||
{"rxvt-unicode", rxvt_unicode_keys, rxvt_unicode_funcs}, |
||||
{"linux", linux_keys, linux_funcs}, |
||||
{"rxvt-256color", rxvt_256color_keys, rxvt_256color_funcs}, |
||||
} |
@ -1,5 +0,0 @@ |
||||
examples/qmlscene/qmlscene |
||||
examples/snapweb/snapweb |
||||
examples/particle/particle |
||||
gl/gengl/gengl |
||||
*.swp |
@ -1,185 +0,0 @@ |
||||
This software is licensed under the LGPLv3, included below. |
||||
|
||||
As a special exception to the GNU Lesser General Public License version 3 |
||||
("LGPL3"), the copyright holders of this Library give you permission to |
||||
convey to a third party a Combined Work that links statically or dynamically |
||||
to this Library without providing any Minimal Corresponding Source or |
||||
Minimal Application Code as set out in 4d or providing the installation |
||||
information set out in section 4e, provided that you comply with the other |
||||
provisions of LGPL3 and provided that you meet, for the Application the |
||||
terms and conditions of the license(s) which apply to the Application. |
||||
|
||||
Except as stated in this special exception, the provisions of LGPL3 will |
||||
continue to comply in full to this Library. If you modify this Library, you |
||||
may apply this exception to your version of this Library, but you are not |
||||
obliged to do so. If you do not wish to do so, delete this exception |
||||
statement from your version. This exception does not (and cannot) modify any |
||||
license terms which apply to the Application, with which you must still |
||||
comply. |
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE |
||||
Version 3, 29 June 2007 |
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> |
||||
Everyone is permitted to copy and distribute verbatim copies |
||||
of this license document, but changing it is not allowed. |
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates |
||||
the terms and conditions of version 3 of the GNU General Public |
||||
License, supplemented by the additional permissions listed below. |
||||
|
||||
0. Additional Definitions. |
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser |
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU |
||||
General Public License. |
||||
|
||||
"The Library" refers to a covered work governed by this License, |
||||
other than an Application or a Combined Work as defined below. |
||||
|
||||
An "Application" is any work that makes use of an interface provided |
||||
by the Library, but which is not otherwise based on the Library. |
||||
Defining a subclass of a class defined by the Library is deemed a mode |
||||
of using an interface provided by the Library. |
||||
|
||||
A "Combined Work" is a work produced by combining or linking an |
||||
Application with the Library. The particular version of the Library |
||||
with which the Combined Work was made is also called the "Linked |
||||
Version". |
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the |
||||
Corresponding Source for the Combined Work, excluding any source code |
||||
for portions of the Combined Work that, considered in isolation, are |
||||
based on the Application, and not on the Linked Version. |
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the |
||||
object code and/or source code for the Application, including any data |
||||
and utility programs needed for reproducing the Combined Work from the |
||||
Application, but excluding the System Libraries of the Combined Work. |
||||
|
||||
1. Exception to Section 3 of the GNU GPL. |
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License |
||||
without being bound by section 3 of the GNU GPL. |
||||
|
||||
2. Conveying Modified Versions. |
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a |
||||
facility refers to a function or data to be supplied by an Application |
||||
that uses the facility (other than as an argument passed when the |
||||
facility is invoked), then you may convey a copy of the modified |
||||
version: |
||||
|
||||
a) under this License, provided that you make a good faith effort to |
||||
ensure that, in the event an Application does not supply the |
||||
function or data, the facility still operates, and performs |
||||
whatever part of its purpose remains meaningful, or |
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of |
||||
this License applicable to that copy. |
||||
|
||||
3. Object Code Incorporating Material from Library Header Files. |
||||
|
||||
The object code form of an Application may incorporate material from |
||||
a header file that is part of the Library. You may convey such object |
||||
code under terms of your choice, provided that, if the incorporated |
||||
material is not limited to numerical parameters, data structure |
||||
layouts and accessors, or small macros, inline functions and templates |
||||
(ten or fewer lines in length), you do both of the following: |
||||
|
||||
a) Give prominent notice with each copy of the object code that the |
||||
Library is used in it and that the Library and its use are |
||||
covered by this License. |
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license |
||||
document. |
||||
|
||||
4. Combined Works. |
||||
|
||||
You may convey a Combined Work under terms of your choice that, |
||||
taken together, effectively do not restrict modification of the |
||||
portions of the Library contained in the Combined Work and reverse |
||||
engineering for debugging such modifications, if you also do each of |
||||
the following: |
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that |
||||
the Library is used in it and that the Library and its use are |
||||
covered by this License. |
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license |
||||
document. |
||||
|
||||
c) For a Combined Work that displays copyright notices during |
||||
execution, include the copyright notice for the Library among |
||||
these notices, as well as a reference directing the user to the |
||||
copies of the GNU GPL and this license document. |
||||
|
||||
d) Do one of the following: |
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this |
||||
License, and the Corresponding Application Code in a form |
||||
suitable for, and under terms that permit, the user to |
||||
recombine or relink the Application with a modified version of |
||||
the Linked Version to produce a modified Combined Work, in the |
||||
manner specified by section 6 of the GNU GPL for conveying |
||||
Corresponding Source. |
||||
|
||||
1) Use a suitable shared library mechanism for linking with the |
||||
Library. A suitable mechanism is one that (a) uses at run time |
||||
a copy of the Library already present on the user's computer |
||||
system, and (b) will operate properly with a modified version |
||||
of the Library that is interface-compatible with the Linked |
||||
Version. |
||||
|
||||
e) Provide Installation Information, but only if you would otherwise |
||||
be required to provide such information under section 6 of the |
||||
GNU GPL, and only to the extent that such information is |
||||
necessary to install and execute a modified version of the |
||||
Combined Work produced by recombining or relinking the |
||||
Application with a modified version of the Linked Version. (If |
||||
you use option 4d0, the Installation Information must accompany |
||||
the Minimal Corresponding Source and Corresponding Application |
||||
Code. If you use option 4d1, you must provide the Installation |
||||
Information in the manner specified by section 6 of the GNU GPL |
||||
for conveying Corresponding Source.) |
||||
|
||||
5. Combined Libraries. |
||||
|
||||
You may place library facilities that are a work based on the |
||||
Library side by side in a single library together with other library |
||||
facilities that are not Applications and are not covered by this |
||||
License, and convey such a combined library under terms of your |
||||
choice, if you do both of the following: |
||||
|
||||
a) Accompany the combined library with a copy of the same work based |
||||
on the Library, uncombined with any other library facilities, |
||||
conveyed under the terms of this License. |
||||
|
||||
b) Give prominent notice with the combined library that part of it |
||||
is a work based on the Library, and explaining where to find the |
||||
accompanying uncombined form of the same work. |
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License. |
||||
|
||||
The Free Software Foundation may publish revised and/or new versions |
||||
of the GNU Lesser General Public License from time to time. Such new |
||||
versions will be similar in spirit to the present version, but may |
||||
differ in detail to address new problems or concerns. |
||||
|
||||
Each version is given a distinguishing version number. If the |
||||
Library as you received it specifies that a certain numbered version |
||||
of the GNU Lesser General Public License "or any later version" |
||||
applies to it, you have the option of following the terms and |
||||
conditions either of that published version or of any later version |
||||
published by the Free Software Foundation. If the Library as you |
||||
received it does not specify a version number of the GNU Lesser |
||||
General Public License, you may choose any version of the GNU Lesser |
||||
General Public License ever published by the Free Software Foundation. |
||||
|
||||
If the Library as you received it specifies that a proxy can decide |
||||
whether future versions of the GNU Lesser General Public License shall |
||||
apply, that proxy's public statement of acceptance of any version is |
||||
permanent authorization for you to choose that version for the |
||||
Library. |
@ -1,157 +0,0 @@ |
||||
# QML support for the Go language |
||||
|
||||
Documentation |
||||
------------- |
||||
|
||||
The introductory documentation as well as the detailed API documentation is |
||||
available at [gopkg.in/qml.v1](http://godoc.org/gopkg.in/qml.v1). |
||||
|
||||
|
||||
Blog posts |
||||
---------- |
||||
|
||||
Some relevant blog posts: |
||||
|
||||
* [Announcing qml v1 for Go](http://blog.labix.org/2014/08/13/announcing-qml-v1-for-go) |
||||
* [Packing resources into Go qml binaries](http://blog.labix.org/2014/09/26/packing-resources-into-go-qml-binaries) |
||||
* [Go qml contest results](http://blog.labix.org/2014/04/25/qml-contest-results) |
||||
* [Arbitrary Qt extensions with Go qml](http://blog.labix.org/2014/03/21/arbitrary-qt-extensions-with-go-qml) |
||||
* [The new Go qml OpenGL API](http://blog.labix.org/2014/08/29/the-new-go-qml-opengl-api) |
||||
* [QML components with Go and OpenGL](http://blog.labix.org/2013/12/23/qml-components-with-go-and-opengl) |
||||
|
||||
|
||||
Videos |
||||
------ |
||||
|
||||
These introductory videos demonstrate the use of Go QML: |
||||
|
||||
* [Initial demo and overview](http://youtu.be/FVQlMrPa7lI) |
||||
* [Initial demo running on an Ubuntu Touch phone](http://youtu.be/HB-3o8Cysec) |
||||
* [Spinning Gopher with Go + QML + OpenGL](http://youtu.be/qkH7_dtOyPk) |
||||
* [SameGame QML tutorial in Go](http://youtu.be/z8noX48hiMI) |
||||
|
||||
|
||||
Community |
||||
--------- |
||||
|
||||
Please join the [mailing list](https://groups.google.com/forum/#!forum/go-qml) for |
||||
following relevant development news and discussing project details. |
||||
|
||||
|
||||
Installation |
||||
------------ |
||||
|
||||
To try the alpha release you'll need: |
||||
|
||||
* Go >= 1.2, for the C++ support of _go build_ |
||||
* Qt 5.0.X or 5.1.X with the development files |
||||
* The Qt headers qmetaobject_p.h and qmetaobjectbuilder_p.h, for the dynamic meta object support |
||||
|
||||
See below for more details about getting these requirements installed in different environments and operating systems. |
||||
|
||||
After the requirements are satisfied, _go get_ should work as usual: |
||||
|
||||
go get gopkg.in/qml.v1 |
||||
|
||||
|
||||
Requirements on Ubuntu |
||||
---------------------- |
||||
|
||||
If you are using Ubuntu, the [Ubuntu SDK](http://developer.ubuntu.com/get-started/) will take care of the Qt dependencies: |
||||
|
||||
$ sudo add-apt-repository ppa:ubuntu-sdk-team/ppa |
||||
$ sudo apt-get update |
||||
$ sudo apt-get install qtdeclarative5-dev qtbase5-private-dev qtdeclarative5-private-dev libqt5opengl5-dev qtdeclarative5-qtquick2-plugin |
||||
|
||||
and Go >= 1.2 may be installed using [godeb](http://blog.labix.org/2013/06/15/in-flight-deb-packages-of-go): |
||||
|
||||
$ # Pick the right one for your system: 386 or amd64 |
||||
$ ARCH=amd64 |
||||
$ wget -q https://godeb.s3.amazonaws.com/godeb-$ARCH.tar.gz |
||||
$ tar xzvf godeb-$ARCH.tar.gz |
||||
godeb |
||||
$ sudo mv godeb /usr/local/bin |
||||
$ godeb install |
||||
$ go get gopkg.in/qml.v1 |
||||
|
||||
|
||||
Requirements on Ubuntu Touch |
||||
---------------------------- |
||||
|
||||
After following the [installation instructions](https://wiki.ubuntu.com/Touch/Install) for Ubuntu Touch, |
||||
run the following commands to get a working build environment inside the device: |
||||
|
||||
$ adb shell |
||||
# cd /tmp |
||||
# wget https://github.com/go-qml/qml/raw/v1/cmd/ubuntu-touch/setup.sh |
||||
# /bin/bash setup.sh |
||||
# su - phablet |
||||
$ |
||||
|
||||
At the end of setup.sh, the phablet user will have GOPATH=$HOME in the environment, |
||||
the qml package will be built, and the particle example will be built and run. For |
||||
stopping it from the command line, run as the phablet user: |
||||
|
||||
$ ubuntu-app-stop gopkg.in.qml.particle-example |
||||
|
||||
for running it again: |
||||
|
||||
$ ubuntu-app-launch gopkg.in.qml.particle-example |
||||
|
||||
These commands depend on the following file, installed by setup.sh: |
||||
|
||||
~/.local/share/applications/gopkg.in.qml.particle-example.desktop |
||||
|
||||
|
||||
Requirements on Mac OS X |
||||
------------------------ |
||||
|
||||
On Mac OS X you'll need QT5. It's easiest to install with Homebrew, a |
||||
third-party package management system for OS X. |
||||
|
||||
Installation instructions for Homebrew are here: |
||||
|
||||
http://brew.sh/ |
||||
|
||||
Then, install the qt5 and pkg-config packages: |
||||
|
||||
$ brew install qt5 pkg-config |
||||
|
||||
Then, force brew to "link" qt5 (this makes it available under /usr/local): |
||||
|
||||
$ brew link --force qt5 |
||||
|
||||
And finally, fetch and install go-qml: |
||||
|
||||
$ go get gopkg.in/qml.v1 |
||||
|
||||
|
||||
Requirements on Windows |
||||
----------------------- |
||||
|
||||
On Windows you'll need the following: |
||||
|
||||
* [MinGW gcc](http://sourceforge.net/projects/mingw/files/latest/download) 4.8.1 (install mingw-get and install the gcc from within the setup GUI) |
||||
* [Qt 5.1.1](http://download.qt-project.org/official_releases/qt/5.1/5.1.1/qt-windows-opensource-5.1.1-mingw48_opengl-x86-offline.exe) for MinGW 4.8 |
||||
* [Go >= 1.2](http://golang.org/doc/install) |
||||
|
||||
Then, assuming Qt was installed under `C:\Qt5.1.1\`, set up the following environment variables in the respective configuration: |
||||
|
||||
CPATH += C:\Qt5.1.1\5.1.1\mingw48_32\include |
||||
LIBRARY_PATH += C:\Qt5.1.1\5.1.1\mingw48_32\lib |
||||
PATH += C:\Qt5.1.1\5.1.1\mingw48_32\bin |
||||
|
||||
After reopening the shell for the environment changes to take effect, this should work: |
||||
|
||||
go get gopkg.in/qml.v1 |
||||
|
||||
|
||||
Requirements everywhere else |
||||
---------------------------- |
||||
|
||||
If your operating system does not offer these dependencies readily, |
||||
you may still have success installing [Go >= 1.2](http://golang.org/doc/install) |
||||
and [Qt 5.0.2](http://download.qt-project.org/archive/qt/5.0/5.0.2/) |
||||
directly from the upstreams. Note that you'll likely have to adapt |
||||
environment variables to reflect the custom installation path for |
||||
these libraries. See the instructions above for examples. |
@ -1,12 +0,0 @@ |
||||
|
||||
#include "cpp/capi.cpp" |
||||
#include "cpp/govalue.cpp" |
||||
#include "cpp/govaluetype.cpp" |
||||
#include "cpp/idletimer.cpp" |
||||
#include "cpp/connector.cpp" |
||||
|
||||
#include "cpp/moc_all.cpp" |
||||
|
||||
#ifdef _WIN32 |
||||
#include "cpp/mmemwin.cpp" |
||||
#endif |
@ -1,681 +0,0 @@ |
||||
package qml |
||||
|
||||
// #cgo CPPFLAGS: -I./cpp
|
||||
// #cgo CXXFLAGS: -std=c++0x -pedantic-errors -Wall -fno-strict-aliasing
|
||||
// #cgo LDFLAGS: -lstdc++
|
||||
// #cgo pkg-config: Qt5Core Qt5Widgets Qt5Quick
|
||||
//
|
||||
// #include <stdlib.h>
|
||||
//
|
||||
// #include "cpp/capi.h"
|
||||
//
|
||||
import "C" |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"reflect" |
||||
"runtime" |
||||
"sync/atomic" |
||||
"unsafe" |
||||
|
||||
"gopkg.in/qml.v1/cdata" |
||||
) |
||||
|
||||
var ( |
||||
guiFunc = make(chan func()) |
||||
guiDone = make(chan struct{}) |
||||
guiLock = 0 |
||||
guiMainRef uintptr |
||||
guiPaintRef uintptr |
||||
guiIdleRun int32 |
||||
|
||||
initialized int32 |
||||
) |
||||
|
||||
func init() { |
||||
runtime.LockOSThread() |
||||
guiMainRef = cdata.Ref() |
||||
} |
||||
|
||||
// Run runs the main QML event loop, runs f, and then terminates the
|
||||
// event loop once f returns.
|
||||
//
|
||||
// Most functions from the qml package block until Run is called.
|
||||
//
|
||||
// The Run function must necessarily be called from the same goroutine as
|
||||
// the main function or the application may fail when running on Mac OS.
|
||||
func Run(f func() error) error { |
||||
if cdata.Ref() != guiMainRef { |
||||
panic("Run must be called on the initial goroutine so apps are portable to Mac OS") |
||||
} |
||||
if !atomic.CompareAndSwapInt32(&initialized, 0, 1) { |
||||
panic("qml.Run called more than once") |
||||
} |
||||
C.newGuiApplication() |
||||
C.idleTimerInit((*C.int32_t)(&guiIdleRun)) |
||||
done := make(chan error, 1) |
||||
go func() { |
||||
RunMain(func() {}) // Block until the event loop is running.
|
||||
done <- f() |
||||
C.applicationExit() |
||||
}() |
||||
C.applicationExec() |
||||
return <-done |
||||
} |
||||
|
||||
// RunMain runs f in the main QML thread and waits for f to return.
|
||||
//
|
||||
// This is meant to be used by extensions that integrate directly with the
|
||||
// underlying QML logic.
|
||||
func RunMain(f func()) { |
||||
ref := cdata.Ref() |
||||
if ref == guiMainRef || ref == atomic.LoadUintptr(&guiPaintRef) { |
||||
// Already within the GUI or render threads. Attempting to wait would deadlock.
|
||||
f() |
||||
return |
||||
} |
||||
|
||||
// Tell Qt we're waiting for the idle hook to be called.
|
||||
if atomic.AddInt32(&guiIdleRun, 1) == 1 { |
||||
C.idleTimerStart() |
||||
} |
||||
|
||||
// Send f to be executed by the idle hook in the main GUI thread.
|
||||
guiFunc <- f |
||||
|
||||
// Wait until f is done executing.
|
||||
<-guiDone |
||||
} |
||||
|
||||
// Lock freezes all QML activity by blocking the main event loop.
|
||||
// Locking is necessary before updating shared data structures
|
||||
// without race conditions.
|
||||
//
|
||||
// It's safe to use qml functionality while holding a lock, as
|
||||
// long as the requests made do not depend on follow up QML
|
||||
// events to be processed before returning. If that happens, the
|
||||
// problem will be observed as the application freezing.
|
||||
//
|
||||
// The Lock function is reentrant. That means it may be called
|
||||
// multiple times, and QML activities will only be resumed after
|
||||
// Unlock is called a matching number of times.
|
||||
func Lock() { |
||||
// TODO Better testing for this.
|
||||
RunMain(func() { |
||||
guiLock++ |
||||
}) |
||||
} |
||||
|
||||
// Unlock releases the QML event loop. See Lock for details.
|
||||
func Unlock() { |
||||
RunMain(func() { |
||||
if guiLock == 0 { |
||||
panic("qml.Unlock called without lock being held") |
||||
} |
||||
guiLock-- |
||||
}) |
||||
} |
||||
|
||||
// Flush synchronously flushes all pending QML activities.
|
||||
func Flush() { |
||||
// TODO Better testing for this.
|
||||
RunMain(func() { |
||||
C.applicationFlushAll() |
||||
}) |
||||
} |
||||
|
||||
// Changed notifies all QML bindings that the given field value has changed.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// qml.Changed(&value, &value.Field)
|
||||
//
|
||||
func Changed(value, fieldAddr interface{}) { |
||||
valuev := reflect.ValueOf(value) |
||||
fieldv := reflect.ValueOf(fieldAddr) |
||||
for valuev.Kind() == reflect.Ptr { |
||||
valuev = valuev.Elem() |
||||
} |
||||
if fieldv.Kind() != reflect.Ptr { |
||||
panic("qml.Changed received non-address value as fieldAddr") |
||||
} |
||||
fieldv = fieldv.Elem() |
||||
if fieldv.Type().Size() == 0 { |
||||
panic("cannot report changes on zero-sized fields") |
||||
} |
||||
offset := fieldv.UnsafeAddr() - valuev.UnsafeAddr() |
||||
if !(0 <= offset && offset < valuev.Type().Size()) { |
||||
panic("provided field is not a member of the given value") |
||||
} |
||||
|
||||
RunMain(func() { |
||||
tinfo := typeInfo(value) |
||||
for _, engine := range engines { |
||||
fold := engine.values[value] |
||||
for fold != nil { |
||||
C.goValueActivate(fold.cvalue, tinfo, C.int(offset)) |
||||
fold = fold.next |
||||
} |
||||
// TODO typeNew might also be a linked list keyed by the gvalue.
|
||||
// This would prevent the iteration and the deferrals.
|
||||
for fold, _ = range typeNew { |
||||
if fold.gvalue == value { |
||||
// Activate these later so they don't get recursively moved
|
||||
// out of typeNew while the iteration is still happening.
|
||||
defer C.goValueActivate(fold.cvalue, tinfo, C.int(offset)) |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// hookIdleTimer is run once per iteration of the Qt event loop,
|
||||
// within the main GUI thread, but only if at least one goroutine
|
||||
// has atomically incremented guiIdleRun.
|
||||
//
|
||||
//export hookIdleTimer
|
||||
func hookIdleTimer() { |
||||
var f func() |
||||
for { |
||||
select { |
||||
case f = <-guiFunc: |
||||
default: |
||||
if guiLock > 0 { |
||||
f = <-guiFunc |
||||
} else { |
||||
return |
||||
} |
||||
} |
||||
f() |
||||
guiDone <- struct{}{} |
||||
atomic.AddInt32(&guiIdleRun, -1) |
||||
} |
||||
} |
||||
|
||||
type valueFold struct { |
||||
engine *Engine |
||||
gvalue interface{} |
||||
cvalue unsafe.Pointer |
||||
init reflect.Value |
||||
prev *valueFold |
||||
next *valueFold |
||||
owner valueOwner |
||||
} |
||||
|
||||
type valueOwner uint8 |
||||
|
||||
const ( |
||||
cppOwner = 1 << iota |
||||
jsOwner |
||||
) |
||||
|
||||
// wrapGoValue creates a new GoValue object in C++ land wrapping
|
||||
// the Go value contained in the given interface.
|
||||
//
|
||||
// This must be run from the main GUI thread.
|
||||
func wrapGoValue(engine *Engine, gvalue interface{}, owner valueOwner) (cvalue unsafe.Pointer) { |
||||
gvaluev := reflect.ValueOf(gvalue) |
||||
gvaluek := gvaluev.Kind() |
||||
if gvaluek == reflect.Struct && !hashable(gvalue) { |
||||
name := gvaluev.Type().Name() |
||||
if name != "" { |
||||
name = " (" + name + ")" |
||||
} |
||||
panic("cannot hand an unhashable struct value" + name + " to QML logic; use its address instead") |
||||
} |
||||
if gvaluek == reflect.Ptr && gvaluev.Elem().Kind() == reflect.Ptr { |
||||
panic("cannot hand pointer of pointer to QML logic; use a simple pointer instead") |
||||
} |
||||
|
||||
painting := cdata.Ref() == atomic.LoadUintptr(&guiPaintRef) |
||||
|
||||
// Cannot reuse a jsOwner because the QML runtime may choose to destroy
|
||||
// the value _after_ we hand it a new reference to the same value.
|
||||
// See issue #68 for details.
|
||||
prev, ok := engine.values[gvalue] |
||||
if ok && (prev.owner == cppOwner || painting) { |
||||
return prev.cvalue |
||||
} |
||||
|
||||
if painting { |
||||
panic("cannot allocate new objects while painting") |
||||
} |
||||
|
||||
parent := nilPtr |
||||
if owner == cppOwner { |
||||
parent = engine.addr |
||||
} |
||||
fold := &valueFold{ |
||||
engine: engine, |
||||
gvalue: gvalue, |
||||
owner: owner, |
||||
} |
||||
fold.cvalue = C.newGoValue(unsafe.Pointer(fold), typeInfo(gvalue), parent) |
||||
if prev != nil { |
||||
// Put new fold first so the single cppOwner, if any, is always the first entry.
|
||||
fold.next = prev |
||||
prev.prev = fold |
||||
} |
||||
engine.values[gvalue] = fold |
||||
|
||||
//fmt.Printf("[DEBUG] value alive (wrapped): cvalue=%x gvalue=%x/%#v\n", fold.cvalue, addrOf(fold.gvalue), fold.gvalue)
|
||||
stats.valuesAlive(+1) |
||||
C.engineSetContextForObject(engine.addr, fold.cvalue) |
||||
switch owner { |
||||
case cppOwner: |
||||
C.engineSetOwnershipCPP(engine.addr, fold.cvalue) |
||||
case jsOwner: |
||||
C.engineSetOwnershipJS(engine.addr, fold.cvalue) |
||||
} |
||||
return fold.cvalue |
||||
} |
||||
|
||||
func addrOf(gvalue interface{}) uintptr { |
||||
return reflect.ValueOf(gvalue).Pointer() |
||||
} |
||||
|
||||
// typeNew holds fold values that are created by registered types.
|
||||
// These values are special in two senses: first, they don't have a
|
||||
// reference to an engine before they are used in a context that can
|
||||
// set the reference; second, these values always hold a new cvalue,
|
||||
// because they are created as a side-effect of the registered type
|
||||
// being instantiated (it's too late to reuse an existent cvalue).
|
||||
//
|
||||
// For these reasons, typeNew holds the fold for these values until
|
||||
// their engine is known, and once it's known they may have to be
|
||||
// added to the linked list, since mulitple references for the same
|
||||
// gvalue may occur.
|
||||
var typeNew = make(map[*valueFold]bool) |
||||
|
||||
//export hookGoValueTypeNew
|
||||
func hookGoValueTypeNew(cvalue unsafe.Pointer, specp unsafe.Pointer) (foldp unsafe.Pointer) { |
||||
// Initialization is postponed until the engine is available, so that
|
||||
// we can hand Init the qml.Object that represents the object.
|
||||
init := reflect.ValueOf((*TypeSpec)(specp).Init) |
||||
fold := &valueFold{ |
||||
init: init, |
||||
gvalue: reflect.New(init.Type().In(0).Elem()).Interface(), |
||||
cvalue: cvalue, |
||||
owner: jsOwner, |
||||
} |
||||
typeNew[fold] = true |
||||
//fmt.Printf("[DEBUG] value alive (type-created): cvalue=%x gvalue=%x/%#v\n", fold.cvalue, addrOf(fold.gvalue), fold.gvalue)
|
||||
stats.valuesAlive(+1) |
||||
return unsafe.Pointer(fold) |
||||
} |
||||
|
||||
//export hookGoValueDestroyed
|
||||
func hookGoValueDestroyed(enginep unsafe.Pointer, foldp unsafe.Pointer) { |
||||
fold := (*valueFold)(foldp) |
||||
engine := fold.engine |
||||
if engine == nil { |
||||
before := len(typeNew) |
||||
delete(typeNew, fold) |
||||
if len(typeNew) == before { |
||||
panic("destroying value without an associated engine; who created the value?") |
||||
} |
||||
} else if engines[engine.addr] == nil { |
||||
// Must never do that. The engine holds memory references that C++ depends on.
|
||||
panic(fmt.Sprintf("engine %p was released from global list while its values were still alive", engine.addr)) |
||||
} else { |
||||
switch { |
||||
case fold.prev != nil: |
||||
fold.prev.next = fold.next |
||||
if fold.next != nil { |
||||
fold.next.prev = fold.prev |
||||
} |
||||
case fold.next != nil: |
||||
fold.next.prev = fold.prev |
||||
if fold.prev != nil { |
||||
fold.prev.next = fold.next |
||||
} else { |
||||
fold.engine.values[fold.gvalue] = fold.next |
||||
} |
||||
default: |
||||
before := len(engine.values) |
||||
delete(engine.values, fold.gvalue) |
||||
if len(engine.values) == before { |
||||
panic("destroying value that knows about the engine, but the engine doesn't know about the value; who cleared the engine?") |
||||
} |
||||
if engine.destroyed && len(engine.values) == 0 { |
||||
delete(engines, engine.addr) |
||||
} |
||||
} |
||||
} |
||||
//fmt.Printf("[DEBUG] value destroyed: cvalue=%x gvalue=%x/%#v\n", fold.cvalue, addrOf(fold.gvalue), fold.gvalue)
|
||||
stats.valuesAlive(-1) |
||||
} |
||||
|
||||
func deref(value reflect.Value) reflect.Value { |
||||
for { |
||||
switch value.Kind() { |
||||
case reflect.Ptr, reflect.Interface: |
||||
value = value.Elem() |
||||
continue |
||||
} |
||||
return value |
||||
} |
||||
panic("cannot happen") |
||||
} |
||||
|
||||
//export hookGoValueReadField
|
||||
func hookGoValueReadField(enginep, foldp unsafe.Pointer, reflectIndex, getIndex, setIndex C.int, resultdv *C.DataValue) { |
||||
fold := ensureEngine(enginep, foldp) |
||||
|
||||
var field reflect.Value |
||||
if getIndex >= 0 { |
||||
field = reflect.ValueOf(fold.gvalue).Method(int(getIndex)).Call(nil)[0] |
||||
} else { |
||||
field = deref(reflect.ValueOf(fold.gvalue)).Field(int(reflectIndex)) |
||||
} |
||||
field = deref(field) |
||||
|
||||
// Cannot compare Type directly as field may be invalid (nil).
|
||||
if field.Kind() == reflect.Slice && field.Type() == typeObjSlice { |
||||
// TODO Handle getters that return []qml.Object.
|
||||
// TODO Handle other GoValue slices (!= []qml.Object).
|
||||
resultdv.dataType = C.DTListProperty |
||||
*(*unsafe.Pointer)(unsafe.Pointer(&resultdv.data)) = C.newListProperty(foldp, C.intptr_t(reflectIndex), C.intptr_t(setIndex)) |
||||
return |
||||
} |
||||
|
||||
fieldk := field.Kind() |
||||
if fieldk == reflect.Slice || fieldk == reflect.Struct && field.Type() != typeRGBA { |
||||
if field.CanAddr() { |
||||
field = field.Addr() |
||||
} else if !hashable(field.Interface()) { |
||||
t := reflect.ValueOf(fold.gvalue).Type() |
||||
for t.Kind() == reflect.Ptr { |
||||
t = t.Elem() |
||||
} |
||||
panic(fmt.Sprintf("cannot access unaddressable and unhashable struct value on interface field %s.%s; value: %#v", t.Name(), t.Field(int(reflectIndex)).Name, field.Interface())) |
||||
} |
||||
} |
||||
var gvalue interface{} |
||||
if field.IsValid() { |
||||
gvalue = field.Interface() |
||||
} |
||||
|
||||
// TODO Strings are being passed in an unsafe manner here. There is a
|
||||
// small chance that the field is changed and the garbage collector is run
|
||||
// before C++ has a chance to look at the data. We can solve this problem
|
||||
// by queuing up values in a stack, and cleaning the stack when the
|
||||
// idle timer fires next.
|
||||
packDataValue(gvalue, resultdv, fold.engine, jsOwner) |
||||
} |
||||
|
||||
//export hookGoValueWriteField
|
||||
func hookGoValueWriteField(enginep, foldp unsafe.Pointer, reflectIndex, setIndex C.int, assigndv *C.DataValue) { |
||||
fold := ensureEngine(enginep, foldp) |
||||
v := reflect.ValueOf(fold.gvalue) |
||||
ve := v |
||||
for ve.Type().Kind() == reflect.Ptr { |
||||
ve = ve.Elem() |
||||
} |
||||
var field, setMethod reflect.Value |
||||
if reflectIndex >= 0 { |
||||
// It's a real field rather than a getter.
|
||||
field = ve.Field(int(reflectIndex)) |
||||
} |
||||
if setIndex >= 0 { |
||||
// It has a setter.
|
||||
setMethod = v.Method(int(setIndex)) |
||||
} |
||||
|
||||
assign := unpackDataValue(assigndv, fold.engine) |
||||
|
||||
// TODO Return false to the call site if it fails. That's how Qt seems to handle it internally.
|
||||
err := convertAndSet(field, reflect.ValueOf(assign), setMethod) |
||||
if err != nil { |
||||
panic(err.Error()) |
||||
} |
||||
} |
||||
|
||||
func convertAndSet(to, from reflect.Value, setMethod reflect.Value) (err error) { |
||||
var toType reflect.Type |
||||
if setMethod.IsValid() { |
||||
toType = setMethod.Type().In(0) |
||||
} else { |
||||
toType = to.Type() |
||||
} |
||||
fromType := from.Type() |
||||
defer func() { |
||||
// TODO This is catching more than it should. There are calls
|
||||
// to custom code below that should be isolated.
|
||||
if v := recover(); v != nil { |
||||
err = fmt.Errorf("cannot use %s as a %s", fromType, toType) |
||||
} |
||||
}() |
||||
if fromType == typeList && toType.Kind() == reflect.Slice { |
||||
list := from.Interface().(*List) |
||||
from = reflect.MakeSlice(toType, len(list.data), len(list.data)) |
||||
elemType := toType.Elem() |
||||
for i, elem := range list.data { |
||||
from.Index(i).Set(reflect.ValueOf(elem).Convert(elemType)) |
||||
} |
||||
} else if fromType == typeMap && toType.Kind() == reflect.Map { |
||||
qmap := from.Interface().(*Map) |
||||
from = reflect.MakeMap(toType) |
||||
elemType := toType.Elem() |
||||
for i := 0; i < len(qmap.data); i += 2 { |
||||
key := reflect.ValueOf(qmap.data[i]) |
||||
val := reflect.ValueOf(qmap.data[i+1]) |
||||
if val.Type() != elemType { |
||||
val = val.Convert(elemType) |
||||
} |
||||
from.SetMapIndex(key, val) |
||||
} |
||||
} else if toType != fromType { |
||||
from = from.Convert(toType) |
||||
} |
||||
if setMethod.IsValid() { |
||||
setMethod.Call([]reflect.Value{from}) |
||||
} else { |
||||
to.Set(from) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
var ( |
||||
dataValueSize = uintptr(unsafe.Sizeof(C.DataValue{})) |
||||
dataValueArray [C.MaxParams]C.DataValue |
||||
) |
||||
|
||||
//export hookGoValueCallMethod
|
||||
func hookGoValueCallMethod(enginep, foldp unsafe.Pointer, reflectIndex C.int, args *C.DataValue) { |
||||
fold := ensureEngine(enginep, foldp) |
||||
v := reflect.ValueOf(fold.gvalue) |
||||
|
||||
// TODO Must assert that v is necessarily a pointer here, but we shouldn't have to manipulate
|
||||
// gvalue here for that. This should happen in a sensible place in the wrapping functions
|
||||
// that can still error out to the user in due time.
|
||||
|
||||
method := v.Method(int(reflectIndex)) |
||||
methodt := method.Type() |
||||
methodName := v.Type().Method(int(reflectIndex)).Name |
||||
|
||||
// TODO Ensure methods with more parameters than this are not registered.
|
||||
var params [C.MaxParams]reflect.Value |
||||
var err error |
||||
|
||||
numIn := methodt.NumIn() |
||||
for i := 0; i < numIn; i++ { |
||||
paramdv := (*C.DataValue)(unsafe.Pointer(uintptr(unsafe.Pointer(args)) + (uintptr(i)+1)*dataValueSize)) |
||||
param := reflect.ValueOf(unpackDataValue(paramdv, fold.engine)) |
||||
if argt := methodt.In(i); param.Type() != argt { |
||||
param, err = convertParam(methodName, i, param, argt) |
||||
if err != nil { |
||||
panic(err.Error()) |
||||
} |
||||
} |
||||
params[i] = param |
||||
} |
||||
|
||||
result := method.Call(params[:numIn]) |
||||
|
||||
if len(result) == 1 { |
||||
packDataValue(result[0].Interface(), args, fold.engine, jsOwner) |
||||
} else if len(result) > 1 { |
||||
if len(result) > len(dataValueArray) { |
||||
panic("function has too many results") |
||||
} |
||||
for i, v := range result { |
||||
packDataValue(v.Interface(), &dataValueArray[i], fold.engine, jsOwner) |
||||
} |
||||
args.dataType = C.DTVariantList |
||||
*(*unsafe.Pointer)(unsafe.Pointer(&args.data)) = C.newVariantList(&dataValueArray[0], C.int(len(result))) |
||||
} |
||||
} |
||||
|
||||
func convertParam(methodName string, index int, param reflect.Value, argt reflect.Type) (reflect.Value, error) { |
||||
out := reflect.New(argt).Elem() |
||||
err := convertAndSet(out, param, reflect.Value{}) |
||||
if err != nil { |
||||
err = fmt.Errorf("cannot convert parameter %d of method %s from %s to %s; provided value: %#v", |
||||
index, methodName, param.Type(), argt, param.Interface()) |
||||
return reflect.Value{}, err |
||||
} |
||||
return out, nil |
||||
} |
||||
|
||||
func printPaintPanic() { |
||||
if v := recover(); v != nil { |
||||
buf := make([]byte, 8192) |
||||
runtime.Stack(buf, false) |
||||
fmt.Fprintf(os.Stderr, "panic while painting: %s\n\n%s", v, buf) |
||||
} |
||||
} |
||||
|
||||
//export hookGoValuePaint
|
||||
func hookGoValuePaint(enginep, foldp unsafe.Pointer, reflectIndex C.intptr_t) { |
||||
// Besides a convenience this is a workaround for http://golang.org/issue/8588
|
||||
defer printPaintPanic() |
||||
defer atomic.StoreUintptr(&guiPaintRef, 0) |
||||
|
||||
// The main GUI thread is mutex-locked while paint methods are called,
|
||||
// so no two paintings should be happening at the same time.
|
||||
atomic.StoreUintptr(&guiPaintRef, cdata.Ref()) |
||||
|
||||
fold := ensureEngine(enginep, foldp) |
||||
if fold.init.IsValid() { |
||||
return |
||||
} |
||||
|
||||
painter := &Painter{engine: fold.engine, obj: &Common{fold.cvalue, fold.engine}} |
||||
v := reflect.ValueOf(fold.gvalue) |
||||
method := v.Method(int(reflectIndex)) |
||||
method.Call([]reflect.Value{reflect.ValueOf(painter)}) |
||||
} |
||||
|
||||
func ensureEngine(enginep, foldp unsafe.Pointer) *valueFold { |
||||
fold := (*valueFold)(foldp) |
||||
if fold.engine != nil { |
||||
if fold.init.IsValid() { |
||||
initGoType(fold) |
||||
} |
||||
return fold |
||||
} |
||||
|
||||
if enginep == nilPtr { |
||||
panic("accessing value without an engine pointer; who created the value?") |
||||
} |
||||
engine := engines[enginep] |
||||
if engine == nil { |
||||
panic("unknown engine pointer; who created the engine?") |
||||
} |
||||
fold.engine = engine |
||||
prev := engine.values[fold.gvalue] |
||||
if prev != nil { |
||||
for prev.next != nil { |
||||
prev = prev.next |
||||
} |
||||
prev.next = fold |
||||
fold.prev = prev |
||||
} else { |
||||
engine.values[fold.gvalue] = fold |
||||
} |
||||
before := len(typeNew) |
||||
delete(typeNew, fold) |
||||
if len(typeNew) == before { |
||||
panic("value had no engine, but was not created by a registered type; who created the value?") |
||||
} |
||||
initGoType(fold) |
||||
return fold |
||||
} |
||||
|
||||
func initGoType(fold *valueFold) { |
||||
if cdata.Ref() == atomic.LoadUintptr(&guiPaintRef) { |
||||
go RunMain(func() { _initGoType(fold, true) }) |
||||
} else { |
||||
_initGoType(fold, false) |
||||
} |
||||
} |
||||
|
||||
func _initGoType(fold *valueFold, schedulePaint bool) { |
||||
if !fold.init.IsValid() { |
||||
return |
||||
} |
||||
// TODO Would be good to preserve identity on the Go side. See unpackDataValue as well.
|
||||
obj := &Common{engine: fold.engine, addr: fold.cvalue} |
||||
fold.init.Call([]reflect.Value{reflect.ValueOf(fold.gvalue), reflect.ValueOf(obj)}) |
||||
fold.init = reflect.Value{} |
||||
if schedulePaint { |
||||
obj.Call("update") |
||||
} |
||||
} |
||||
|
||||
//export hookPanic
|
||||
func hookPanic(message *C.char) { |
||||
defer C.free(unsafe.Pointer(message)) |
||||
panic(C.GoString(message)) |
||||
} |
||||
|
||||
func listSlice(fold *valueFold, reflectIndex C.intptr_t) *[]Object { |
||||
field := deref(reflect.ValueOf(fold.gvalue)).Field(int(reflectIndex)) |
||||
return field.Addr().Interface().(*[]Object) |
||||
} |
||||
|
||||
//export hookListPropertyAt
|
||||
func hookListPropertyAt(foldp unsafe.Pointer, reflectIndex, setIndex C.intptr_t, index C.int) (objp unsafe.Pointer) { |
||||
fold := (*valueFold)(foldp) |
||||
slice := listSlice(fold, reflectIndex) |
||||
return (*slice)[int(index)].Common().addr |
||||
} |
||||
|
||||
//export hookListPropertyCount
|
||||
func hookListPropertyCount(foldp unsafe.Pointer, reflectIndex, setIndex C.intptr_t) C.int { |
||||
fold := (*valueFold)(foldp) |
||||
slice := listSlice(fold, reflectIndex) |
||||
return C.int(len(*slice)) |
||||
} |
||||
|
||||
//export hookListPropertyAppend
|
||||
func hookListPropertyAppend(foldp unsafe.Pointer, reflectIndex, setIndex C.intptr_t, objp unsafe.Pointer) { |
||||
fold := (*valueFold)(foldp) |
||||
slice := listSlice(fold, reflectIndex) |
||||
var objdv C.DataValue |
||||
objdv.dataType = C.DTObject |
||||
*(*unsafe.Pointer)(unsafe.Pointer(&objdv.data)) = objp |
||||
newslice := append(*slice, unpackDataValue(&objdv, fold.engine).(Object)) |
||||
if setIndex >= 0 { |
||||
reflect.ValueOf(fold.gvalue).Method(int(setIndex)).Call([]reflect.Value{reflect.ValueOf(newslice)}) |
||||
} else { |
||||
*slice = newslice |
||||
} |
||||
} |
||||
|
||||
//export hookListPropertyClear
|
||||
func hookListPropertyClear(foldp unsafe.Pointer, reflectIndex, setIndex C.intptr_t) { |
||||
fold := (*valueFold)(foldp) |
||||
slice := listSlice(fold, reflectIndex) |
||||
newslice := (*slice)[0:0] |
||||
if setIndex >= 0 { |
||||
reflect.ValueOf(fold.gvalue).Method(int(setIndex)).Call([]reflect.Value{reflect.ValueOf(newslice)}) |
||||
} else { |
||||
for i := range *slice { |
||||
(*slice)[i] = nil |
||||
} |
||||
*slice = newslice |
||||
} |
||||
} |
@ -1,6 +0,0 @@ |
||||
// Package cdata supports the implementation of the qml package.
|
||||
package cdata |
||||
|
||||
func Ref() uintptr |
||||
|
||||
func Addrs() (uintptr, uintptr) |