diff --git a/config.yml b/config.yml index f31c5a8..b609559 100644 --- a/config.yml +++ b/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 \ No newline at end of file diff --git a/config/config.go b/config/config.go index 60e1a64..4282d83 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/config/data.go b/config/data.go deleted file mode 100644 index e66204c..0000000 --- a/config/data.go +++ /dev/null @@ -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 -} diff --git a/config/linechart.go b/config/linechart.go deleted file mode 100644 index 99366c5..0000000 --- a/config/linechart.go +++ /dev/null @@ -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"` -} diff --git a/config/position.go b/config/position.go deleted file mode 100644 index 182b076..0000000 --- a/config/position.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type Position struct { - X int `yaml:"x"` - Y int `yaml:"y"` -} diff --git a/config/size.go b/config/size.go deleted file mode 100644 index 0269394..0000000 --- a/config/size.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type Size struct { - X int `yaml:"x"` - Y int `yaml:"y"` -} diff --git a/data/consumer.go b/data/consumer.go new file mode 100644 index 0000000..0bc8722 --- /dev/null +++ b/data/consumer.go @@ -0,0 +1,6 @@ +package data + +type Consumer interface { + ConsumeValue(value string, label string) + ConsumeError(err error) +} diff --git a/data/poller.go b/data/poller.go new file mode 100644 index 0000000..4e3e2be --- /dev/null +++ b/data/poller.go @@ -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) +} diff --git a/layout/item.go b/layout/item.go new file mode 100644 index 0000000..93b0915 --- /dev/null +++ b/layout/item.go @@ -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 +} diff --git a/layout/layout.go b/layout/layout.go new file mode 100644 index 0000000..5a71c58 --- /dev/null +++ b/layout/layout.go @@ -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) + } +} diff --git a/main.go b/main.go index f4c9f7d..084c3f8 100644 --- a/main.go +++ b/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", "": // press 'q' or 'C-c' to quit + case "q", "": return case "": payload := e.Payload.(ui.Resize) - layout.ChangeDimensions(payload.Width, payload.Height) + lout.ChangeDimensions(payload.Width, payload.Height) + case "": + //payload := e.Payload.(ui.Mouse) + //x, y := payload.X, payload.Y + //log.Printf("x: %v, y: %v", x, y) } - //case "": - // 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 "": - layout.MoveItem(-1, 0) + // here we are going to move selection (special type of layout item) + //lout.GetItem("").MoveItem(-1, 0) case "": - layout.MoveItem(1, 0) + //lout.GetItem(0).MoveItem(1, 0) case "": - layout.MoveItem(0, 1) + //lout.GetItem(0).MoveItem(0, 1) case "": - layout.MoveItem(0, -1) + //lout.GetItem(0).MoveItem(0, -1) case "p": - pause = !pause + for _, poller := range pollers { + poller.TogglePause() + } } } - case <-uiTicker.C: - if !pause { - ui.Render(layout) - } + case <-ticker.C: + ui.Render(lout) } } } diff --git a/widgets/geometry.go b/widgets/geometry.go index 18adfe1..9a1c439 100644 --- a/widgets/geometry.go +++ b/widgets/geometry.go @@ -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} } diff --git a/widgets/layout.go b/widgets/layout.go deleted file mode 100644 index 3d909a4..0000000 --- a/widgets/layout.go +++ /dev/null @@ -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) - } -} diff --git a/widgets/timeplot.go b/widgets/timeplot.go index d7a29ad..9fee4f4 100644 --- a/widgets/timeplot.go +++ b/widgets/timeplot.go @@ -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) - } + 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 ) }