poller implementation
This commit is contained in:
parent
34a3c0845d
commit
dd146c72f0
20
config.yml
20
config.yml
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package config
|
||||
|
||||
type Position struct {
|
||||
X int `yaml:"x"`
|
||||
Y int `yaml:"y"`
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package config
|
||||
|
||||
type Size struct {
|
||||
X int `yaml:"x"`
|
||||
Y int `yaml:"y"`
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package data
|
||||
|
||||
type Consumer interface {
|
||||
ConsumeValue(value string, label string)
|
||||
ConsumeError(err error)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
94
main.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue