poller implementation

This commit is contained in:
sqshq 2019-01-30 19:02:38 -05:00
parent 34a3c0845d
commit dd146c72f0
14 changed files with 232 additions and 225 deletions

View File

@ -4,21 +4,27 @@ line-charts:
data: data:
- label: example.com - label: example.com
script: curl -o /dev/null -s -w '%{time_total}' http://example.com script: curl -o /dev/null -s -w '%{time_total}' http://example.com
color: red
- label: google.com - label: google.com
script: curl -o /dev/null -s -w '%{time_total}' http://google.com script: curl -o /dev/null -s -w '%{time_total}' http://google.com
color: yellow refresh-rate-ms: 200
refresh-rate-ms: 100
time-scale-sec: 1 time-scale-sec: 1
style: dots/lines style: dots/lines
position: position:
x: 1 x: 0
y: 2 y: 0
size: size:
x: 3 x: 15
y: 4 y: 15
- title: mongo-count - title: mongo-count
data: data:
- label: posts - label: posts
script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2 script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2
color: red color: red
refresh-rate-ms: 200
time-scale-sec: 1
position:
x: 15
y: 0
size:
x: 15
y: 15

View File

@ -1,13 +1,28 @@
package config package config
import ( import (
. "github.com/sqshq/vcmd/layout"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"log" "log"
) )
type Config struct { type Config struct {
LineCharts []LineChartConfig `yaml:"line-charts"` LineChartConfigs []LineChartConfig `yaml:"line-charts"`
}
type DataConfig struct {
Script string `yaml:"script"`
Label string `yaml:"label"`
}
type LineChartConfig struct {
Title string `yaml:"title"`
DataConfig []DataConfig `yaml:"data"`
Position Position `yaml:"position"`
Size Size `yaml:"size"`
RefreshRateMs int `yaml:"refresh-rate-ms"`
TimeScaleSec int `yaml:"time-scale-sec"`
} }
func Load(location string) *Config { func Load(location string) *Config {

View File

@ -1,31 +0,0 @@
package config
import (
"log"
"os/exec"
"strconv"
"strings"
)
type Data struct {
Label string `yaml:"label"`
Color string `yaml:"color"`
Script string `yaml:"script"`
}
func (d *Data) NextValue() (float64, error) {
output, err := exec.Command("sh", "-c", d.Script).Output()
if err != nil {
log.Printf("%s", err)
}
trimmedOutput := strings.TrimSpace(string(output))
floatValue, err := strconv.ParseFloat(trimmedOutput, 64)
if err != nil {
return 0, err
}
return floatValue, nil
}

View File

@ -1,10 +0,0 @@
package config
type LineChartConfig struct {
Title string `yaml:"title"`
Data []Data `yaml:"data"`
Position Position `yaml:"position"`
Size Size `yaml:"size"`
RefreshRateMs int `yaml:"refresh-rate-ms"`
Scale string `yaml:"scale"`
}

View File

@ -1,6 +0,0 @@
package config
type Position struct {
X int `yaml:"x"`
Y int `yaml:"y"`
}

View File

@ -1,6 +0,0 @@
package config
type Size struct {
X int `yaml:"x"`
Y int `yaml:"y"`
}

6
data/consumer.go Normal file
View File

@ -0,0 +1,6 @@
package data
type Consumer interface {
ConsumeValue(value string, label string)
ConsumeError(err error)
}

51
data/poller.go Normal file
View File

@ -0,0 +1,51 @@
package data
import (
"os/exec"
"strings"
"time"
)
type Poller struct {
consumer Consumer
script string
label string
pause bool
}
func NewPoller(consumer Consumer, script string, label string, rateMs int) Poller {
ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond)))
poller := Poller{consumer, script, label, false}
go func() {
for {
select {
case <-ticker.C:
poller.poll()
}
}
}()
return poller
}
func (self *Poller) TogglePause() {
self.pause = !self.pause
}
func (self *Poller) poll() {
if self.pause {
return
}
output, err := exec.Command("sh", "-c", self.script).Output()
if err != nil {
self.consumer.ConsumeError(err)
}
value := strings.TrimSpace(string(output))
self.consumer.ConsumeValue(value, self.label)
}

31
layout/item.go Normal file
View File

@ -0,0 +1,31 @@
package layout
import (
. "github.com/sqshq/termui"
)
type Item struct {
Data Drawable
Position Position
Size Size
}
type Position struct {
X int `yaml:"x"`
Y int `yaml:"y"`
}
type Size struct {
X int `yaml:"x"`
Y int `yaml:"y"`
}
func (self *Item) MoveItem(x, y int) {
self.Position.X += x
self.Position.Y += y
}
func (self *Item) ResizeItem(x, y int) {
self.Size.X += x
self.Size.Y += y
}

51
layout/layout.go Normal file
View File

@ -0,0 +1,51 @@
package layout
import (
. "github.com/sqshq/termui"
)
type Layout struct {
Block
items []Item
}
const (
columnsCount = 30
rowsCount = 30
)
func NewLayout(width, height int) *Layout {
block := *NewBlock()
block.SetRect(0, 0, width, height)
return &Layout{
Block: block,
items: make([]Item, 0),
}
}
func (self *Layout) AddItem(drawable Drawable, position Position, size Size) {
self.items = append(self.items, Item{drawable, position, size})
}
func (self *Layout) ChangeDimensions(width, height int) {
self.SetRect(0, 0, width, height)
}
func (self *Layout) Draw(buf *Buffer) {
columnWidth := float64(self.GetRect().Dx()) / columnsCount
rowHeight := float64(self.GetRect().Dy()) / rowsCount
for _, item := range self.items {
x1 := float64(item.Position.X) * columnWidth
y1 := float64(item.Position.Y) * rowHeight
x2 := x1 + float64(item.Size.X)*columnWidth
y2 := y1 + float64(item.Size.Y)*rowHeight
item.Data.SetRect(int(x1), int(y1), int(x2), int(y2))
item.Data.Draw(buf)
}
}

94
main.go
View File

@ -3,100 +3,80 @@ package main
import ( import (
ui "github.com/sqshq/termui" ui "github.com/sqshq/termui"
"github.com/sqshq/vcmd/config" "github.com/sqshq/vcmd/config"
"github.com/sqshq/vcmd/data"
"github.com/sqshq/vcmd/layout"
"github.com/sqshq/vcmd/widgets" "github.com/sqshq/vcmd/widgets"
"log" "log"
"time" "time"
) )
/*
TODO validation
- title uniquness and mandatory within a single type of widget
- label uniqueness and mandatory (if > 1 data bullets)
*/
func main() { func main() {
// todo error handling + validation
cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml") cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml")
for _, linechart := range cfg.LineCharts {
for _, data := range linechart.Data {
value, _ := data.NextValue()
log.Printf("%s: %s - %v", linechart.Title, data.Label, value)
}
}
p1 := widgets.NewTimePlot()
p1.Title = " CURL LATENCY STATISTICS (sec) "
p1.LineColors[0] = ui.ColorYellow
p1.Marker = widgets.MarkerBraille
p2 := widgets.NewTimePlot()
p2.Title = " CURL LATENCY STATISTICS 2 (sec) "
p2.LineColors[0] = ui.ColorYellow
p2.Marker = widgets.MarkerBraille
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
//log.Fatalf("failed to initialize termui: %v", err) log.Fatalf("failed to initialize termui: %v", err)
} }
defer ui.Close() defer ui.Close()
uiEvents := ui.PollEvents() events := ui.PollEvents()
layout := widgets.NewLayout(ui.TerminalDimensions()) pollers := make([]data.Poller, 0)
layout.AddItem(p1, 0, 0, 6, 6) lout := layout.NewLayout(ui.TerminalDimensions())
layout.AddItem(p2, 0, 6, 6, 12)
dataTicker := time.NewTicker(200 * time.Millisecond) for _, chartConfig := range cfg.LineChartConfigs {
uiTicker := time.NewTicker(50 * time.Millisecond)
pause := false chart := widgets.NewTimePlot(chartConfig.Title)
lout.AddItem(chart, chartConfig.Position, chartConfig.Size)
go func() { for _, chartData := range chartConfig.DataConfig {
for { pollers = append(pollers,
select { data.NewPoller(chart, chartData.Script, chartData.Label, chartConfig.RefreshRateMs))
case <-dataTicker.C:
if !pause {
value, err := cfg.LineCharts[0].Data[0].NextValue()
if err != nil {
log.Printf("failed to get value: %s", err)
break
}
p1.AddValue(value)
p2.AddValue(value)
} }
} }
}
}() ticker := time.NewTicker(50 * time.Millisecond)
for { for {
select { select {
case e := <-uiEvents: case e := <-events:
switch e.ID { switch e.ID {
case "q", "<C-c>": // press 'q' or 'C-c' to quit case "q", "<C-c>":
return return
case "<Resize>": case "<Resize>":
payload := e.Payload.(ui.Resize) payload := e.Payload.(ui.Resize)
layout.ChangeDimensions(payload.Width, payload.Height) lout.ChangeDimensions(payload.Width, payload.Height)
case "<MouseLeft>":
//payload := e.Payload.(ui.Mouse)
//x, y := payload.X, payload.Y
//log.Printf("x: %v, y: %v", x, y)
} }
//case "<MouseLeft>":
// payload := e.Payload.(ui.Mouse)
// x, y := payload.X, payload.Y
// log.Printf("x: %v, y: %v", x, y)
//}
switch e.Type { switch e.Type {
case ui.KeyboardEvent: // handle all key presses case ui.KeyboardEvent:
//log.Printf("key: %v", e.ID)
switch e.ID { switch e.ID {
case "<Left>": case "<Left>":
layout.MoveItem(-1, 0) // here we are going to move selection (special type of layout item)
//lout.GetItem("").MoveItem(-1, 0)
case "<Right>": case "<Right>":
layout.MoveItem(1, 0) //lout.GetItem(0).MoveItem(1, 0)
case "<Down>": case "<Down>":
layout.MoveItem(0, 1) //lout.GetItem(0).MoveItem(0, 1)
case "<Up>": case "<Up>":
layout.MoveItem(0, -1) //lout.GetItem(0).MoveItem(0, -1)
case "p": case "p":
pause = !pause for _, poller := range pollers {
poller.TogglePause()
} }
} }
case <-uiTicker.C:
if !pause {
ui.Render(layout)
} }
case <-ticker.C:
ui.Render(lout)
} }
} }
} }

View File

@ -9,10 +9,10 @@ const (
yBrailleMultiplier = 4 yBrailleMultiplier = 4
) )
func braille(point image.Point) image.Point { func braillePoint(point image.Point) image.Point {
return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier} return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier}
} }
func deBraille(point image.Point) image.Point { func debraillePoint(point image.Point) image.Point {
return image.Point{X: point.X / xBrailleMultiplier, Y: point.Y / yBrailleMultiplier} return image.Point{X: point.X / xBrailleMultiplier, Y: point.Y / yBrailleMultiplier}
} }

View File

@ -1,81 +0,0 @@
package widgets
import (
. "github.com/sqshq/termui"
)
type Item struct {
drawable Drawable
coordinates ItemCoordinates
}
type ItemCoordinates struct {
x1 int
y1 int
x2 int
y2 int
}
type LayoutDimensions struct {
width int
height int
}
type Layout struct {
Block
dimensions LayoutDimensions
items []Item
}
const (
columnsCount = 12
rowsCount = 12
)
func NewLayout(width, height int) *Layout {
b := *NewBlock()
b.SetRect(0, 0, width, height)
return &Layout{
Block: b,
dimensions: LayoutDimensions{width, height},
items: make([]Item, 0),
}
}
func (self *Layout) AddItem(drawable interface{}, x1, y1, x2, y2 int) {
self.items = append(self.items, Item{
drawable: drawable.(Drawable),
coordinates: ItemCoordinates{x1, y1, x2, y2},
})
}
func (self *Layout) MoveItem(x, y int) {
self.items[0].coordinates.x1 += x
self.items[0].coordinates.y1 += y
self.items[0].coordinates.x2 += x
self.items[0].coordinates.y2 += y
}
func (self *Layout) ChangeDimensions(width, height int) {
self.dimensions = LayoutDimensions{width, height}
self.SetRect(0, 0, width, height)
}
func (self *Layout) Draw(buf *Buffer) {
columnWidth := float64(self.dimensions.width) / columnsCount
rowHeight := float64(self.dimensions.height) / rowsCount
for _, item := range self.items {
x1 := float64(item.coordinates.x1) * columnWidth
y1 := float64(item.coordinates.y1) * rowHeight
x2 := float64(item.coordinates.x2) * columnWidth
y2 := float64(item.coordinates.y2) * rowHeight
item.drawable.SetRect(int(x1), int(y1), int(x2), int(y2))
item.drawable.Draw(buf)
}
}

View File

@ -3,21 +3,21 @@ package widgets
import ( import (
"fmt" "fmt"
"image" "image"
"log"
"strconv"
"sync" "sync"
"time" "time"
. "github.com/sqshq/termui" . "github.com/sqshq/termui"
) )
type TimePlot struct { type TimePlot struct { // TODO rename to linechart
Block Block
DataLabels []string DataLabels []string
MaxValueTimePoint TimePoint MaxValueTimePoint TimePoint
LineColors []Color LineColors []Color
ShowAxes bool
DotRune rune DotRune rune
HorizontalScale int HorizontalScale int
Marker PlotMarker
timePoints []TimePoint timePoints []TimePoint
dataMutex *sync.Mutex dataMutex *sync.Mutex
@ -37,21 +37,15 @@ type TimePoint struct {
Time time.Time Time time.Time
} }
type PlotMarker uint func NewTimePlot(title string) *TimePlot {
block := *NewBlock()
const ( block.Title = title
MarkerBraille PlotMarker = iota //self.LineColors[0] = ui.ColorYellow
MarkerDot
)
func NewTimePlot() *TimePlot {
return &TimePlot{ return &TimePlot{
Block: *NewBlock(), Block: block,
LineColors: Theme.Plot.Lines, LineColors: Theme.Plot.Lines,
DotRune: DOT, DotRune: DOT,
HorizontalScale: 1, HorizontalScale: 1,
ShowAxes: true,
Marker: MarkerBraille,
timePoints: make([]TimePoint, 0), timePoints: make([]TimePoint, 0),
dataMutex: &sync.Mutex{}, dataMutex: &sync.Mutex{},
} }
@ -93,10 +87,7 @@ func (self *TimePlot) Draw(buf *Buffer) {
self.dataMutex.Lock() self.dataMutex.Lock()
self.Block.Draw(buf) self.Block.Draw(buf)
self.grid = self.newPlotGrid() self.grid = self.newPlotGrid()
if self.ShowAxes {
self.plotAxes(buf) self.plotAxes(buf)
}
drawArea := image.Rect( drawArea := image.Rect(
self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y, self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y,
@ -107,13 +98,23 @@ func (self *TimePlot) Draw(buf *Buffer) {
self.dataMutex.Unlock() self.dataMutex.Unlock()
} }
func (self *TimePlot) AddValue(value float64) { func (self *TimePlot) ConsumeValue(value string, label string) {
float, err := strconv.ParseFloat(value, 64)
if err != nil {
log.Fatalf("Expected float number, but got %v", value) // TODO visual notification
}
self.dataMutex.Lock() self.dataMutex.Lock()
self.timePoints = append(self.timePoints, TimePoint{Value: value, Time: time.Now()}) self.timePoints = append(self.timePoints, TimePoint{Value: float, Time: time.Now()})
self.trimOutOfRangeValues() self.trimOutOfRangeValues()
self.dataMutex.Unlock() self.dataMutex.Unlock()
} }
func (self *TimePlot) ConsumeError(err error) {
// TODO visual notification
}
func (self *TimePlot) trimOutOfRangeValues() { func (self *TimePlot) trimOutOfRangeValues() {
lastOutOfRangeValueIndex := -1 lastOutOfRangeValueIndex := -1
@ -175,8 +176,8 @@ func (self *TimePlot) renderBraille(buf *Buffer, drawArea image.Rectangle) {
//) //)
canvas.Line( canvas.Line(
braille(previousPoint), braillePoint(previousPoint),
braille(currentPoint), braillePoint(currentPoint),
SelectColor(self.LineColors, 0), //i SelectColor(self.LineColors, 0), //i
) )
} }