diff --git a/config.yml b/config.yml index f92696d..4e388cd 100644 --- a/config.yml +++ b/config.yml @@ -1,22 +1,26 @@ theme: dark run-charts: - - title: curl-latency + - title: CURL-LATENCY data: - - label: google.com + - label: GOOGLE script: curl -o /dev/null -s -w '%{time_total}' http://google.com - - label: yahoo.com + - label: YAHOO script: curl -o /dev/null -s -w '%{time_total}' http://yahoo.com - - label: example.com - script: curl -o /dev/null -s -w '%{time_total}' http://example.com + - label: YANDEX + script: curl -o /dev/null -s -w '%{time_total}' http://yandex.com refresh-rate-ms: 300 time-scale-sec: 1 + decimal-places: 1 + legend: + labels: true + details: true position: x: 0 y: 0 size: x: 15 y: 15 - - title: mongo-count + - title: MONGO-COUNT data: - label: posts script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2 diff --git a/config/config.go b/config/config.go index 21c4621..61473db 100644 --- a/config/config.go +++ b/config/config.go @@ -2,19 +2,19 @@ package config import ( "github.com/sqshq/vcmd/data" - . "github.com/sqshq/vcmd/layout" "github.com/sqshq/vcmd/settings" + . "github.com/sqshq/vcmd/widgets" "gopkg.in/yaml.v2" "io/ioutil" "log" ) type Config struct { - Theme settings.Theme `yaml:"theme"` - RunCharts []RunChart `yaml:"run-charts"` + Theme settings.Theme `yaml:"theme"` + RunCharts []RunChartConfig `yaml:"run-charts"` } -type RunChart struct { +type RunChartConfig struct { Title string `yaml:"title"` Items []data.Item `yaml:"data"` Position Position `yaml:"position"` diff --git a/data/poller.go b/data/poller.go index 9e21379..a66da17 100644 --- a/data/poller.go +++ b/data/poller.go @@ -7,13 +7,12 @@ import ( type Poller struct { consumer Consumer item Item - pause bool } func NewPoller(consumer Consumer, item Item, rateMs int) Poller { ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond))) - poller := Poller{consumer, item, false} + poller := Poller{consumer, item} go func() { for { @@ -27,16 +26,8 @@ func NewPoller(consumer Consumer, item Item, rateMs int) Poller { return poller } -func (self *Poller) TogglePause() { - self.pause = !self.pause -} - func (self *Poller) poll() { - if self.pause { - return - } - value, err := self.item.nextValue() if err != nil { diff --git a/main.go b/main.go index 5d0e908..77e5299 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ 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/settings" "github.com/sqshq/vcmd/widgets" "log" "time" @@ -12,6 +12,8 @@ import ( func main() { + print("\033]0;vcmd\007") + cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml") if err := ui.Init(); err != nil { @@ -22,7 +24,7 @@ func main() { events := ui.PollEvents() pollers := make([]data.Poller, 0) - lout := layout.NewLayout(ui.TerminalDimensions()) + lout := widgets.NewLayout(ui.TerminalDimensions()) for _, chartConfig := range cfg.RunCharts { @@ -36,17 +38,18 @@ func main() { } ticker := time.NewTicker(30 * time.Millisecond) + pause := false for { select { case e := <-events: switch e.ID { - case "q", "": + case settings.EventQuit, settings.EventExit: return - case "": + case settings.EventResize: payload := e.Payload.(ui.Resize) lout.ChangeDimensions(payload.Width, payload.Height) - case "": + case settings.EventMouseClick: //payload := e.Payload.(ui.Mouse) //x, y := payload.X, payload.Y //log.Printf("x: %v, y: %v", x, y) @@ -54,23 +57,23 @@ func main() { switch e.Type { case ui.KeyboardEvent: switch e.ID { - case "": + case settings.EventKeyboardLeft: // here we are going to move selection (special type of layout item) //lout.GetItem("").Move(-1, 0) - case "": + case settings.EventKeyboardRight: //lout.GetItem(0).Move(1, 0) - case "": + case settings.EventKeyboardDown: //lout.GetItem(0).Move(0, 1) - case "": + case settings.EventKeyboardUp: //lout.GetItem(0).Move(0, -1) - case "p": - for _, poller := range pollers { - poller.TogglePause() - } + case settings.EventPause: + pause = !pause } } case <-ticker.C: - ui.Render(lout) + if !pause { + ui.Render(lout) + } } } } diff --git a/settings/event.go b/settings/event.go new file mode 100644 index 0000000..037b1b8 --- /dev/null +++ b/settings/event.go @@ -0,0 +1,15 @@ +package settings + +type Event string + +const ( + EventPause = "p" + EventQuit = "q" + EventResize = "" + EventExit = "" + EventMouseClick = "" + EventKeyboardLeft = "" + EventKeyboardRight = "" + EventKeyboardUp = "" + EventKeyboardDown = "" +) diff --git a/layout/component.go b/widgets/component.go similarity index 96% rename from layout/component.go rename to widgets/component.go index 99e3e08..8bfdef3 100644 --- a/layout/component.go +++ b/widgets/component.go @@ -1,4 +1,4 @@ -package layout +package widgets import ( . "github.com/sqshq/termui" diff --git a/layout/layout.go b/widgets/layout.go similarity index 98% rename from layout/layout.go rename to widgets/layout.go index 75d1a93..c0015e0 100644 --- a/layout/layout.go +++ b/widgets/layout.go @@ -1,4 +1,4 @@ -package layout +package widgets import ( . "github.com/sqshq/termui" diff --git a/widgets/runchart.go b/widgets/runchart.go index d31d829..916944c 100644 --- a/widgets/runchart.go +++ b/widgets/runchart.go @@ -20,6 +20,7 @@ const ( xAxisLabelsGap = 2 yAxisLabelsWidth = 5 yAxisLabelsGap = 1 + xAxisLegendWidth = 15 ) type RunChart struct { @@ -79,7 +80,7 @@ func (self *RunChart) newChartGrid() ChartGrid { paddingWidth: xAxisLabelsGap + xAxisLabelsWidth, maxTimeWidth: self.Inner.Max.X - xAxisLabelsWidth/2 - xAxisLabelsGap, timeExtremum: GetTimeExtremum(linesCount, paddingDuration), - valueExtremum: GetValueExtremum(self.lines), + valueExtremum: GetChartValueExtremum(self.lines), } } @@ -88,7 +89,7 @@ func (self *RunChart) Draw(buf *Buffer) { self.mutex.Lock() self.Block.Draw(buf) self.grid = self.newChartGrid() - self.plotAxes(buf) + self.renderAxes(buf) drawArea := image.Rect( self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y, @@ -96,6 +97,7 @@ func (self *RunChart) Draw(buf *Buffer) { ) self.renderItems(buf, drawArea) + self.renderLegend(buf, drawArea) self.mutex.Unlock() } @@ -151,7 +153,61 @@ func (self *RunChart) trimOutOfRangeValues() { } } -func (self *RunChart) renderItems(buf *Buffer, drawArea image.Rectangle) { +func (self *RunChart) renderAxes(buffer *Buffer) { + // draw origin cell + buffer.SetCell( + NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)), + image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1), + ) + + // draw x axis line + for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ { + buffer.SetCell( + NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)), + image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1), + ) + } + + // draw grid + for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-1; y = y + 2 { + for x := 0; x < self.grid.linesCount; x++ { + buffer.SetCell( + NewCell(VERTICAL_DASH, NewStyle(settings.ColorDarkGrey)), + image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1), + ) + } + } + + // draw y axis line + for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ { + buffer.SetCell( + NewCell(VERTICAL_DASH, NewStyle(ColorWhite)), + image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y), + ) + } + + // draw x axis time labels + for i := 0; i < self.grid.linesCount; i++ { + labelTime := self.grid.timeExtremum.max.Add(time.Duration(-i) * time.Second) + buffer.SetString( + labelTime.Format("15:04:05"), + NewStyle(ColorWhite), + image.Pt(self.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(self.grid.paddingWidth), self.Inner.Max.Y-1), + ) + } + + // draw y axis labels + verticalScale := self.grid.valueExtremum.max - self.grid.valueExtremum.min/float64(self.Inner.Dy()-xAxisLabelsHeight-1) + for i := 1; i*(yAxisLabelsGap+1) <= self.Inner.Dy()-1; i++ { + buffer.SetString( + fmt.Sprintf("%.3f", float64(i)*self.grid.valueExtremum.min*verticalScale*(yAxisLabelsGap+1)), + NewStyle(ColorWhite), + image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2), + ) + } +} + +func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) { canvas := NewCanvas() canvas.Rectangle = drawArea @@ -193,7 +249,7 @@ func (self *RunChart) renderItems(buf *Buffer, drawArea image.Rectangle) { previousPoint = xToPoint[pointsOrder[i-1]] } - //buf.SetCell( + //buffer.SetCell( // NewCell(self.DotRune, NewStyle(SelectColor(self.LineColors, 0))), // currentPoint, //) @@ -206,68 +262,47 @@ func (self *RunChart) renderItems(buf *Buffer, drawArea image.Rectangle) { } } - canvas.Draw(buf) + canvas.Draw(buffer) +} + +func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle) { + for i, line := range self.lines { + + extremum := GetLineValueExtremum(line.points) + + buffer.SetString( + fmt.Sprintf("•"), + NewStyle(line.item.Color), + image.Pt(self.Inner.Max.X-xAxisLegendWidth-2, self.Inner.Min.Y+1+i*5), + ) + buffer.SetString( + fmt.Sprintf("%s", line.item.Label), + NewStyle(line.item.Color), + image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+1+i*5), + ) + buffer.SetString( + fmt.Sprintf("max %.3f", extremum.max), + NewStyle(ColorWhite), + image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5), + ) + buffer.SetString( + fmt.Sprintf("min %.3f", extremum.min), + NewStyle(ColorWhite), + image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+3+i*5), + ) + buffer.SetString( + fmt.Sprintf("cur %.3f", line.points[len(line.points)-1].Value), + NewStyle(ColorWhite), + image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5), + ) + } } func (self *RunChart) isTimePointInRange(point TimePoint) bool { return point.Time.After(self.grid.timeExtremum.min.Add(self.grid.paddingDuration)) } -func (self *RunChart) plotAxes(buf *Buffer) { - // draw origin cell - buf.SetCell( - NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)), - image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1), - ) - - // draw x axis line - for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ { - buf.SetCell( - NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)), - image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1), - ) - } - - // draw grid - for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-1; y = y + 2 { - for x := 0; x < self.grid.linesCount; x++ { - buf.SetCell( - NewCell(VERTICAL_DASH, NewStyle(settings.ColorDarkGrey)), - image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1), - ) - } - } - - // draw y axis line - for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ { - buf.SetCell( - NewCell(VERTICAL_DASH, NewStyle(ColorWhite)), - image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y), - ) - } - - // draw x axis time labels - for i := 0; i < self.grid.linesCount; i++ { - labelTime := self.grid.timeExtremum.max.Add(time.Duration(-i) * time.Second) - buf.SetString( - labelTime.Format("15:04:05"), - NewStyle(ColorWhite), - image.Pt(self.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(self.grid.paddingWidth), self.Inner.Max.Y-1), - ) - } - - // draw y axis labels - verticalScale := self.grid.valueExtremum.max - self.grid.valueExtremum.min/float64(self.Inner.Dy()-xAxisLabelsHeight-1) - for i := 1; i*(yAxisLabelsGap+1) <= self.Inner.Dy()-1; i++ { - buf.SetString( - fmt.Sprintf("%.3f", float64(i)*self.grid.valueExtremum.min*verticalScale*(yAxisLabelsGap+1)), - NewStyle(ColorWhite), - image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2), - ) - } -} - -func GetValueExtremum(items []TimeLine) ValueExtremum { +func GetChartValueExtremum(items []TimeLine) ValueExtremum { if len(items) == 0 { return ValueExtremum{0, 0} @@ -289,6 +324,26 @@ func GetValueExtremum(items []TimeLine) ValueExtremum { return ValueExtremum{max: max, min: min} } +func GetLineValueExtremum(points []TimePoint) ValueExtremum { + + if len(points) == 0 { + return ValueExtremum{0, 0} + } + + var max, min = -math.MaxFloat64, math.MaxFloat64 + + for _, point := range points { + if point.Value > max { + max = point.Value + } + if point.Value < min { + min = point.Value + } + } + + return ValueExtremum{max: max, min: min} +} + func GetTimeExtremum(linesCount int, paddingDuration time.Duration) TimeExtremum { maxTime := time.Now() return TimeExtremum{