Tease out fim code, further implementation of config ui (INCOMPLETE)

This commit is contained in:
KemoNine 2020-08-31 22:57:23 -04:00
parent 4a516cbed8
commit 83743a9f90
2 changed files with 331 additions and 120 deletions

118
cmd/ui/slideshow.go Normal file
View file

@ -0,0 +1,118 @@
package ui
import (
"context"
"io"
"log"
"os/exec"
"time"
"github.com/eiannone/keyboard"
)
const (
CMD_FIM = "/usr/local/bin/pf-fim.sh"
SLIDESHOW_INTERVAL = 300 * time.Second
)
func Slideshow() {
// fim placeholder so we can operate on it when a exit slideshow is received
var fim *exec.Cmd = nil
// Run slideshow
fim = exec.Command(CMD_FIM)
// Setup stdin for fim to control slideshow
stdin, err := fim.StdinPipe()
if err != nil {
log.Fatalf("Error getting fim stdin : %s", err)
}
// Advance slideshow every interval as defined in const()
ticker := time.NewTicker(SLIDESHOW_INTERVAL)
stop_ticker := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
_, err = io.WriteString(stdin, "n")
if err != nil {
log.Fatalf("Error advancing slides : %s", err)
}
case <-stop_ticker:
ticker.Stop()
stdin.Close()
return
}
}
}()
// Start watching for key strokes and echo them back to stdout
keysEvents, err := keyboard.GetKeys(10)
if err != nil {
log.Fatalf("Error setting up keyboard listener : %s", err)
}
// NOT deferring keyboard.close as we do that by hand further down when fim exits ahead of showing the config UI
// Goroutine for tracking which keys are pressed and controlling fim if appropriate
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
go func(keyboardCtx context.Context) {
for {
select {
case <-keyboardCtx.Done():
return
case event := <-keysEvents:
if event.Err != nil {
log.Fatalf("Error listening to key events : %s", err)
}
log.Printf("You pressed: key %X\n", event.Key)
// Keys for fim event management (previous/next in particular)
fimKey := ""
if event.Key == keyboard.KeyArrowLeft || event.Key == keyboard.KeyArrowDown {
fimKey = "p"
}
if event.Key == keyboard.KeyArrowRight || event.Key == keyboard.KeyArrowUp {
fimKey = "n"
}
// Exit fim and move to the config UI
if event.Key == keyboard.KeyEsc || event.Key == keyboard.KeyEnter || event.Key == keyboard.KeySpace {
if fim != nil { // Just in case someone lays on exit key or similar during startup
if err := fim.Process.Kill(); err != nil {
log.Fatalf("failed to kill fim : %s", err)
}
}
break
}
// Control fim if we received a valid key for next/previous slide
if fimKey != "" {
log.Printf("Sending key to fim : %s", fimKey)
_, err = io.WriteString(stdin, fimKey)
if err != nil {
log.Fatalf("Error controlling fim : %s", err)
}
ticker.Reset(SLIDESHOW_INTERVAL)
}
}
}
}(keyboardCtx)
// Run fim
// if err := fim.Run(); err != nil {
// // Unwrap the error a bit so we can find out if a signal killed fim or something else
// // An exit code of -1 means the program didn't exit in time or was terminated by a signal (per the docs)
// if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != -1 {
// log.Fatalf("Error running fim : %s", err)
// }
// }
// Stop fim slideshow advancing go routine
close(stop_ticker)
// Stop listening to keyboard events
keyboard.Close()
keyboardCancel()
}

View file

@ -2,122 +2,37 @@ package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"math"
"net"
"os/exec"
"strconv"
"strings"
"time"
"github.com/eiannone/keyboard"
"github.com/gdamore/tcell"
"github.com/guillermo/go.procmeminfo"
"github.com/rivo/tview"
)
const (
CMD_SYSTEMCTL = "/usr/bin/systemctl"
CMD_FINDMNT = "/usr/bin/findmnt"
CMD_FIM = "/usr/local/bin/pf-fim.sh"
SLIDESHOW_INTERVAL = 300 * time.Second
CMD_VCGENCMD = "/opt/vc/bin/vcgencmd"
FILE_CPU_TEMP = "/sys/class/thermal/thermal_zone0/temp"
)
const (
PAGE_MAIN_UI = "PAGE_MAIN_UI"
PAGE_EXIT = "PAGE_EXIT"
PAGE_REBOOT = "PAGE_REBOOT"
PAGE_POWEROFF = "PAGE_POWEROFF"
)
func main() {
// fim placeholder so we can operate on it when a exit slideshow is received
var fim *exec.Cmd = nil
// Run slideshow
fim = exec.Command(CMD_FIM)
// Setup stdin for fim to control slideshow
stdin, err := fim.StdinPipe()
if err != nil {
log.Fatalf("Error getting fim stdin : %s", err)
}
// Advance slideshow every interval as defined in const()
ticker := time.NewTicker(SLIDESHOW_INTERVAL)
stop_ticker := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
_, err = io.WriteString(stdin, "n")
if err != nil {
log.Fatalf("Error advancing slides : %s", err)
}
case <-stop_ticker:
ticker.Stop()
stdin.Close()
return
}
}
}()
// Start watching for key strokes and echo them back to stdout
keysEvents, err := keyboard.GetKeys(10)
if err != nil {
panic(err)
}
defer func() {
_ = keyboard.Close()
}()
// Goroutine for tracking which keys are pressed and controlling fim if appropriate
go func() {
for {
event := <-keysEvents
if event.Err != nil {
panic(event.Err)
}
log.Printf("You pressed: key %X\n", event.Key)
// Keys for fim event management (previous/next in particular)
fimKey := ""
if event.Key == keyboard.KeyArrowLeft || event.Key == keyboard.KeyArrowDown {
fimKey = "p"
}
if event.Key == keyboard.KeyArrowRight || event.Key == keyboard.KeyArrowUp {
fimKey = "n"
}
// Exit fim and move to the config UI
if event.Key == keyboard.KeyEsc || event.Key == keyboard.KeyEnter || event.Key == keyboard.KeySpace {
if fim != nil { // Just in case someone lays on exit key or similar during startup
if err := fim.Process.Kill(); err != nil {
log.Fatalf("failed to kill fim : %s", err)
}
}
break
}
// Control fim if we received a valid key for next/previous slide
if fimKey != "" {
log.Printf("Sending key to fim : %s", fimKey)
_, err = io.WriteString(stdin, fimKey)
if err != nil {
log.Fatalf("Error controlling fim : %s", err)
}
ticker.Reset(SLIDESHOW_INTERVAL)
}
}
}()
// Run fim
// if err := fim.Run(); err != nil {
// // Unwrap the error a bit so we can find out if a signal killed fim or something else
// // An exit code of -1 means the program didn't exit in time or was terminated by a signal (per the docs)
// if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != -1 {
// log.Fatalf("Error running fim : %s", err)
// }
// }
// Stop fim slideshow advancing go routine
close(stop_ticker)
// Memory info for status panel
meminfo := &procmeminfo.MemInfo{}
err = meminfo.Update()
err := meminfo.Update()
if err != nil {
log.Printf("Error getting memory info : %s", err)
}
@ -135,6 +50,19 @@ func main() {
"-t", "ext4,exfat,vfat,btrfs,zfs,xfs").Output()
filesystems := strings.Split(strings.Trim(string(findmntOut), "\n"), "\n")
// GPU Temp
vcgencmdOut, _ := exec.Command(CMD_VCGENCMD, "measure_temp").Output()
gpuTemp := strings.Split(strings.Trim(string(vcgencmdOut), "\n"), "=")[1]
// CPU Temp
cpuTempFileContents, err := ioutil.ReadFile(FILE_CPU_TEMP)
if err != nil {
log.Fatalf("Error reading file %s : %s", FILE_CPU_TEMP, err)
}
cpuTempStr := strings.Trim(string(cpuTempFileContents), "\n")
cpuTempInt, _ := strconv.Atoi(cpuTempStr)
cpuTemp := fmt.Sprintf("%.2f'C", float64(cpuTempInt)/1000.0)
// Run config UI when slideshow stops
app := tview.NewApplication()
@ -155,9 +83,7 @@ func main() {
// Footer fields (Left Column)
exitButton := tview.NewButton("Exit").
SetSelectedFunc(func() {
app.Stop()
})
SetBackgroundColorActivated(tcell.ColorGray)
exitButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
@ -165,9 +91,7 @@ func main() {
SetRect(0, 0, 22, 3)
rebootButton := tview.NewButton("Reboot").
SetSelectedFunc(func() {
app.Stop()
})
SetBackgroundColorActivated(tcell.ColorGray)
rebootButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
@ -175,9 +99,7 @@ func main() {
SetRect(0, 0, 22, 3)
powerOffButton := tview.NewButton("Power Off").
SetSelectedFunc(func() {
app.Stop()
})
SetBackgroundColorActivated(tcell.ColorGray)
powerOffButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
@ -189,21 +111,70 @@ func main() {
footer.AddItem(exitButton, 0, 1, false).
AddItem(rebootButton, 0, 1, false).
AddItem(powerOffButton, 0, 1, false)
// Setup menu
menu := tview.NewFlex().
menu := tview.NewList()
menu.SetBorder(true).
SetTitle("Menu").
SetTitleColor(tcell.ColorAqua)
menu.AddItem("Select Albums", "", '1', nil)
menu.AddItem("Configure WiFi", "", '2', nil)
// Setup base var for main column so the menu setup is easier to manage
main := tview.NewFlex().
SetDirection(tview.FlexRow)
menu.SetTitle("Menu").
// Setup main panel (Center column)
main.SetTitle("").
SetBorder(true).
SetTitleColor(tcell.ColorAqua)
// Setup main panel (Center column)
main := tview.NewFlex().
SetDirection(tview.FlexRow)
main.SetTitle("").
SetBorder(true)
// WiFi Config Form
wifiConfigForm := tview.NewForm()
wifiConfigForm.AddInputField("Access Point", "", 0, nil, nil)
wifiConfigForm.AddPasswordField("Password", "", 0, '*', nil)
wifiConfigForm.AddButton("Apply", nil)
wifiConfigForm.AddButton("Cancel", func() {
main.Clear()
app.SetFocus(menu)
})
// Select Albums Form
selectAlbumsForm := tview.NewForm()
selectAlbumsForm.AddCheckbox("Album 1", false, nil)
selectAlbumsForm.AddCheckbox("Album 2", false, nil)
selectAlbumsForm.AddButton("Apply", nil)
selectAlbumsForm.AddButton("Cancel", func() {
main.Clear()
app.SetFocus(menu)
})
// Setup menu selection handler
menu.SetSelectedFunc(func(index int, title string, desc string, shortcut rune) {
if title == "Select Albums" {
main.SetTitle("Select Albums")
main.Clear()
main.AddItem(selectAlbumsForm, 0, 1, true)
app.SetFocus(selectAlbumsForm)
}
if title == "Configure WiFi" {
main.SetTitle("Configure WiFi")
main.Clear()
main.AddItem(wifiConfigForm, 0, 1, true)
app.SetFocus(wifiConfigForm)
}
})
// Side bar fields
sideBarCPUTempTitle := tview.NewTextView().
SetText("CPU Temperature").
SetTextColor(tcell.ColorYellow)
sideBarCPUTemp := tview.NewTextView().
SetText(fmt.Sprintf(" %s", cpuTemp))
sideBarGPUTempTitle := tview.NewTextView().
SetText("GPU Temperature").
SetTextColor(tcell.ColorYellow)
sideBarGPUTemp := tview.NewTextView().
SetText(fmt.Sprintf(" %s", gpuTemp))
sideBarMemoryTitle := tview.NewTextView().
SetText("Memory Use (Mb)").
SetTextColor(tcell.ColorYellow)
@ -249,6 +220,10 @@ func main() {
sideBar.SetTitle("System Info").
SetBorder(true).
SetTitleColor(tcell.ColorAqua)
sideBar.AddItem(sideBarCPUTempTitle, 1, 1, false)
sideBar.AddItem(sideBarCPUTemp, 1, 1, false)
sideBar.AddItem(sideBarGPUTempTitle, 1, 1, false)
sideBar.AddItem(sideBarGPUTemp, 1, 1, false)
sideBar.AddItem(sideBarMemoryTitle, 1, 1, false)
sideBar.AddItem(sideBarMemoryStats, 1, 1, false)
sideBar.AddItem(sideBarSwapTitle, 1, 1, false)
@ -262,20 +237,138 @@ func main() {
sideBar.AddItem(ipAsTextView, 1, 1, false)
}
// Pages
pages := tview.NewPages()
// Main UI
grid := tview.NewGrid().
mainUI := tview.NewGrid().
SetRows(2, 0, 4).
SetColumns(25, 50, 50).
SetBorders(true).
AddItem(header, 0, 0, 1, 3, 0, 0, false).
AddItem(footer, 2, 0, 1, 3, 0, 0, false)
grid.AddItem(menu, 1, 0, 1, 1, 0, 100, true).
mainUI.AddItem(menu, 1, 0, 1, 1, 0, 100, true).
AddItem(main, 1, 1, 1, 1, 0, 100, false).
AddItem(sideBar, 1, 2, 1, 1, 0, 100, false)
pages.AddPage(PAGE_MAIN_UI, mainUI, true, true)
// Button modals
exitModal := tview.NewModal().
SetText("Are you sure you want to [red]EXIT?").
AddButtons([]string{"Yes", "Cancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Yes" {
app.Stop()
}
pages.SwitchToPage(PAGE_MAIN_UI)
})
pages.AddPage(PAGE_EXIT, exitModal, true, false)
exitButton.SetSelectedFunc(func() {
pages.ShowPage(PAGE_EXIT)
})
rebootModal := tview.NewModal().
SetText("Are you sure you want to [red]REBOOT?").
AddButtons([]string{"Yes", "Cancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Yes" {
err := exec.Command(CMD_SYSTEMCTL, "reboot").Run()
if err != nil {
log.Fatalf("Could not reboot : %s ", err)
}
}
pages.SwitchToPage(PAGE_MAIN_UI)
})
pages.AddPage(PAGE_REBOOT, rebootModal, true, false)
rebootButton.SetSelectedFunc(func() {
pages.ShowPage(PAGE_REBOOT)
})
powerOffModal := tview.NewModal().
SetText("Are you sure you want to [red]POWER [red]OFF?").
AddButtons([]string{"Yes", "Cancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Yes" {
err := exec.Command(CMD_SYSTEMCTL, "poweroff").Run()
if err != nil {
log.Fatalf("Could not power off : %s ", err)
}
}
pages.SwitchToPage(PAGE_MAIN_UI)
})
pages.AddPage(PAGE_POWEROFF, powerOffModal, true, false)
powerOffButton.SetSelectedFunc(func() {
pages.ShowPage(PAGE_POWEROFF)
})
// Setup tracking of which are of the UI can/has focus
primitivesThatCanFocus := []tview.Primitive{menu, exitButton, rebootButton, powerOffButton}
currentFocus := 0
// Setup basic switching between main menu and buttons for the UI
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
// Don't process input if we aren't on an existing element that can focus
canContinue := false
focusedPrimitive := app.GetFocus()
// Override some of the default behavior so up/dn move between fields in forms
// Per API GetFocusedItemIndex on a form will be -1 if a form item isn't currently focused
// We use this as a bit of a cheat to figure out if we're inside of a form that needs better nav options for users (ie. tab doesn't exist on a remote)
wifiField, wifiButton := wifiConfigForm.GetFocusedItemIndex()
albumField, albumButton := selectAlbumsForm.GetFocusedItemIndex()
if wifiField != -1 || wifiButton != -1 || albumField != -1 || albumButton != -1 {
switch event.Key() {
case tcell.KeyUp:
return tcell.NewEventKey(tcell.KeyBacktab, 0, event.Modifiers())
case tcell.KeyDown:
return tcell.NewEventKey(tcell.KeyTab, 0, event.Modifiers())
}
}
// Standard override for the screen primitives we are using (ie. don't take over control in modals)
for _, primitive := range primitivesThatCanFocus {
if primitive == focusedPrimitive {
canContinue = true
break
}
}
// Bail if we shouldn't be affecting user input
if !canContinue {
return event
}
// Add various forms of nav that make sense for a TUI (common stuff folks are used to)
currentFocusChanged := false
switch event.Key() {
case tcell.KeyRight, tcell.KeyTab:
currentFocus += 1
if currentFocus >= len(primitivesThatCanFocus) {
currentFocus = 0
}
currentFocusChanged = true
case tcell.KeyLeft, tcell.KeyBacktab:
currentFocus -= 1
if currentFocus < 0 {
currentFocus = len(primitivesThatCanFocus) - 1
}
currentFocusChanged = true
}
// Update the focus based on navigation
if currentFocusChanged {
app.SetFocus(primitivesThatCanFocus[currentFocus])
return nil
}
// Pass through event so the main UI can process stuff properly
return event
})
// Show UI and panic if there are any errors
if err := app.SetRoot(grid, true).EnableMouse(false).Run(); err != nil {
panic(err)
if err := app.SetRoot(pages, true).SetFocus(primitivesThatCanFocus[currentFocus]).EnableMouse(false).Run(); err != nil {
log.Fatalf("Failed to run UI : ", err)
}
}