diff --git a/component/alerter.go b/component/alert.go similarity index 65% rename from component/alerter.go rename to component/alert.go index c8a7eac..63becfb 100644 --- a/component/alerter.go +++ b/component/alert.go @@ -9,48 +9,26 @@ import ( "strings" ) -type Alerter struct { - channel <-chan data.Alert - alert *data.Alert -} +func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buffer) { -func NewAlerter(channel <-chan data.Alert) *Alerter { - alerter := Alerter{channel: channel} - alerter.consume() - return &alerter -} - -func (a *Alerter) consume() { - go func() { - for { - select { - case alert := <-a.channel: - a.alert = &alert - } - } - }() -} - -func (a *Alerter) RenderAlert(buffer *ui.Buffer, area image.Rectangle) { - - if a.alert == nil { + if alert == nil { return } color := console.ColorWhite - if a.alert.Color != nil { - color = *a.alert.Color + if alert.Color != nil { + color = *alert.Color } - width := max(len(a.alert.Title), len(a.alert.Text)) + 10 + width := max(len(alert.Title), len(alert.Text)) + 10 if width > area.Dx() { width = area.Dx() } cells := ui.WrapCells(ui.ParseStyles(fmt.Sprintf("%s\n%s\n", - strings.ToUpper(a.alert.Title), a.alert.Text), ui.NewStyle(console.ColorWhite)), uint(width)) + strings.ToUpper(alert.Title), alert.Text), ui.NewStyle(console.ColorWhite)), uint(width)) var lines []string line := "" diff --git a/component/asciibox/asciibox.go b/component/asciibox/asciibox.go index ccb71a0..ca1b9d2 100644 --- a/component/asciibox/asciibox.go +++ b/component/asciibox/asciibox.go @@ -6,11 +6,14 @@ import ( "github.com/sqshq/sampler/asset" "github.com/sqshq/sampler/component" "github.com/sqshq/sampler/config" + "github.com/sqshq/sampler/data" "image" ) type AsciiBox struct { - *component.Component + *ui.Block + *data.Consumer + alert *data.Alert text string ascii string style ui.Style @@ -34,10 +37,11 @@ func NewAsciiBox(c config.AsciiBoxConfig) *AsciiBox { _ = render.LoadBindataFont(fontStr, options.FontName) box := AsciiBox{ - Component: component.NewComponent(c.ComponentConfig, config.TypeAsciiBox), - style: ui.NewStyle(*c.Color), - render: render, - options: options, + Block: component.NewBlock(c.Title, true), + Consumer: data.NewConsumer(), + style: ui.NewStyle(*c.Color), + render: render, + options: options, } go func() { @@ -46,6 +50,8 @@ func NewAsciiBox(c config.AsciiBoxConfig) *AsciiBox { case sample := <-box.SampleChannel: box.text = sample.Value box.ascii, _ = box.render.RenderOpts(sample.Value, box.options) + case alert := <-box.AlertChannel: + box.alert = alert } } }() @@ -69,4 +75,6 @@ func (a *AsciiBox) Draw(buffer *ui.Buffer) { point = point.Add(image.Pt(1, 0)) } } + + component.RenderAlert(a.alert, a.Rectangle, buffer) } diff --git a/component/barchart/barchart.go b/component/barchart/barchart.go index f67cc3b..16a22f8 100644 --- a/component/barchart/barchart.go +++ b/component/barchart/barchart.go @@ -18,7 +18,9 @@ const ( ) type BarChart struct { - *component.Component + *ui.Block + *data.Consumer + alert *data.Alert bars []Bar scale int maxValue float64 @@ -35,10 +37,11 @@ type Bar struct { func NewBarChart(c config.BarChartConfig) *BarChart { chart := BarChart{ - Component: component.NewComponent(c.ComponentConfig, config.TypeBarChart), - bars: []Bar{}, - scale: *c.Scale, - maxValue: -math.MaxFloat64, + Block: component.NewBlock(c.Title, true), + Consumer: data.NewConsumer(), + bars: []Bar{}, + scale: *c.Scale, + maxValue: -math.MaxFloat64, } for _, i := range c.Items { @@ -50,6 +53,8 @@ func NewBarChart(c config.BarChartConfig) *BarChart { select { case sample := <-chart.SampleChannel: chart.consumeSample(sample) + case alert := <-chart.AlertChannel: + chart.alert = alert } } }() @@ -57,13 +62,19 @@ func NewBarChart(c config.BarChartConfig) *BarChart { return &chart } -func (b *BarChart) consumeSample(sample data.Sample) { +func (b *BarChart) consumeSample(sample *data.Sample) { b.count++ float, err := strconv.ParseFloat(sample.Value, 64) + if err != nil { - // TODO visual notification + check sample.Error + b.AlertChannel <- &data.Alert{ + Title: "FAILED TO PARSE NUMBER", + Text: err.Error(), + Color: sample.Color, + } + return } index := -1 @@ -102,8 +113,8 @@ func (b *BarChart) reselectMaxValue() { b.maxValue = maxValue } -func (b *BarChart) Draw(buf *ui.Buffer) { - b.Block.Draw(buf) +func (b *BarChart) Draw(buffer *ui.Buffer) { + b.Block.Draw(buffer) barWidth := int(math.Ceil(float64(b.Inner.Dx()-2*barIndent-len(b.bars)*barIndent) / float64(len(b.bars)))) barXCoordinate := b.Inner.Min.X + barIndent @@ -122,7 +133,7 @@ func (b *BarChart) Draw(buf *ui.Buffer) { for x := barXCoordinate; x < ui.MinInt(barXCoordinate+barWidth, b.Inner.Max.X-barIndent); x++ { for y := b.Inner.Max.Y - 2; y >= maxYCoordinate; y-- { c := ui.NewCell(console.SymbolShade, ui.NewStyle(bar.color)) - buf.SetCell(c, image.Pt(x, y)) + buffer.SetCell(c, image.Pt(x, y)) } } @@ -130,7 +141,7 @@ func (b *BarChart) Draw(buf *ui.Buffer) { labelXCoordinate := barXCoordinate + int(float64(barWidth)/2) - int(float64(rw.StringWidth(bar.label))/2) - buf.SetString( + buffer.SetString( bar.label, labelStyle, image.Pt(labelXCoordinate, b.Inner.Max.Y-1)) @@ -143,13 +154,15 @@ func (b *BarChart) Draw(buf *ui.Buffer) { valueXCoordinate := barXCoordinate + int(float64(barWidth)/2) - int(float64(rw.StringWidth(value))/2) - buf.SetString( + buffer.SetString( value, labelStyle, image.Pt(valueXCoordinate, maxYCoordinate-1)) barXCoordinate += barWidth + barIndent } + + component.RenderAlert(b.alert, b.Rectangle, buffer) } // TODO extract to utils diff --git a/component/block.go b/component/block.go new file mode 100644 index 0000000..cade7a6 --- /dev/null +++ b/component/block.go @@ -0,0 +1,12 @@ +package component + +import ( + ui "github.com/gizak/termui/v3" +) + +func NewBlock(title string, border bool) *ui.Block { + block := ui.NewBlock() + block.Title = title + block.Border = border + return block +} diff --git a/component/component.go b/component/component.go index e3fe121..c6f8799 100644 --- a/component/component.go +++ b/component/component.go @@ -7,9 +7,8 @@ import ( ) type Component struct { - ui.Block - data.Consumer - *Alerter + ui.Drawable + *data.Consumer Type config.ComponentType Title string Position config.Position @@ -17,21 +16,15 @@ type Component struct { RefreshRateMs int } -func NewComponent(c config.ComponentConfig, t config.ComponentType) *Component { - - consumer := data.NewConsumer() - block := *ui.NewBlock() - block.Title = c.Title - +func NewComponent(dbl ui.Drawable, cmr *data.Consumer, cfg config.ComponentConfig, ct config.ComponentType) *Component { return &Component{ - Block: block, - Consumer: consumer, - Alerter: NewAlerter(consumer.AlertChannel), - Type: t, - Title: c.Title, - Position: c.Position, - Size: c.Size, - RefreshRateMs: *c.RefreshRateMs, + Drawable: dbl, + Consumer: cmr, + Type: ct, + Title: cfg.Title, + Position: cfg.Position, + Size: cfg.Size, + RefreshRateMs: *cfg.RefreshRateMs, } } diff --git a/component/gauge/gauge.go b/component/gauge/gauge.go index a22760e..14946fd 100644 --- a/component/gauge/gauge.go +++ b/component/gauge/gauge.go @@ -19,7 +19,9 @@ const ( ) type Gauge struct { - *component.Component + *ui.Block + *data.Consumer + alert *data.Alert minValue float64 maxValue float64 curValue float64 @@ -30,9 +32,10 @@ type Gauge struct { func NewGauge(c config.GaugeConfig) *Gauge { gauge := Gauge{ - Component: component.NewComponent(c.ComponentConfig, config.TypeGauge), - scale: *c.Scale, - color: *c.Color, + Block: component.NewBlock(c.Title, true), + Consumer: data.NewConsumer(), + scale: *c.Scale, + color: *c.Color, } go func() { @@ -40,6 +43,8 @@ func NewGauge(c config.GaugeConfig) *Gauge { select { case sample := <-gauge.SampleChannel: gauge.ConsumeSample(sample) + case alert := <-gauge.AlertChannel: + gauge.alert = alert } } }() @@ -47,11 +52,16 @@ func NewGauge(c config.GaugeConfig) *Gauge { return &gauge } -func (g *Gauge) ConsumeSample(sample data.Sample) { +func (g *Gauge) ConsumeSample(sample *data.Sample) { float, err := strconv.ParseFloat(sample.Value, 64) if err != nil { - // TODO handle in Component + g.AlertChannel <- &data.Alert{ + Title: "FAILED TO PARSE NUMBER", + Text: err.Error(), + Color: sample.Color, + } + return } switch sample.Label { @@ -67,9 +77,9 @@ func (g *Gauge) ConsumeSample(sample data.Sample) { } } -func (g *Gauge) Draw(buf *ui.Buffer) { +func (g *Gauge) Draw(buffer *ui.Buffer) { - g.Block.Draw(buf) + g.Block.Draw(buffer) percent := 0.0 if g.curValue != 0 && g.maxValue != g.minValue { @@ -85,7 +95,7 @@ func (g *Gauge) Draw(buf *ui.Buffer) { } else if barWidth > g.Dx()-2 { barWidth = g.Dx() - 2 } - buf.Fill( + buffer.Fill( ui.NewCell(console.SymbolVerticalBar, ui.NewStyle(g.color)), image.Rect(g.Inner.Min.X+1, g.Inner.Min.Y, g.Inner.Min.X+barWidth, g.Inner.Max.Y), ) @@ -99,9 +109,11 @@ func (g *Gauge) Draw(buf *ui.Buffer) { if labelXCoordinate+i+1 <= g.Inner.Min.X+barWidth { style = ui.NewStyle(console.ColorWhite, ui.ColorClear) } - buf.SetCell(ui.NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate)) + buffer.SetCell(ui.NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate)) } } + + component.RenderAlert(g.alert, g.Rectangle, buffer) } // TODO extract to utils diff --git a/component/layout/layout.go b/component/layout/layout.go index 242a5b6..28da451 100644 --- a/component/layout/layout.go +++ b/component/layout/layout.go @@ -57,7 +57,7 @@ func NewLayout(width, height int, statusline *component.StatusBar, menu *compone } } -func (l *Layout) AddComponent(cpt *component.Component, Type config.ComponentType) { +func (l *Layout) AddComponent(cpt *component.Component) { l.Components = append(l.Components, cpt) } @@ -76,7 +76,7 @@ func (l *Layout) HandleConsoleEvent(e string) { l.changeMode(ModeDefault) } else { if selected.Type == config.TypeRunChart { - selected.CommandChannel <- data.Command{Type: runchart.CommandDisableSelection} + selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection} } l.menu.Idle() l.changeMode(ModePause) @@ -98,7 +98,7 @@ func (l *Layout) HandleConsoleEvent(e string) { case component.MenuOptionPinpoint: l.changeMode(ModeChartPinpoint) l.menu.Idle() - selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: 0} + selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 0} case component.MenuOptionResume: l.changeMode(ModeDefault) l.menu.Idle() @@ -112,7 +112,7 @@ func (l *Layout) HandleConsoleEvent(e string) { case console.KeyEsc: switch l.mode { case ModeChartPinpoint: - selected.CommandChannel <- data.Command{Type: runchart.CommandDisableSelection} + selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection} fallthrough case ModeComponentSelect: fallthrough @@ -126,7 +126,7 @@ func (l *Layout) HandleConsoleEvent(e string) { l.changeMode(ModeComponentSelect) l.menu.Highlight(l.getComponent(l.selection)) case ModeChartPinpoint: - selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: -1} + selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: -1} case ModeComponentSelect: l.moveSelection(e) l.menu.Highlight(l.getComponent(l.selection)) @@ -141,7 +141,7 @@ func (l *Layout) HandleConsoleEvent(e string) { l.changeMode(ModeComponentSelect) l.menu.Highlight(l.getComponent(l.selection)) case ModeChartPinpoint: - selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: 1} + selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 1} case ModeComponentSelect: l.moveSelection(e) l.menu.Highlight(l.getComponent(l.selection)) diff --git a/component/menu.go b/component/menu.go index fb0c55c..cba6767 100644 --- a/component/menu.go +++ b/component/menu.go @@ -219,7 +219,7 @@ func (m *Menu) renderOptions(buffer *ui.Buffer) { } func (m *Menu) updateDimensions() { - r := m.component.Block.GetRect() + r := m.component.GetRect() m.SetRect(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y) } diff --git a/component/runchart/runchart.go b/component/runchart/runchart.go index 0f88f35..057ebc0 100644 --- a/component/runchart/runchart.go +++ b/component/runchart/runchart.go @@ -40,7 +40,9 @@ const ( ) type RunChart struct { - *component.Component + *ui.Block + *data.Consumer + alert *data.Alert lines []TimeLine grid ChartGrid timescale time.Duration @@ -79,7 +81,8 @@ type ValueExtrema struct { func NewRunChart(c config.RunChartConfig) *RunChart { chart := RunChart{ - Component: component.NewComponent(c.ComponentConfig, config.TypeRunChart), + Block: component.NewBlock(c.Title, true), + Consumer: data.NewConsumer(), lines: []TimeLine{}, timescale: calculateTimescale(*c.RefreshRateMs), mutex: &sync.Mutex{}, @@ -97,6 +100,8 @@ func NewRunChart(c config.RunChartConfig) *RunChart { select { case sample := <-chart.SampleChannel: chart.consumeSample(sample) + case alert := <-chart.AlertChannel: + chart.alert = alert case command := <-chart.CommandChannel: switch command.Type { case CommandDisableSelection: @@ -134,7 +139,7 @@ func (c *RunChart) Draw(buffer *ui.Buffer) { c.renderAxes(buffer) c.renderLines(buffer, drawArea) c.renderLegend(buffer, drawArea) - c.RenderAlert(buffer, c.Rectangle) + component.RenderAlert(c.alert, c.Rectangle, buffer) c.mutex.Unlock() } @@ -148,16 +153,17 @@ func (c *RunChart) AddLine(Label string, color ui.Color) { c.lines = append(c.lines, line) } -func (c *RunChart) consumeSample(sample data.Sample) { +func (c *RunChart) consumeSample(sample *data.Sample) { float, err := strconv.ParseFloat(sample.Value, 64) if err != nil { - c.AlertChannel <- data.Alert{ - Title: "SAMPLING FAILURE", + c.AlertChannel <- &data.Alert{ + Title: "FAILED TO PARSE NUMBER", Text: err.Error(), Color: sample.Color, } + return } c.mutex.Lock() diff --git a/data/consumer.go b/data/consumer.go index 8a1b2dd..a587cb2 100644 --- a/data/consumer.go +++ b/data/consumer.go @@ -2,11 +2,10 @@ package data import ui "github.com/gizak/termui/v3" -// TODO interface here, move fields declaration in the Component type Consumer struct { - SampleChannel chan Sample - AlertChannel chan Alert - CommandChannel chan Command + SampleChannel chan *Sample + AlertChannel chan *Alert + CommandChannel chan *Command } type Sample struct { @@ -26,10 +25,10 @@ type Command struct { Value interface{} } -func NewConsumer() Consumer { - return Consumer{ - SampleChannel: make(chan Sample), - AlertChannel: make(chan Alert), - CommandChannel: make(chan Command), +func NewConsumer() *Consumer { + return &Consumer{ + SampleChannel: make(chan *Sample), + AlertChannel: make(chan *Alert), + CommandChannel: make(chan *Command), } } diff --git a/data/sampler.go b/data/sampler.go index 0e70e92..ad787a5 100644 --- a/data/sampler.go +++ b/data/sampler.go @@ -5,13 +5,13 @@ import ( ) type Sampler struct { - consumer Consumer + consumer *Consumer items []Item triggers []Trigger - triggersChannel chan Sample + triggersChannel chan *Sample } -func NewSampler(consumer Consumer, items []Item, triggers []Trigger, rateMs int) Sampler { +func NewSampler(consumer *Consumer, items []Item, triggers []Trigger, rateMs int) Sampler { ticker := time.NewTicker( time.Duration(rateMs * int(time.Millisecond)), @@ -21,7 +21,7 @@ func NewSampler(consumer Consumer, items []Item, triggers []Trigger, rateMs int) consumer, items, triggers, - make(chan Sample), + make(chan *Sample), } go func() { @@ -51,11 +51,11 @@ func (s *Sampler) sample(item Item) { val, err := item.nextValue() if err == nil { - sample := Sample{Label: item.Label, Value: val, Color: item.Color} + sample := &Sample{Label: item.Label, Value: val, Color: item.Color} s.consumer.SampleChannel <- sample s.triggersChannel <- sample } else { - s.consumer.AlertChannel <- Alert{ + s.consumer.AlertChannel <- &Alert{ Title: "SAMPLING FAILURE", Text: err.Error(), Color: item.Color, diff --git a/data/trigger.go b/data/trigger.go index f4dd6e3..0c292bf 100644 --- a/data/trigger.go +++ b/data/trigger.go @@ -18,8 +18,8 @@ const ( type Trigger struct { title string condition string - actions Actions - consumer Consumer + actions *Actions + consumer *Consumer valuesByLabel map[string]Values player *asset.AudioPlayer digitsRegexp *regexp.Regexp @@ -37,7 +37,7 @@ type Values struct { previous string } -func NewTriggers(cfgs []config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) []Trigger { +func NewTriggers(cfgs []config.TriggerConfig, consumer *Consumer, player *asset.AudioPlayer) []Trigger { triggers := make([]Trigger, 0) @@ -48,7 +48,7 @@ func NewTriggers(cfgs []config.TriggerConfig, consumer Consumer, player *asset.A return triggers } -func NewTrigger(config config.TriggerConfig, consumer Consumer, player *asset.AudioPlayer) Trigger { +func NewTrigger(config config.TriggerConfig, consumer *Consumer, player *asset.AudioPlayer) Trigger { return Trigger{ title: config.Title, condition: config.Condition, @@ -56,7 +56,7 @@ func NewTrigger(config config.TriggerConfig, consumer Consumer, player *asset.Au valuesByLabel: make(map[string]Values), player: player, digitsRegexp: regexp.MustCompile("[^0-9]+"), - actions: Actions{ + actions: &Actions{ terminalBell: *config.Actions.TerminalBell, sound: *config.Actions.Sound, visual: *config.Actions.Visual, @@ -65,7 +65,7 @@ func NewTrigger(config config.TriggerConfig, consumer Consumer, player *asset.Au } } -func (t *Trigger) Execute(sample Sample) { +func (t *Trigger) Execute(sample *Sample) { if t.evaluate(sample) { if t.actions.terminalBell { @@ -77,7 +77,7 @@ func (t *Trigger) Execute(sample Sample) { } if t.actions.visual { - t.consumer.AlertChannel <- Alert{ + t.consumer.AlertChannel <- &Alert{ Title: t.title, Text: fmt.Sprintf("%s: %v", sample.Label, sample.Value), Color: sample.Color, @@ -90,7 +90,7 @@ func (t *Trigger) Execute(sample Sample) { } } -func (t *Trigger) evaluate(sample Sample) bool { +func (t *Trigger) evaluate(sample *Sample) bool { if values, ok := t.valuesByLabel[sample.Label]; ok { values.previous = values.current @@ -103,7 +103,7 @@ func (t *Trigger) evaluate(sample Sample) bool { output, err := runScript(t.condition, sample.Label, t.valuesByLabel[sample.Label]) if err != nil { - t.consumer.AlertChannel <- Alert{Title: "TRIGGER CONDITION FAILURE", Text: err.Error()} + t.consumer.AlertChannel <- &Alert{Title: "TRIGGER CONDITION FAILURE", Text: err.Error()} } return t.digitsRegexp.ReplaceAllString(string(output), "") == TrueIndicator diff --git a/main.go b/main.go index 0aea187..57ce7bb 100644 --- a/main.go +++ b/main.go @@ -30,30 +30,34 @@ func main() { for _, c := range cfg.RunCharts { chart := runchart.NewRunChart(c) + cpt := component.NewComponent(chart, chart.Consumer, c.ComponentConfig, config.TypeRunChart) triggers := data.NewTriggers(c.Triggers, chart.Consumer, player) data.NewSampler(chart.Consumer, data.NewItems(c.Items), triggers, *c.RefreshRateMs) - lout.AddComponent(chart.Component, config.TypeRunChart) + lout.AddComponent(cpt) } for _, a := range cfg.AsciiBoxes { box := asciibox.NewAsciiBox(a) + cpt := component.NewComponent(box, box.Consumer, a.ComponentConfig, config.TypeRunChart) triggers := data.NewTriggers(a.Triggers, box.Consumer, player) data.NewSampler(box.Consumer, data.NewItems([]config.Item{a.Item}), triggers, *a.RefreshRateMs) - lout.AddComponent(box.Component, config.TypeAsciiBox) + lout.AddComponent(cpt) } for _, b := range cfg.BarCharts { chart := barchart.NewBarChart(b) + cpt := component.NewComponent(chart, chart.Consumer, b.ComponentConfig, config.TypeRunChart) triggers := data.NewTriggers(b.Triggers, chart.Consumer, player) data.NewSampler(chart.Consumer, data.NewItems(b.Items), triggers, *b.RefreshRateMs) - lout.AddComponent(chart.Component, config.TypeBarChart) + lout.AddComponent(cpt) } for _, gc := range cfg.Gauges { g := gauge.NewGauge(gc) + cpt := component.NewComponent(g, g.Consumer, gc.ComponentConfig, config.TypeRunChart) triggers := data.NewTriggers(gc.Triggers, g.Consumer, player) data.NewSampler(g.Consumer, data.NewItems(gc.Items), triggers, *gc.RefreshRateMs) - lout.AddComponent(g.Component, config.TypeGauge) + lout.AddComponent(cpt) } handler := event.NewHandler(lout)