added palette

This commit is contained in:
sqshq 2019-01-31 18:40:05 -05:00
parent e977d045f6
commit 843375bc14
9 changed files with 149 additions and 75 deletions

View File

@ -1,12 +1,14 @@
theme: dark / bright theme: dark
run-charts: run-charts:
- title: curl-latency - title: curl-latency
data: data:
- label: example.com
script: curl -o /dev/null -s -w '%{time_total}' http://example.com
- 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
refresh-rate-ms: 200 - label: yahoo.com
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
refresh-rate-ms: 300
time-scale-sec: 1 time-scale-sec: 1
style: dots/lines style: dots/lines
position: position:
@ -19,7 +21,7 @@ run-charts:
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: 3
refresh-rate-ms: 200 refresh-rate-ms: 200
time-scale-sec: 1 time-scale-sec: 1
position: position:

View File

@ -1,32 +1,39 @@
package config package config
import ( import (
"github.com/sqshq/vcmd/data"
. "github.com/sqshq/vcmd/layout" . "github.com/sqshq/vcmd/layout"
"github.com/sqshq/vcmd/settings"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"log" "log"
) )
type Config struct { type Config struct {
RunCharts []RunChart `yaml:"run-charts"` Theme settings.Theme `yaml:"theme"`
} RunCharts []RunChart `yaml:"run-charts"`
type DataConfig struct {
Script string `yaml:"script"`
Label string `yaml:"label"`
} }
type RunChart struct { type RunChart struct {
Title string `yaml:"title"` Title string `yaml:"title"`
DataConfig []DataConfig `yaml:"data"` Items []data.Item `yaml:"data"`
Position Position `yaml:"position"` Position Position `yaml:"position"`
Size Size `yaml:"size"` Size Size `yaml:"size"`
RefreshRateMs int `yaml:"refresh-rate-ms"` RefreshRateMs int `yaml:"refresh-rate-ms"`
TimeScaleSec int `yaml:"time-scale-sec"` TimeScaleSec int `yaml:"time-scale-sec"`
} }
func Load(location string) *Config { func Load(location string) *Config {
cfg := readFile(location)
validate(cfg)
setColors(cfg)
return cfg
}
func readFile(location string) *Config {
yamlFile, err := ioutil.ReadFile(location) yamlFile, err := ioutil.ReadFile(location)
if err != nil { if err != nil {
log.Fatalf("Can't read config file: %s", location) log.Fatalf("Can't read config file: %s", location)
@ -41,3 +48,26 @@ func Load(location string) *Config {
return cfg return cfg
} }
/*
TODO validation
- title uniquness and mandatory within a single type of widget
- label uniqueness and mandatory (if > 1 data bullets)
*/
func validate(config *Config) {
}
func setColors(config *Config) {
palette := settings.GetPalette(config.Theme)
for i, chart := range config.RunCharts {
for j, item := range chart.Items {
if item.Color == 0 {
item.Color = palette.Colors[i+j]
chart.Items[j] = item
}
}
}
}

View File

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

24
data/item.go Normal file
View File

@ -0,0 +1,24 @@
package data
import (
ui "github.com/sqshq/termui"
"os/exec"
"strings"
)
type Item struct {
Script string `yaml:"script"`
Label string `yaml:"label"`
Color ui.Color `yaml:"color"`
}
func (self *Item) nextValue() (value string, err error) {
output, err := exec.Command("sh", "-c", self.Script).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(output)), nil
}

View File

@ -1,22 +1,19 @@
package data package data
import ( import (
"os/exec"
"strings"
"time" "time"
) )
type Poller struct { type Poller struct {
consumer Consumer consumer Consumer
script string item Item
label string
pause bool pause bool
} }
func NewPoller(consumer Consumer, script string, label string, rateMs int) Poller { func NewPoller(consumer Consumer, item Item, rateMs int) Poller {
ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond))) ticker := time.NewTicker(time.Duration(rateMs * int(time.Millisecond)))
poller := Poller{consumer, script, label, false} poller := Poller{consumer, item, false}
go func() { go func() {
for { for {
@ -40,12 +37,11 @@ func (self *Poller) poll() {
return return
} }
output, err := exec.Command("sh", "-c", self.script).Output() value, err := self.item.nextValue()
if err != nil { if err != nil {
self.consumer.ConsumeError(err) self.consumer.ConsumeError(self.item, err)
} }
value := strings.TrimSpace(string(output)) self.consumer.ConsumeValue(self.item, value)
self.consumer.ConsumeValue(value, self.label)
} }

10
main.go
View File

@ -10,14 +10,8 @@ import (
"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")
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
@ -35,9 +29,9 @@ func main() {
chart := widgets.NewRunChart(chartConfig.Title) chart := widgets.NewRunChart(chartConfig.Title)
lout.AddItem(chart, chartConfig.Position, chartConfig.Size) lout.AddItem(chart, chartConfig.Position, chartConfig.Size)
for _, chartData := range chartConfig.DataConfig { for _, item := range chartConfig.Items {
pollers = append(pollers, pollers = append(pollers,
data.NewPoller(chart, chartData.Script, chartData.Label, chartConfig.RefreshRateMs)) data.NewPoller(chart, item, chartConfig.RefreshRateMs))
} }
} }

35
settings/color.go Normal file
View File

@ -0,0 +1,35 @@
package settings
import (
"fmt"
ui "github.com/sqshq/termui"
)
type Theme string
const (
ThemeDark Theme = "dark"
ThemeLight Theme = "light"
)
const (
ColorOlive ui.Color = 178
ColorDeepSkyBlue ui.Color = 39
ColorDeepPink ui.Color = 162
ColorDarkGrey ui.Color = 240
)
type Palette struct {
Colors []ui.Color
}
func GetPalette(theme Theme) Palette {
switch theme {
case ThemeDark:
return Palette{Colors: []ui.Color{ColorOlive, ColorDeepSkyBlue, ColorDeepPink}}
case ThemeLight:
return Palette{Colors: []ui.Color{ColorOlive, ColorDeepSkyBlue, ColorDeepPink}}
default:
panic(fmt.Sprintf("Following theme is not supported: %v", theme))
}
}

View File

@ -2,6 +2,8 @@ package widgets
import ( import (
"fmt" "fmt"
"github.com/sqshq/vcmd/data"
"github.com/sqshq/vcmd/settings"
"image" "image"
"log" "log"
"math" "math"
@ -22,7 +24,7 @@ const (
type RunChart struct { type RunChart struct {
Block Block
items []ChartItem lines []TimeLine
grid ChartGrid grid ChartGrid
mutex *sync.Mutex mutex *sync.Mutex
} }
@ -32,10 +34,9 @@ type TimePoint struct {
Time time.Time Time time.Time
} }
type ChartItem struct { type TimeLine struct {
timePoints []TimePoint points []TimePoint
label string item data.Item
color Color
} }
type ChartGrid struct { type ChartGrid struct {
@ -62,7 +63,7 @@ func NewRunChart(title string) *RunChart {
block.Title = title block.Title = title
return &RunChart{ return &RunChart{
Block: block, Block: block,
items: []ChartItem{}, lines: []TimeLine{},
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
} }
} }
@ -78,7 +79,7 @@ func (self *RunChart) newChartGrid() ChartGrid {
paddingWidth: xAxisLabelsGap + xAxisLabelsWidth, paddingWidth: xAxisLabelsGap + xAxisLabelsWidth,
maxTimeWidth: self.Inner.Max.X - xAxisLabelsWidth/2 - xAxisLabelsGap, maxTimeWidth: self.Inner.Max.X - xAxisLabelsWidth/2 - xAxisLabelsGap,
timeExtremum: GetTimeExtremum(linesCount, paddingDuration), timeExtremum: GetTimeExtremum(linesCount, paddingDuration),
valueExtremum: GetValueExtremum(self.items), valueExtremum: GetValueExtremum(self.lines),
} }
} }
@ -98,7 +99,7 @@ func (self *RunChart) Draw(buf *Buffer) {
self.mutex.Unlock() self.mutex.Unlock()
} }
func (self *RunChart) ConsumeValue(value string, label string) { func (self *RunChart) ConsumeValue(item data.Item, value string) {
float, err := strconv.ParseFloat(value, 64) float, err := strconv.ParseFloat(value, 64)
if err != nil { if err != nil {
@ -109,44 +110,43 @@ func (self *RunChart) ConsumeValue(value string, label string) {
self.mutex.Lock() self.mutex.Lock()
itemExists := false itemExists := false
for i, item := range self.items { for i, line := range self.lines {
if item.label == label { if line.item.Label == item.Label {
item.timePoints = append(item.timePoints, timePoint) line.points = append(line.points, timePoint)
self.items[i] = item self.lines[i] = line
itemExists = true itemExists = true
} }
} }
if !itemExists { if !itemExists {
item := &ChartItem{ item := &TimeLine{
timePoints: []TimePoint{timePoint}, points: []TimePoint{timePoint},
label: label, item: item,
color: ColorYellow,
} }
self.items = append(self.items, *item) self.lines = append(self.lines, *item)
} }
self.trimOutOfRangeValues() self.trimOutOfRangeValues()
self.mutex.Unlock() self.mutex.Unlock()
} }
func (self *RunChart) ConsumeError(err error) { func (self *RunChart) ConsumeError(item data.Item, err error) {
// TODO visual notification // TODO visual notification
} }
func (self *RunChart) trimOutOfRangeValues() { func (self *RunChart) trimOutOfRangeValues() {
for i, item := range self.items { for i, item := range self.lines {
lastOutOfRangeValueIndex := -1 lastOutOfRangeValueIndex := -1
for j, timePoint := range item.timePoints { for j, timePoint := range item.points {
if !self.isTimePointInRange(timePoint) { if !self.isTimePointInRange(timePoint) {
lastOutOfRangeValueIndex = j lastOutOfRangeValueIndex = j
} }
} }
if lastOutOfRangeValueIndex > 0 { if lastOutOfRangeValueIndex > 0 {
item.timePoints = append(item.timePoints[:0], item.timePoints[lastOutOfRangeValueIndex+1:]...) item.points = append(item.points[:0], item.points[lastOutOfRangeValueIndex+1:]...)
self.items[i] = item self.lines[i] = item
} }
} }
} }
@ -156,23 +156,23 @@ func (self *RunChart) renderItems(buf *Buffer, drawArea image.Rectangle) {
canvas := NewCanvas() canvas := NewCanvas()
canvas.Rectangle = drawArea canvas.Rectangle = drawArea
for _, item := range self.items { for _, line := range self.lines {
xToPoint := make(map[int]image.Point) xToPoint := make(map[int]image.Point)
pointsOrder := make([]int, 0) pointsOrder := make([]int, 0)
for _, timePoint := range item.timePoints { for _, point := range line.points {
if !self.isTimePointInRange(timePoint) { if !self.isTimePointInRange(point) {
continue continue
} }
timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(timePoint.Time) timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(point.Time)
deltaToPaddingRelation := float64(timeDeltaWithGridMaxTime.Nanoseconds()) / float64(self.grid.paddingDuration.Nanoseconds()) deltaToPaddingRelation := float64(timeDeltaWithGridMaxTime.Nanoseconds()) / float64(self.grid.paddingDuration.Nanoseconds())
x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * deltaToPaddingRelation)) x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * deltaToPaddingRelation))
valuePerYDot := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-1) valuePerYDot := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-1)
y := int(float64(timePoint.Value-self.grid.valueExtremum.min) / valuePerYDot) y := int(float64(point.Value-self.grid.valueExtremum.min) / valuePerYDot)
if _, exists := xToPoint[x]; exists { if _, exists := xToPoint[x]; exists {
continue continue
@ -201,7 +201,7 @@ func (self *RunChart) renderItems(buf *Buffer, drawArea image.Rectangle) {
canvas.Line( canvas.Line(
braillePoint(previousPoint), braillePoint(previousPoint),
braillePoint(currentPoint), braillePoint(currentPoint),
item.color, line.item.Color,
) )
} }
} }
@ -232,7 +232,7 @@ func (self *RunChart) plotAxes(buf *Buffer) {
for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-1; y = y + 2 { for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-1; y = y + 2 {
for x := 0; x < self.grid.linesCount; x++ { for x := 0; x < self.grid.linesCount; x++ {
buf.SetCell( buf.SetCell(
NewCell(VERTICAL_DASH, NewStyle(ColorDarkGrey)), NewCell(VERTICAL_DASH, NewStyle(settings.ColorDarkGrey)),
image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1), image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1),
) )
} }
@ -267,7 +267,7 @@ func (self *RunChart) plotAxes(buf *Buffer) {
} }
} }
func GetValueExtremum(items []ChartItem) ValueExtremum { func GetValueExtremum(items []TimeLine) ValueExtremum {
if len(items) == 0 { if len(items) == 0 {
return ValueExtremum{0, 0} return ValueExtremum{0, 0}
@ -276,7 +276,7 @@ func GetValueExtremum(items []ChartItem) ValueExtremum {
var max, min = -math.MaxFloat64, math.MaxFloat64 var max, min = -math.MaxFloat64, math.MaxFloat64
for _, item := range items { for _, item := range items {
for _, point := range item.timePoints { for _, point := range item.points {
if point.Value > max { if point.Value > max {
max = point.Value max = point.Value
} }

View File

@ -1,7 +0,0 @@
package widgets
import "github.com/sqshq/termui"
const (
ColorDarkGrey termui.Color = 240
)