package tui import ( "fmt" "log" "sync" "strconv" "strings" "time" "FRMS/internal/pkg/logging" "github.com/rivo/tview" _ "github.com/gdamore/tcell/v2" "os" ) type Device struct { Id uint32 Type string Status string Data string Index uint32 } type TUI struct { *Display *TUIClient SelectedReactor <-chan uint32 SelectedDevice <-chan uint32 Err chan error } func NewTUI(ip string, port int, ifconfig string, ch chan error) *TUI { t := &TUI{} t.Err = ch client := NewTUIClient(ip, port, ifconfig) t.TUIClient = client return t } func (t *TUI) Start() { // setup tview app and wait for user connection in standin modal if err := t.TUIClient.Start(); err != nil { t.Err <- err } logging.Debug(logging.DStart, "TUI %v starting", t.Id) go t.Monitor() t.CreateDisplay() t.Display.Start() } func (t *TUI) CreateDisplay() { rc := make(chan uint32) dc := make(chan uint32) t.Display = NewDisplay(rc,dc) t.SelectedReactor = rc t.SelectedDevice = dc t.Flex.AddItem(t.ReactorList,0,1,true). AddItem(t.DeviceList,0,2,false) } func (t *TUI) Monitor() { // orchestrates updates and grpc requests timer := make(chan struct{}) go func(signal chan struct{}){ for { signal <- struct{}{} time.Sleep(1 * time.Second) } }(timer) for { select { case reactor := <-t.SelectedReactor: // reactor has been selected in tui, grabbing devs t.App.QueueUpdateDraw(func() { t.UpdateDevices(reactor) }) logging.Debug(logging.DClient, "%v getting reactor devices", t.Id) case dev := <-t.SelectedDevice: logging.Debug(logging.DClient, "%v editing device %v", t.Id, dev) // TODO case <-timer: // time to ping for status logging.Debug(logging.DClient, "%v pinging for updates", t.Id) t.App.QueueUpdateDraw(func() { t.UpdateDevices() }) } } } func (t *TUI) UpdateDevices(r ...uint32) { // get devices for the reactor and update the tui // see if there is a page being displayed // overwrite if called as a func var devs map[uint32]*Device var err error if len(r) > 0 { // could be a reactor id or 1 for update reactors if r[0] != 0 { t.Display.DeviceList.Clear() } else { t.ReactorList.Clear() t.ReactorList.AddItem("Refresh","Press (r) to refresh manually", 114, nil) t.ReactorList.AddItem("Quit","Press (q) to quit",113,func() { t.App.Stop() os.Exit(0) }) } devs, err = t.TUIClient.GetDevices(r[0]) } else { devs, err = t.TUIClient.GetDevices() } if err != nil { log.Fatal(err) } //if id != 0 { // split based on type to simplify update reactors := make(map[uint32]*Device) devices := make(map[uint32]*Device) for id, dev := range devs { if dev.Type == "Reactor" { reactors[id] = dev } else { devices[id] = dev } } t.DisplayDevices(devices) t.DisplayReactors(reactors) } // display struct and logic type Display struct { App *tview.Application Flex *tview.Flex ReactorList *tview.List DeviceList *tview.List SelectedReactor chan<- uint32 SelectedDevice chan<- uint32 sync.Mutex } func NewDisplay(rc,dc chan uint32) *Display { d := &Display{} d.App = tview.NewApplication() d.Flex = tview.NewFlex() d.DeviceList = tview.NewList() d.ReactorList = tview.NewList() d.ReactorList.AddItem("Refresh","Press (r) to refresh manually", 114, nil) d.ReactorList.AddItem("Quit","Press (q) to quit",113,func() { d.App.Stop() os.Exit(0) }) d.ReactorList.SetTitle("Reactors").SetBorder(true) d.ReactorList.SetSelectedFunc(d.SelectReactor) d.DeviceList.SetTitle("Devices").SetBorder(true) d.DeviceList.SetSelectedFunc(d.SelectDevice) d.SelectedReactor = rc d.SelectedDevice = dc return d } func (d *Display) Start() { if err := d.App.SetRoot(d.Flex, true).Run(); err != nil { d.App.Stop() log.Fatal(err) } } func (d *Display) DisplayReactors(r map[uint32]*Device) { // this func takes in a list of devices to update and loops over them // works by padding list for entries not seen yet for _, reactor := range r { txt := fmt.Sprintf("%v %v", reactor.Id, reactor.Status) indx := int(reactor.Index) for indx + 2 >= d.ReactorList.GetItemCount() { // this prevent overwriting quit entry d.ReactorList.InsertItem(-3,txt,reactor.Data,rune(47+d.ReactorList.GetItemCount()),nil) } if indx + 2 < d.ReactorList.GetItemCount() { d.ReactorList.SetItemText(indx,txt,reactor.Data) } } } func (d *Display) DisplayDevices(devs map[uint32]*Device) { // going to just clear every time as we reload new dev lists anyway // going to clear on every reactor selection to simplify // can probably just load from SM to save system resources on spam reloading for _, dev := range devs { txt := fmt.Sprintf("0x%x %v %v",dev.Id,dev.Status,dev.Type) indx := int(dev.Index) for indx >= d.DeviceList.GetItemCount() { d.DeviceList.AddItem(txt,dev.Data,rune(49+d.DeviceList.GetItemCount()), nil) } if indx < d.DeviceList.GetItemCount() { d.DeviceList.SetItemText(indx,txt,dev.Data) } } } func (d *Display) SelectReactor(index int, main, data string, r rune) { // called when reactor in list in selected if main != "Quit" { if main == "Refresh" { // TODO } else { maintxt := strings.Split(main," ") id := maintxt[0] if id, err := strconv.ParseUint(id, 10, 32); err != nil { log.Fatal(err) } else { d.SelectedReactor <-uint32(id) } } } } func (d *Display) SelectDevice(index int, main, data string, r rune) { // called when device is selected in sub menu maintxt := strings.Split(main," ") id := maintxt[0] id = strings.Trim(id,"0x \n") logging.Debug(logging.DClient,"Selected dev %v", id) if id, err := strconv.ParseUint(id, 16, 32); err != nil { logging.Debug(logging.DError, "Error parsing: %v", err) os.Exit(1) } else { d.SelectedDevice <-uint32(id) } }