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:
- label: example.com
script: curl -o /dev/null -s -w '%{time_total}' http://example.com
color: red
- label: google.com
script: curl -o /dev/null -s -w '%{time_total}' http://google.com
color: yellow
refresh-rate-ms: 100
refresh-rate-ms: 200
time-scale-sec: 1
style: dots/lines
position:
x: 1
y: 2
x: 0
y: 0
size:
x: 3
y: 4
x: 15
y: 15
- title: mongo-count
data:
- label: posts
script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2
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
import (
. "github.com/sqshq/vcmd/layout"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
)
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 {

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 (
ui "github.com/sqshq/termui"
"github.com/sqshq/vcmd/config"
"github.com/sqshq/vcmd/data"
"github.com/sqshq/vcmd/layout"
"github.com/sqshq/vcmd/widgets"
"log"
"time"
)
/*
TODO validation
- title uniquness and mandatory within a single type of widget
- label uniqueness and mandatory (if > 1 data bullets)
*/
func main() {
// todo error handling + validation
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 {
//log.Fatalf("failed to initialize termui: %v", err)
log.Fatalf("failed to initialize termui: %v", err)
}
defer ui.Close()
uiEvents := ui.PollEvents()
events := ui.PollEvents()
layout := widgets.NewLayout(ui.TerminalDimensions())
layout.AddItem(p1, 0, 0, 6, 6)
layout.AddItem(p2, 0, 6, 6, 12)
pollers := make([]data.Poller, 0)
lout := layout.NewLayout(ui.TerminalDimensions())
dataTicker := time.NewTicker(200 * time.Millisecond)
uiTicker := time.NewTicker(50 * time.Millisecond)
for _, chartConfig := range cfg.LineChartConfigs {
pause := false
chart := widgets.NewTimePlot(chartConfig.Title)
lout.AddItem(chart, chartConfig.Position, chartConfig.Size)
go func() {
for {
select {
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)
for _, chartData := range chartConfig.DataConfig {
pollers = append(pollers,
data.NewPoller(chart, chartData.Script, chartData.Label, chartConfig.RefreshRateMs))
}
}
}
}()
ticker := time.NewTicker(50 * time.Millisecond)
for {
select {
case e := <-uiEvents:
case e := <-events:
switch e.ID {
case "q", "<C-c>": // press 'q' or 'C-c' to quit
case "q", "<C-c>":
return
case "<Resize>":
payload := e.Payload.(ui.Resize)
layout.ChangeDimensions(payload.Width, payload.Height)
}
//case "<MouseLeft>":
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)
//}
}
switch e.Type {
case ui.KeyboardEvent: // handle all key presses
//log.Printf("key: %v", e.ID)
case ui.KeyboardEvent:
switch e.ID {
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>":
layout.MoveItem(1, 0)
//lout.GetItem(0).MoveItem(1, 0)
case "<Down>":
layout.MoveItem(0, 1)
//lout.GetItem(0).MoveItem(0, 1)
case "<Up>":
layout.MoveItem(0, -1)
//lout.GetItem(0).MoveItem(0, -1)
case "p":
pause = !pause
}
}
case <-uiTicker.C:
if !pause {
ui.Render(layout)
for _, poller := range pollers {
poller.TogglePause()
}
}
}
case <-ticker.C:
ui.Render(lout)
}
}
}

View File

@ -9,10 +9,10 @@ const (
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}
}
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}
}

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