combine common functionality in Component struct
This commit is contained in:
parent
dcfdc16a41
commit
fec7eefd9f
|
@ -6,14 +6,11 @@ import (
|
||||||
"github.com/sqshq/sampler/asset"
|
"github.com/sqshq/sampler/asset"
|
||||||
"github.com/sqshq/sampler/component"
|
"github.com/sqshq/sampler/component"
|
||||||
"github.com/sqshq/sampler/config"
|
"github.com/sqshq/sampler/config"
|
||||||
"github.com/sqshq/sampler/data"
|
|
||||||
"image"
|
"image"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AsciiBox struct {
|
type AsciiBox struct {
|
||||||
ui.Block
|
*component.Component
|
||||||
data.Consumer
|
|
||||||
*component.Alerter
|
|
||||||
text string
|
text string
|
||||||
ascii string
|
ascii string
|
||||||
style ui.Style
|
style ui.Style
|
||||||
|
@ -25,10 +22,6 @@ const asciiFontExtension = ".flf"
|
||||||
|
|
||||||
func NewAsciiBox(c config.AsciiBoxConfig) *AsciiBox {
|
func NewAsciiBox(c config.AsciiBoxConfig) *AsciiBox {
|
||||||
|
|
||||||
consumer := data.NewConsumer()
|
|
||||||
block := *ui.NewBlock()
|
|
||||||
block.Title = c.Title
|
|
||||||
|
|
||||||
options := fl.NewRenderOptions()
|
options := fl.NewRenderOptions()
|
||||||
options.FontName = string(*c.Font)
|
options.FontName = string(*c.Font)
|
||||||
|
|
||||||
|
@ -36,35 +29,30 @@ func NewAsciiBox(c config.AsciiBoxConfig) *AsciiBox {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("Can't load the font: " + err.Error())
|
panic("Can't load the font: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
render := fl.NewAsciiRender()
|
render := fl.NewAsciiRender()
|
||||||
_ = render.LoadBindataFont(fontStr, options.FontName)
|
_ = render.LoadBindataFont(fontStr, options.FontName)
|
||||||
|
|
||||||
box := AsciiBox{
|
box := AsciiBox{
|
||||||
Block: block,
|
Component: component.NewComponent(c.ComponentConfig, config.TypeAsciiBox),
|
||||||
Consumer: consumer,
|
style: ui.NewStyle(*c.Color),
|
||||||
Alerter: component.NewAlerter(consumer.AlertChannel),
|
render: render,
|
||||||
style: ui.NewStyle(*c.Color),
|
options: options,
|
||||||
render: render,
|
|
||||||
options: options,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go box.consume()
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sample := <-box.SampleChannel:
|
||||||
|
box.text = sample.Value
|
||||||
|
box.ascii, _ = box.render.RenderOpts(sample.Value, box.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return &box
|
return &box
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AsciiBox) consume() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sample := <-a.SampleChannel:
|
|
||||||
a.text = sample.Value
|
|
||||||
a.ascii, _ = a.render.RenderOpts(sample.Value, a.options)
|
|
||||||
//case alert := <-a.alertChannel:
|
|
||||||
// TODO base alerting mechanism
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AsciiBox) Draw(buffer *ui.Buffer) {
|
func (a *AsciiBox) Draw(buffer *ui.Buffer) {
|
||||||
|
|
||||||
buffer.Fill(ui.NewCell(' ', ui.NewStyle(ui.ColorBlack)), a.GetRect())
|
buffer.Fill(ui.NewCell(' ', ui.NewStyle(ui.ColorBlack)), a.GetRect())
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
ui "github.com/gizak/termui/v3"
|
ui "github.com/gizak/termui/v3"
|
||||||
rw "github.com/mattn/go-runewidth"
|
rw "github.com/mattn/go-runewidth"
|
||||||
"github.com/sqshq/sampler/component"
|
"github.com/sqshq/sampler/component"
|
||||||
|
"github.com/sqshq/sampler/config"
|
||||||
"github.com/sqshq/sampler/console"
|
"github.com/sqshq/sampler/console"
|
||||||
"github.com/sqshq/sampler/data"
|
"github.com/sqshq/sampler/data"
|
||||||
"image"
|
"image"
|
||||||
|
@ -17,9 +18,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type BarChart struct {
|
type BarChart struct {
|
||||||
ui.Block
|
*component.Component
|
||||||
data.Consumer
|
|
||||||
*component.Alerter
|
|
||||||
bars []Bar
|
bars []Bar
|
||||||
scale int
|
scale int
|
||||||
maxValue float64
|
maxValue float64
|
||||||
|
@ -33,35 +32,31 @@ type Bar struct {
|
||||||
delta float64
|
delta float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBarChart(title string, scale int) *BarChart {
|
func NewBarChart(c config.BarChartConfig) *BarChart {
|
||||||
consumer := data.NewConsumer()
|
|
||||||
block := *ui.NewBlock()
|
|
||||||
block.Title = title
|
|
||||||
chart := BarChart{
|
chart := BarChart{
|
||||||
Block: block,
|
Component: component.NewComponent(c.ComponentConfig, config.TypeBarChart),
|
||||||
Consumer: consumer,
|
bars: []Bar{},
|
||||||
Alerter: component.NewAlerter(consumer.AlertChannel),
|
scale: *c.Scale,
|
||||||
bars: []Bar{},
|
maxValue: -math.MaxFloat64,
|
||||||
scale: scale,
|
|
||||||
maxValue: -math.MaxFloat64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go chart.consume()
|
for _, i := range c.Items {
|
||||||
|
chart.AddBar(*i.Label, *i.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sample := <-chart.SampleChannel:
|
||||||
|
chart.consumeSample(sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return &chart
|
return &chart
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BarChart) consume() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sample := <-b.SampleChannel:
|
|
||||||
b.consumeSample(sample)
|
|
||||||
//case alert := <-b.alertChannel:
|
|
||||||
// TODO base alerting mechanism
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BarChart) consumeSample(sample data.Sample) {
|
func (b *BarChart) consumeSample(sample data.Sample) {
|
||||||
|
|
||||||
b.count++
|
b.count++
|
||||||
|
|
|
@ -3,17 +3,38 @@ package component
|
||||||
import (
|
import (
|
||||||
ui "github.com/gizak/termui/v3"
|
ui "github.com/gizak/termui/v3"
|
||||||
"github.com/sqshq/sampler/config"
|
"github.com/sqshq/sampler/config"
|
||||||
|
"github.com/sqshq/sampler/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Component struct {
|
type Component struct {
|
||||||
|
ui.Block
|
||||||
|
data.Consumer
|
||||||
|
*Alerter
|
||||||
Type config.ComponentType
|
Type config.ComponentType
|
||||||
Drawable ui.Drawable
|
|
||||||
Title string
|
Title string
|
||||||
Position config.Position
|
Position config.Position
|
||||||
Size config.Size
|
Size config.Size
|
||||||
RefreshRateMs int
|
RefreshRateMs int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewComponent(c config.ComponentConfig, t config.ComponentType) *Component {
|
||||||
|
|
||||||
|
consumer := data.NewConsumer()
|
||||||
|
block := *ui.NewBlock()
|
||||||
|
block.Title = c.Title
|
||||||
|
|
||||||
|
return &Component{
|
||||||
|
Block: block,
|
||||||
|
Consumer: consumer,
|
||||||
|
Alerter: NewAlerter(consumer.AlertChannel),
|
||||||
|
Type: t,
|
||||||
|
Title: c.Title,
|
||||||
|
Position: c.Position,
|
||||||
|
Size: c.Size,
|
||||||
|
RefreshRateMs: *c.RefreshRateMs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Component) Move(x, y int) {
|
func (c *Component) Move(x, y int) {
|
||||||
c.Position.X += x
|
c.Position.X += x
|
||||||
c.Position.Y += y
|
c.Position.Y += y
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
ui "github.com/gizak/termui/v3"
|
ui "github.com/gizak/termui/v3"
|
||||||
"github.com/sqshq/sampler/component"
|
"github.com/sqshq/sampler/component"
|
||||||
|
"github.com/sqshq/sampler/config"
|
||||||
"github.com/sqshq/sampler/console"
|
"github.com/sqshq/sampler/console"
|
||||||
"github.com/sqshq/sampler/data"
|
"github.com/sqshq/sampler/data"
|
||||||
"image"
|
"image"
|
||||||
|
@ -18,49 +19,39 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Gauge struct {
|
type Gauge struct {
|
||||||
ui.Block
|
*component.Component
|
||||||
data.Consumer
|
|
||||||
*component.Alerter
|
|
||||||
minValue float64
|
minValue float64
|
||||||
maxValue float64
|
maxValue float64
|
||||||
curValue float64
|
curValue float64
|
||||||
scale int
|
|
||||||
color ui.Color
|
color ui.Color
|
||||||
|
scale int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGauge(title string, scale int, color ui.Color) *Gauge {
|
func NewGauge(c config.GaugeConfig) *Gauge {
|
||||||
consumer := data.NewConsumer()
|
|
||||||
block := *ui.NewBlock()
|
|
||||||
block.Title = title
|
|
||||||
gauge := Gauge{
|
gauge := Gauge{
|
||||||
Block: block,
|
Component: component.NewComponent(c.ComponentConfig, config.TypeGauge),
|
||||||
Consumer: consumer,
|
scale: *c.Scale,
|
||||||
Alerter: component.NewAlerter(consumer.AlertChannel),
|
color: *c.Color,
|
||||||
scale: scale,
|
|
||||||
color: color,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go gauge.consume()
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sample := <-gauge.SampleChannel:
|
||||||
|
gauge.ConsumeSample(sample)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return &gauge
|
return &gauge
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Gauge) consume() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sample := <-g.SampleChannel:
|
|
||||||
g.ConsumeSample(sample)
|
|
||||||
//case alert := <-g.alertChannel:
|
|
||||||
// TODO base alerting mechanism
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *Gauge) ConsumeSample(sample data.Sample) {
|
func (g *Gauge) ConsumeSample(sample data.Sample) {
|
||||||
|
|
||||||
float, err := strconv.ParseFloat(sample.Value, 64)
|
float, err := strconv.ParseFloat(sample.Value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO visual notification + check sample.Error
|
// TODO handle in Component
|
||||||
}
|
}
|
||||||
|
|
||||||
switch sample.Label {
|
switch sample.Label {
|
||||||
|
@ -87,7 +78,7 @@ func (g *Gauge) Draw(buf *ui.Buffer) {
|
||||||
|
|
||||||
label := fmt.Sprintf("%v%% (%v)", formatValue(percent, g.scale), g.curValue)
|
label := fmt.Sprintf("%v%% (%v)", formatValue(percent, g.scale), g.curValue)
|
||||||
|
|
||||||
// plot bar
|
// draw bar
|
||||||
barWidth := int((percent / 100) * float64(g.Inner.Dx()))
|
barWidth := int((percent / 100) * float64(g.Inner.Dx()))
|
||||||
if barWidth == 0 {
|
if barWidth == 0 {
|
||||||
barWidth = 1
|
barWidth = 1
|
||||||
|
@ -99,7 +90,7 @@ func (g *Gauge) Draw(buf *ui.Buffer) {
|
||||||
image.Rect(g.Inner.Min.X+1, g.Inner.Min.Y, g.Inner.Min.X+barWidth, g.Inner.Max.Y),
|
image.Rect(g.Inner.Min.X+1, g.Inner.Min.Y, g.Inner.Min.X+barWidth, g.Inner.Max.Y),
|
||||||
)
|
)
|
||||||
|
|
||||||
// plot label
|
// draw label
|
||||||
labelXCoordinate := g.Inner.Min.X + (g.Inner.Dx() / 2) - int(float64(len(label))/2)
|
labelXCoordinate := g.Inner.Min.X + (g.Inner.Dx() / 2) - int(float64(len(label))/2)
|
||||||
labelYCoordinate := g.Inner.Min.Y + ((g.Inner.Dy() - 1) / 2)
|
labelYCoordinate := g.Inner.Min.Y + ((g.Inner.Dy() - 1) / 2)
|
||||||
if labelYCoordinate < g.Inner.Max.Y {
|
if labelYCoordinate < g.Inner.Max.Y {
|
||||||
|
|
|
@ -5,28 +5,28 @@ import (
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRectLeftAgeCenter(rect image.Rectangle) image.Point {
|
func GetRectLeftSideCenter(rect image.Rectangle) image.Point {
|
||||||
return image.Point{
|
return image.Point{
|
||||||
X: rect.Min.X,
|
X: rect.Min.X,
|
||||||
Y: rect.Min.Y + rect.Dy()/2,
|
Y: rect.Min.Y + rect.Dy()/2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRectRightAgeCenter(rect image.Rectangle) image.Point {
|
func GetRectRightSideCenter(rect image.Rectangle) image.Point {
|
||||||
return image.Point{
|
return image.Point{
|
||||||
X: rect.Max.X,
|
X: rect.Max.X,
|
||||||
Y: rect.Min.Y + rect.Dy()/2,
|
Y: rect.Min.Y + rect.Dy()/2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRectTopAgeCenter(rect image.Rectangle) image.Point {
|
func GetRectTopSideCenter(rect image.Rectangle) image.Point {
|
||||||
return image.Point{
|
return image.Point{
|
||||||
X: rect.Min.X + rect.Dx()/2,
|
X: rect.Min.X + rect.Dx()/2,
|
||||||
Y: rect.Min.Y,
|
Y: rect.Min.Y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRectBottomAgeCenter(rect image.Rectangle) image.Point {
|
func GetRectBottomSideCenter(rect image.Rectangle) image.Point {
|
||||||
return image.Point{
|
return image.Point{
|
||||||
X: rect.Min.X + rect.Dx()/2,
|
X: rect.Min.X + rect.Dx()/2,
|
||||||
Y: rect.Max.Y,
|
Y: rect.Max.Y,
|
||||||
|
|
|
@ -6,16 +6,17 @@ import (
|
||||||
"github.com/sqshq/sampler/component/runchart"
|
"github.com/sqshq/sampler/component/runchart"
|
||||||
"github.com/sqshq/sampler/config"
|
"github.com/sqshq/sampler/config"
|
||||||
"github.com/sqshq/sampler/console"
|
"github.com/sqshq/sampler/console"
|
||||||
|
"github.com/sqshq/sampler/data"
|
||||||
"image"
|
"image"
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Layout struct {
|
type Layout struct {
|
||||||
ui.Block
|
ui.Block
|
||||||
Components []component.Component
|
Components []*component.Component
|
||||||
ChangeModeEvents chan Mode
|
|
||||||
statusbar *component.StatusBar
|
statusbar *component.StatusBar
|
||||||
menu *component.Menu
|
menu *component.Menu
|
||||||
|
ChangeModeEvents chan Mode
|
||||||
mode Mode
|
mode Mode
|
||||||
selection int
|
selection int
|
||||||
}
|
}
|
||||||
|
@ -47,7 +48,7 @@ func NewLayout(width, height int, statusline *component.StatusBar, menu *compone
|
||||||
|
|
||||||
return &Layout{
|
return &Layout{
|
||||||
Block: block,
|
Block: block,
|
||||||
Components: make([]component.Component, 0),
|
Components: make([]*component.Component, 0),
|
||||||
statusbar: statusline,
|
statusbar: statusline,
|
||||||
menu: menu,
|
menu: menu,
|
||||||
mode: ModeDefault,
|
mode: ModeDefault,
|
||||||
|
@ -56,28 +57,8 @@ func NewLayout(width, height int, statusline *component.StatusBar, menu *compone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) AddComponent(Type config.ComponentType, drawable ui.Drawable, title string, position config.Position, size config.Size, refreshRateMs int) {
|
func (l *Layout) AddComponent(cpt *component.Component, Type config.ComponentType) {
|
||||||
l.Components = append(l.Components, component.Component{
|
l.Components = append(l.Components, cpt)
|
||||||
Type: Type,
|
|
||||||
Drawable: drawable,
|
|
||||||
Title: title,
|
|
||||||
Position: position,
|
|
||||||
Size: size,
|
|
||||||
RefreshRateMs: refreshRateMs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Layout) GetComponents(Type config.ComponentType) []component.Component {
|
|
||||||
|
|
||||||
var components []component.Component
|
|
||||||
|
|
||||||
for _, c := range l.Components {
|
|
||||||
if c.Type == Type {
|
|
||||||
components = append(components, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return components
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) changeMode(m Mode) {
|
func (l *Layout) changeMode(m Mode) {
|
||||||
|
@ -86,14 +67,16 @@ func (l *Layout) changeMode(m Mode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) HandleConsoleEvent(e string) {
|
func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
|
|
||||||
|
selected := l.getSelection()
|
||||||
|
|
||||||
switch e {
|
switch e {
|
||||||
case console.KeyPause:
|
case console.KeyPause:
|
||||||
if l.mode == ModePause {
|
if l.mode == ModePause {
|
||||||
l.changeMode(ModeDefault)
|
l.changeMode(ModeDefault)
|
||||||
} else {
|
} else {
|
||||||
if l.getSelectedComponent().Type == config.TypeRunChart {
|
if selected.Type == config.TypeRunChart {
|
||||||
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
selected.CommandChannel <- data.Command{Type: runchart.CommandDisableSelection}
|
||||||
chart.DisableSelection()
|
|
||||||
}
|
}
|
||||||
l.menu.Idle()
|
l.menu.Idle()
|
||||||
l.changeMode(ModePause)
|
l.changeMode(ModePause)
|
||||||
|
@ -115,8 +98,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case component.MenuOptionPinpoint:
|
case component.MenuOptionPinpoint:
|
||||||
l.changeMode(ModeChartPinpoint)
|
l.changeMode(ModeChartPinpoint)
|
||||||
l.menu.Idle()
|
l.menu.Idle()
|
||||||
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: 0}
|
||||||
chart.MoveSelection(0)
|
|
||||||
case component.MenuOptionResume:
|
case component.MenuOptionResume:
|
||||||
l.changeMode(ModeDefault)
|
l.changeMode(ModeDefault)
|
||||||
l.menu.Idle()
|
l.menu.Idle()
|
||||||
|
@ -130,8 +112,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case console.KeyEsc:
|
case console.KeyEsc:
|
||||||
switch l.mode {
|
switch l.mode {
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
selected.CommandChannel <- data.Command{Type: runchart.CommandDisableSelection}
|
||||||
chart.DisableSelection()
|
|
||||||
fallthrough
|
fallthrough
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
fallthrough
|
fallthrough
|
||||||
|
@ -145,15 +126,14 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
l.changeMode(ModeComponentSelect)
|
l.changeMode(ModeComponentSelect)
|
||||||
l.menu.Highlight(l.getComponent(l.selection))
|
l.menu.Highlight(l.getComponent(l.selection))
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: -1}
|
||||||
chart.MoveSelection(-1)
|
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
l.moveSelection(e)
|
l.moveSelection(e)
|
||||||
l.menu.Highlight(l.getComponent(l.selection))
|
l.menu.Highlight(l.getComponent(l.selection))
|
||||||
case ModeComponentMove:
|
case ModeComponentMove:
|
||||||
l.getSelectedComponent().Move(-1, 0)
|
selected.Move(-1, 0)
|
||||||
case ModeComponentResize:
|
case ModeComponentResize:
|
||||||
l.getSelectedComponent().Resize(-1, 0)
|
selected.Resize(-1, 0)
|
||||||
}
|
}
|
||||||
case console.KeyRight:
|
case console.KeyRight:
|
||||||
switch l.mode {
|
switch l.mode {
|
||||||
|
@ -161,15 +141,14 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
l.changeMode(ModeComponentSelect)
|
l.changeMode(ModeComponentSelect)
|
||||||
l.menu.Highlight(l.getComponent(l.selection))
|
l.menu.Highlight(l.getComponent(l.selection))
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
selected.CommandChannel <- data.Command{Type: runchart.CommandMoveSelection, Value: 1}
|
||||||
chart.MoveSelection(1)
|
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
l.moveSelection(e)
|
l.moveSelection(e)
|
||||||
l.menu.Highlight(l.getComponent(l.selection))
|
l.menu.Highlight(l.getComponent(l.selection))
|
||||||
case ModeComponentMove:
|
case ModeComponentMove:
|
||||||
l.getSelectedComponent().Move(1, 0)
|
selected.Move(1, 0)
|
||||||
case ModeComponentResize:
|
case ModeComponentResize:
|
||||||
l.getSelectedComponent().Resize(1, 0)
|
selected.Resize(1, 0)
|
||||||
}
|
}
|
||||||
case console.KeyUp:
|
case console.KeyUp:
|
||||||
switch l.mode {
|
switch l.mode {
|
||||||
|
@ -182,9 +161,9 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case ModeMenuOptionSelect:
|
case ModeMenuOptionSelect:
|
||||||
l.menu.Up()
|
l.menu.Up()
|
||||||
case ModeComponentMove:
|
case ModeComponentMove:
|
||||||
l.getSelectedComponent().Move(0, -1)
|
selected.Move(0, -1)
|
||||||
case ModeComponentResize:
|
case ModeComponentResize:
|
||||||
l.getSelectedComponent().Resize(0, -1)
|
selected.Resize(0, -1)
|
||||||
}
|
}
|
||||||
case console.KeyDown:
|
case console.KeyDown:
|
||||||
switch l.mode {
|
switch l.mode {
|
||||||
|
@ -197,9 +176,9 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case ModeMenuOptionSelect:
|
case ModeMenuOptionSelect:
|
||||||
l.menu.Down()
|
l.menu.Down()
|
||||||
case ModeComponentMove:
|
case ModeComponentMove:
|
||||||
l.getSelectedComponent().Move(0, 1)
|
selected.Move(0, 1)
|
||||||
case ModeComponentResize:
|
case ModeComponentResize:
|
||||||
l.getSelectedComponent().Resize(0, 1)
|
selected.Resize(0, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -209,16 +188,16 @@ func (l *Layout) ChangeDimensions(width, height int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) getComponent(i int) *component.Component {
|
func (l *Layout) getComponent(i int) *component.Component {
|
||||||
return &l.Components[i]
|
return l.Components[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) getSelectedComponent() *component.Component {
|
func (l *Layout) getSelection() *component.Component {
|
||||||
return &l.Components[l.selection]
|
return l.Components[l.selection]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Layout) moveSelection(direction string) {
|
func (l *Layout) moveSelection(direction string) {
|
||||||
|
|
||||||
previouslySelected := *l.getSelectedComponent()
|
previouslySelected := l.getSelection()
|
||||||
newlySelectedIndex := l.selection
|
newlySelectedIndex := l.selection
|
||||||
|
|
||||||
for i, current := range l.Components {
|
for i, current := range l.Components {
|
||||||
|
@ -237,21 +216,21 @@ func (l *Layout) moveSelection(direction string) {
|
||||||
|
|
||||||
switch direction {
|
switch direction {
|
||||||
case console.KeyLeft:
|
case console.KeyLeft:
|
||||||
previouslySelectedCornerPoint = component.GetRectLeftAgeCenter(previouslySelected.Drawable.GetRect())
|
previouslySelectedCornerPoint = component.GetRectLeftSideCenter(previouslySelected.GetRect())
|
||||||
newlySelectedCornerPoint = component.GetRectRightAgeCenter(l.getComponent(newlySelectedIndex).Drawable.GetRect())
|
newlySelectedCornerPoint = component.GetRectRightSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
||||||
currentCornerPoint = component.GetRectRightAgeCenter(current.Drawable.GetRect())
|
currentCornerPoint = component.GetRectRightSideCenter(current.GetRect())
|
||||||
case console.KeyRight:
|
case console.KeyRight:
|
||||||
previouslySelectedCornerPoint = component.GetRectRightAgeCenter(previouslySelected.Drawable.GetRect())
|
previouslySelectedCornerPoint = component.GetRectRightSideCenter(previouslySelected.GetRect())
|
||||||
newlySelectedCornerPoint = component.GetRectLeftAgeCenter(l.getComponent(newlySelectedIndex).Drawable.GetRect())
|
newlySelectedCornerPoint = component.GetRectLeftSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
||||||
currentCornerPoint = component.GetRectLeftAgeCenter(current.Drawable.GetRect())
|
currentCornerPoint = component.GetRectLeftSideCenter(current.GetRect())
|
||||||
case console.KeyUp:
|
case console.KeyUp:
|
||||||
previouslySelectedCornerPoint = component.GetRectTopAgeCenter(previouslySelected.Drawable.GetRect())
|
previouslySelectedCornerPoint = component.GetRectTopSideCenter(previouslySelected.GetRect())
|
||||||
newlySelectedCornerPoint = component.GetRectBottomAgeCenter(l.getComponent(newlySelectedIndex).Drawable.GetRect())
|
newlySelectedCornerPoint = component.GetRectBottomSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
||||||
currentCornerPoint = component.GetRectBottomAgeCenter(current.Drawable.GetRect())
|
currentCornerPoint = component.GetRectBottomSideCenter(current.GetRect())
|
||||||
case console.KeyDown:
|
case console.KeyDown:
|
||||||
previouslySelectedCornerPoint = component.GetRectBottomAgeCenter(previouslySelected.Drawable.GetRect())
|
previouslySelectedCornerPoint = component.GetRectBottomSideCenter(previouslySelected.GetRect())
|
||||||
newlySelectedCornerPoint = component.GetRectTopAgeCenter(l.getComponent(newlySelectedIndex).Drawable.GetRect())
|
newlySelectedCornerPoint = component.GetRectTopSideCenter(l.getComponent(newlySelectedIndex).GetRect())
|
||||||
currentCornerPoint = component.GetRectTopAgeCenter(current.Drawable.GetRect())
|
currentCornerPoint = component.GetRectTopSideCenter(current.GetRect())
|
||||||
}
|
}
|
||||||
|
|
||||||
if component.GetDistance(previouslySelectedCornerPoint, currentCornerPoint) <
|
if component.GetDistance(previouslySelectedCornerPoint, currentCornerPoint) <
|
||||||
|
@ -283,8 +262,8 @@ func (l *Layout) Draw(buffer *ui.Buffer) {
|
||||||
y2 = y1 + minDimension
|
y2 = y1 + minDimension
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Drawable.SetRect(int(x1), int(y1), int(x2), int(y2))
|
c.SetRect(int(x1), int(y1), int(x2), int(y2))
|
||||||
c.Drawable.Draw(buffer)
|
c.Draw(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.statusbar.SetRect(
|
l.statusbar.SetRect(
|
||||||
|
|
|
@ -219,7 +219,7 @@ func (m *Menu) renderOptions(buffer *ui.Buffer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) updateDimensions() {
|
func (m *Menu) updateDimensions() {
|
||||||
r := m.component.Drawable.GetRect()
|
r := m.component.Block.GetRect()
|
||||||
m.SetRect(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
m.SetRect(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
xAxisLabelsHeight = 1
|
xAxisLabelsHeight = 1
|
||||||
xAxisLabelsWidth = 8
|
xAxisLabelsWidth = 8
|
||||||
xAxisLabelsIndent = 2
|
xAxisLabelsIndent = 2
|
||||||
xAxisGridWidth = xAxisLabelsIndent + xAxisLabelsWidth
|
xAxisGridWidth = xAxisLabelsIndent + xAxisLabelsWidth
|
||||||
yAxisLabelsHeight = 1
|
yAxisLabelsHeight = 1
|
||||||
yAxisLabelsIndent = 1
|
yAxisLabelsIndent = 1
|
||||||
|
historyReserveMin = 20
|
||||||
historyReserveMin = 20
|
|
||||||
|
|
||||||
xBrailleMultiplier = 2
|
xBrailleMultiplier = 2
|
||||||
yBrailleMultiplier = 4
|
yBrailleMultiplier = 4
|
||||||
)
|
)
|
||||||
|
@ -36,10 +34,13 @@ const (
|
||||||
ModePinpoint Mode = 1
|
ModePinpoint Mode = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandDisableSelection = "DISABLE_SELECTION"
|
||||||
|
CommandMoveSelection = "MOVE_SELECTION"
|
||||||
|
)
|
||||||
|
|
||||||
type RunChart struct {
|
type RunChart struct {
|
||||||
ui.Block
|
*component.Component
|
||||||
data.Consumer
|
|
||||||
*component.Alerter
|
|
||||||
lines []TimeLine
|
lines []TimeLine
|
||||||
grid ChartGrid
|
grid ChartGrid
|
||||||
timescale time.Duration
|
timescale time.Duration
|
||||||
|
@ -75,40 +76,41 @@ type ValueExtrema struct {
|
||||||
min float64
|
min float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunChart(c config.RunChartConfig, l Legend) *RunChart {
|
func NewRunChart(c config.RunChartConfig) *RunChart {
|
||||||
|
|
||||||
consumer := data.NewConsumer()
|
|
||||||
block := *ui.NewBlock()
|
|
||||||
block.Title = c.Title
|
|
||||||
|
|
||||||
chart := RunChart{
|
chart := RunChart{
|
||||||
Block: block,
|
Component: component.NewComponent(c.ComponentConfig, config.TypeRunChart),
|
||||||
Consumer: consumer,
|
|
||||||
Alerter: component.NewAlerter(consumer.AlertChannel),
|
|
||||||
lines: []TimeLine{},
|
lines: []TimeLine{},
|
||||||
timescale: calculateTimescale(*c.RefreshRateMs),
|
timescale: calculateTimescale(*c.RefreshRateMs),
|
||||||
mutex: &sync.Mutex{},
|
mutex: &sync.Mutex{},
|
||||||
scale: *c.Scale,
|
scale: *c.Scale,
|
||||||
mode: ModeDefault,
|
mode: ModeDefault,
|
||||||
legend: l,
|
legend: Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details},
|
||||||
}
|
}
|
||||||
|
|
||||||
go chart.consume()
|
for _, i := range c.Items {
|
||||||
|
chart.AddLine(*i.Label, *i.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sample := <-chart.SampleChannel:
|
||||||
|
chart.consumeSample(sample)
|
||||||
|
case command := <-chart.CommandChannel:
|
||||||
|
switch command.Type {
|
||||||
|
case CommandDisableSelection:
|
||||||
|
chart.disableSelection()
|
||||||
|
case CommandMoveSelection:
|
||||||
|
chart.moveSelection(command.Value.(int))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return &chart
|
return &chart
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RunChart) consume() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sample := <-c.SampleChannel:
|
|
||||||
c.consumeSample(sample)
|
|
||||||
//case alert := <-c.alertChannel:
|
|
||||||
// TODO base alerting mechanism
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RunChart) newTimePoint(value float64) TimePoint {
|
func (c *RunChart) newTimePoint(value float64) TimePoint {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
return TimePoint{
|
return TimePoint{
|
||||||
|
@ -151,7 +153,11 @@ func (c *RunChart) consumeSample(sample data.Sample) {
|
||||||
float, err := strconv.ParseFloat(sample.Value, 64)
|
float, err := strconv.ParseFloat(sample.Value, 64)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO visual notification
|
c.AlertChannel <- data.Alert{
|
||||||
|
Title: "SAMPLING FAILURE",
|
||||||
|
Text: err.Error(),
|
||||||
|
Color: sample.Color,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
|
@ -320,7 +326,7 @@ func (c *RunChart) getMaxValueLength() int {
|
||||||
return maxValueLength
|
return maxValueLength
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RunChart) MoveSelection(shift int) {
|
func (c *RunChart) moveSelection(shift int) {
|
||||||
|
|
||||||
if c.mode == ModeDefault {
|
if c.mode == ModeDefault {
|
||||||
c.mode = ModePinpoint
|
c.mode = ModePinpoint
|
||||||
|
@ -340,7 +346,7 @@ func (c *RunChart) MoveSelection(shift int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RunChart) DisableSelection() {
|
func (c *RunChart) disableSelection() {
|
||||||
if c.mode == ModePinpoint {
|
if c.mode == ModePinpoint {
|
||||||
c.mode = ModeDefault
|
c.mode = ModeDefault
|
||||||
return
|
return
|
||||||
|
|
|
@ -24,16 +24,16 @@ type Flags struct {
|
||||||
|
|
||||||
func Load() (Config, Flags) {
|
func Load() (Config, Flags) {
|
||||||
|
|
||||||
if len(os.Args) < 2 {
|
//if len(os.Args) < 2 {
|
||||||
println("Please specify config file location. See www.github.com/sqshq/sampler for the reference")
|
// println("Please specify config file location. See www.github.com/sqshq/sampler for the reference")
|
||||||
os.Exit(0)
|
// os.Exit(0)
|
||||||
}
|
//}
|
||||||
|
|
||||||
cfg := readFile(os.Args[1])
|
cfg := readFile("config.yml")
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
cfg.setDefaults()
|
cfg.setDefaults()
|
||||||
|
|
||||||
flg := Flags{ConfigFileName: os.Args[1]}
|
flg := Flags{ConfigFileName: "config.yml"}
|
||||||
|
|
||||||
return *cfg, flg
|
return *cfg, flg
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,10 @@ const (
|
||||||
AppVersion = "0.1.0"
|
AppVersion = "0.1.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BellCharacter = "\a"
|
||||||
|
)
|
||||||
|
|
||||||
type AsciiFont string
|
type AsciiFont string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,13 +25,7 @@ const (
|
||||||
AsciiFont3D AsciiFont = "3d"
|
AsciiFont3D AsciiFont = "3d"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Console struct{}
|
func Init() {
|
||||||
|
|
||||||
const (
|
|
||||||
BellCharacter = "\a"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (self *Console) Init() {
|
|
||||||
|
|
||||||
fmt.Printf("\033]0;%s\007", AppTitle)
|
fmt.Printf("\033]0;%s\007", AppTitle)
|
||||||
|
|
||||||
|
@ -36,6 +34,6 @@ func (self *Console) Init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Console) Close() {
|
func Close() {
|
||||||
ui.Close()
|
ui.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package data
|
||||||
|
|
||||||
import ui "github.com/gizak/termui/v3"
|
import ui "github.com/gizak/termui/v3"
|
||||||
|
|
||||||
|
// TODO interface here, move fields declaration in the Component
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
SampleChannel chan Sample
|
SampleChannel chan Sample
|
||||||
AlertChannel chan Alert
|
AlertChannel chan Alert
|
||||||
|
CommandChannel chan Command
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sample struct {
|
type Sample struct {
|
||||||
|
@ -19,9 +21,15 @@ type Alert struct {
|
||||||
Color *ui.Color
|
Color *ui.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Command struct {
|
||||||
|
Type string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
func NewConsumer() Consumer {
|
func NewConsumer() Consumer {
|
||||||
return Consumer{
|
return Consumer{
|
||||||
SampleChannel: make(chan Sample),
|
SampleChannel: make(chan Sample),
|
||||||
AlertChannel: make(chan Alert),
|
AlertChannel: make(chan Alert),
|
||||||
|
CommandChannel: make(chan Command),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,9 @@ type Handler struct {
|
||||||
renderRate time.Duration
|
renderRate time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(layout *layout.Layout) Handler {
|
func NewHandler(layout *layout.Layout) *Handler {
|
||||||
renderRate := calcMinRenderRate(layout)
|
renderRate := calcMinRenderRate(layout)
|
||||||
return Handler{
|
return &Handler{
|
||||||
layout: layout,
|
layout: layout,
|
||||||
consoleEvents: ui.PollEvents(),
|
consoleEvents: ui.PollEvents(),
|
||||||
renderTicker: time.NewTicker(renderRate),
|
renderTicker: time.NewTicker(renderRate),
|
||||||
|
|
47
main.go
47
main.go
|
@ -18,57 +18,42 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
cfg, flg := config.Load()
|
cfg, flg := config.Load()
|
||||||
csl := console.Console{}
|
|
||||||
csl.Init()
|
|
||||||
defer csl.Close()
|
|
||||||
|
|
||||||
width, height := ui.TerminalDimensions()
|
console.Init()
|
||||||
lout := layout.NewLayout(width, height, component.NewStatusLine(flg.ConfigFileName), component.NewMenu())
|
defer console.Close()
|
||||||
|
|
||||||
player := asset.NewAudioPlayer()
|
player := asset.NewAudioPlayer()
|
||||||
defer player.Close()
|
defer player.Close()
|
||||||
|
|
||||||
|
width, height := ui.TerminalDimensions()
|
||||||
|
lout := layout.NewLayout(width, height, component.NewStatusLine(flg.ConfigFileName), component.NewMenu())
|
||||||
|
|
||||||
for _, c := range cfg.RunCharts {
|
for _, c := range cfg.RunCharts {
|
||||||
|
chart := runchart.NewRunChart(c)
|
||||||
legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details}
|
|
||||||
chart := runchart.NewRunChart(c, legend)
|
|
||||||
lout.AddComponent(config.TypeRunChart, chart, c.Title, c.Position, c.Size, *c.RefreshRateMs)
|
|
||||||
triggers := data.NewTriggers(c.Triggers, chart.Consumer, player)
|
triggers := data.NewTriggers(c.Triggers, chart.Consumer, player)
|
||||||
items := data.NewItems(c.Items)
|
data.NewSampler(chart.Consumer, data.NewItems(c.Items), triggers, *c.RefreshRateMs)
|
||||||
data.NewSampler(chart.Consumer, items, triggers, *c.RefreshRateMs)
|
lout.AddComponent(chart.Component, config.TypeRunChart)
|
||||||
|
|
||||||
for _, i := range c.Items {
|
|
||||||
chart.AddLine(*i.Label, *i.Color)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range cfg.AsciiBoxes {
|
for _, a := range cfg.AsciiBoxes {
|
||||||
box := asciibox.NewAsciiBox(a)
|
box := asciibox.NewAsciiBox(a)
|
||||||
item := data.Item{Label: *a.Label, Script: a.Script, Color: a.Color}
|
|
||||||
lout.AddComponent(config.TypeAsciiBox, box, a.Title, a.Position, a.Size, *a.RefreshRateMs)
|
|
||||||
triggers := data.NewTriggers(a.Triggers, box.Consumer, player)
|
triggers := data.NewTriggers(a.Triggers, box.Consumer, player)
|
||||||
data.NewSampler(box.Consumer, []data.Item{item}, triggers, *a.RefreshRateMs)
|
data.NewSampler(box.Consumer, data.NewItems([]config.Item{a.Item}), triggers, *a.RefreshRateMs)
|
||||||
|
lout.AddComponent(box.Component, config.TypeAsciiBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range cfg.BarCharts {
|
for _, b := range cfg.BarCharts {
|
||||||
|
chart := barchart.NewBarChart(b)
|
||||||
chart := barchart.NewBarChart(b.Title, *b.Scale)
|
|
||||||
triggers := data.NewTriggers(b.Triggers, chart.Consumer, player)
|
triggers := data.NewTriggers(b.Triggers, chart.Consumer, player)
|
||||||
lout.AddComponent(config.TypeBarChart, chart, b.Title, b.Position, b.Size, *b.RefreshRateMs)
|
data.NewSampler(chart.Consumer, data.NewItems(b.Items), triggers, *b.RefreshRateMs)
|
||||||
items := data.NewItems(b.Items)
|
lout.AddComponent(chart.Component, config.TypeBarChart)
|
||||||
data.NewSampler(chart.Consumer, items, triggers, *b.RefreshRateMs)
|
|
||||||
|
|
||||||
for _, i := range b.Items {
|
|
||||||
chart.AddBar(*i.Label, *i.Color)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, gc := range cfg.Gauges {
|
for _, gc := range cfg.Gauges {
|
||||||
g := gauge.NewGauge(gc.Title, *gc.Scale, *gc.Color)
|
g := gauge.NewGauge(gc)
|
||||||
triggers := data.NewTriggers(gc.Triggers, g.Consumer, player)
|
triggers := data.NewTriggers(gc.Triggers, g.Consumer, player)
|
||||||
lout.AddComponent(config.TypeGauge, g, gc.Title, gc.Position, gc.Size, *gc.RefreshRateMs)
|
data.NewSampler(g.Consumer, data.NewItems(gc.Items), triggers, *gc.RefreshRateMs)
|
||||||
items := data.NewItems(gc.Items)
|
lout.AddComponent(g.Component, config.TypeGauge)
|
||||||
data.NewSampler(g.Consumer, items, triggers, *gc.RefreshRateMs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := event.NewHandler(lout)
|
handler := event.NewHandler(lout)
|
||||||
|
|
Loading…
Reference in New Issue