piframe-go/cmd/ui/ui.go

423 lines
13 KiB
Go
Raw Normal View History

2020-08-25 22:55:05 +00:00
package main
import (
2020-08-30 23:46:17 +00:00
"fmt"
"io/ioutil"
2020-08-28 22:33:03 +00:00
"log"
2020-08-30 23:46:17 +00:00
"math"
"net"
"os"
2020-08-28 22:33:03 +00:00
"os/exec"
"path/filepath"
"strconv"
2020-08-30 23:46:17 +00:00
"strings"
2020-08-28 22:33:03 +00:00
"github.com/gdamore/tcell"
2020-08-30 23:46:17 +00:00
"github.com/guillermo/go.procmeminfo"
2020-08-28 22:33:03 +00:00
"github.com/rivo/tview"
"git.kemonine.info/PiFrame/wifi"
2020-08-25 22:55:05 +00:00
)
const (
CMD_SYSTEMCTL = "/usr/bin/systemctl"
CMD_FINDMNT = "/usr/bin/findmnt"
CMD_VCGENCMD = "/opt/vc/bin/vcgencmd"
FILE_CPU_TEMP = "/sys/class/thermal/thermal_zone0/temp"
ALBUM_ROOT_DIR = "/tank/pictures/"
SYNCTHING_FOLDER_SKIP = ".stfolder"
)
const (
PAGE_MAIN_UI = "PAGE_MAIN_UI"
PAGE_EXIT = "PAGE_EXIT"
PAGE_REBOOT = "PAGE_REBOOT"
PAGE_POWEROFF = "PAGE_POWEROFF"
)
func main() {
2020-08-30 23:46:17 +00:00
// Memory info for status panel
meminfo := &procmeminfo.MemInfo{}
err := meminfo.Update()
2020-08-30 23:46:17 +00:00
if err != nil {
log.Printf("Error getting memory info : %s", err)
}
// Network interfaces for status panel
ifaces, err := net.Interfaces()
if err != nil {
log.Fatalf("Error getting netork interfaces : %s", err)
2020-08-30 23:46:17 +00:00
return
}
// Disk use
findmntOut, err := exec.Command(CMD_FINDMNT, "-n", "-l",
2020-08-30 23:46:17 +00:00
"-o", "TARGET,USE%",
"-t", "ext4,exfat,vfat,btrfs,zfs,xfs").Output()
if err != nil {
log.Fatalf("Error getting disk use : %s", err)
}
2020-08-30 23:46:17 +00:00
filesystems := strings.Split(strings.Trim(string(findmntOut), "\n"), "\n")
// GPU Temp
vcgencmdOut, err := exec.Command(CMD_VCGENCMD, "measure_temp").Output()
if err != nil {
log.Fatalf("Error getting GPU temp : %s", err)
}
gpuTemp := strings.Split(strings.Trim(string(vcgencmdOut), "\n"), "=")[1]
// CPU Temp
cpuTempFileContents, err := ioutil.ReadFile(FILE_CPU_TEMP)
if err != nil {
log.Fatalf("Error getting CPU temp : %s", err)
}
cpuTempStr := strings.Trim(string(cpuTempFileContents), "\n")
cpuTempInt, err := strconv.Atoi(cpuTempStr)
if err != nil {
log.Fatalf("Error processing CPU temp : %S", err)
}
cpuTemp := fmt.Sprintf("%.2f'C", float64(cpuTempInt)/1000.0)
// Get list of all folders that can be used as albums
var albums []string
err = filepath.Walk(ALBUM_ROOT_DIR, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
if strings.Contains(path, SYNCTHING_FOLDER_SKIP) {
return nil
}
albumName := strings.TrimPrefix(path, ALBUM_ROOT_DIR)
if albumName == "" {
albumName = "Main Folder"
}
albums = append(albums, albumName)
}
return nil
})
if err != nil {
log.Fatalf("Error getting list of albums : %s", err)
}
// Run config UI when slideshow stops
app := tview.NewApplication()
2020-08-30 23:46:17 +00:00
// Header
headerTitle := tview.NewTextView().
SetText("PiFrame").
SetTextAlign(tview.AlignCenter).
SetTextColor(tcell.ColorAqua)
headerSubTitle := tview.NewTextView().
SetText("Management Utility").
SetTextAlign(tview.AlignCenter).
SetTextColor(tcell.ColorSilver)
header := tview.NewFlex().
SetDirection(tview.FlexRow)
header.AddItem(headerTitle, 0, 1, false).
AddItem(headerSubTitle, 0, 1, false)
// Footer fields (Left Column)
exitButton := tview.NewButton("Exit").
SetBackgroundColorActivated(tcell.ColorGray)
2020-08-30 23:46:17 +00:00
exitButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
SetBackgroundColor(tcell.ColorGreen).
SetRect(0, 0, 22, 3)
rebootButton := tview.NewButton("Reboot").
SetBackgroundColorActivated(tcell.ColorGray)
2020-08-30 23:46:17 +00:00
rebootButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
SetBackgroundColor(tcell.ColorYellow).
SetRect(0, 0, 22, 3)
powerOffButton := tview.NewButton("Power Off").
SetBackgroundColorActivated(tcell.ColorGray)
2020-08-30 23:46:17 +00:00
powerOffButton.SetLabelColor(tcell.ColorBlack).
SetBorder(true).
SetBorderColor(tcell.ColorBlack).
SetBackgroundColor(tcell.ColorRed).
SetRect(0, 0, 22, 3)
// Footer
footer := tview.NewFlex()
footer.AddItem(exitButton, 0, 1, false).
AddItem(rebootButton, 0, 1, false).
AddItem(powerOffButton, 0, 1, false)
// Setup menu
menu := tview.NewList()
menu.SetBorder(true).
SetTitle("Menu").
2020-08-30 23:46:17 +00:00
SetTitleColor(tcell.ColorAqua)
menu.AddItem("Select Albums", "", '1', nil)
menu.AddItem("Configure WiFi", "", '2', nil)
2020-08-30 23:46:17 +00:00
// Setup base var for main column so the menu setup is easier to manage
2020-08-30 23:46:17 +00:00
main := tview.NewFlex().
SetDirection(tview.FlexRow)
// Setup main panel (Center column)
2020-08-30 23:46:17 +00:00
main.SetTitle("").
SetBorder(true).
SetTitleColor(tcell.ColorAqua)
// WiFi Config Form
wifiConfigForm := tview.NewForm()
wifiConfigAccessPoint := ""
wifiConfigPassword := ""
wifiConfigForm.AddInputField("Access Point", "", 0, nil, func(value string) {
wifiConfigAccessPoint = value
})
wifiConfigForm.AddPasswordField("Password", "", 0, '*', func(value string) {
wifiConfigPassword = value
})
wifiConfigForm.AddButton("Apply", func() {
// Cleanup old wifi configs and apply new one
nmWifi := wifi.New(wifiConfigAccessPoint, wifiConfigPassword)
nmWifi.ApplyConfig()
})
wifiConfigForm.AddButton("Cancel", func() {
main.Clear()
app.SetFocus(menu)
})
// Select Albums Form
selectAlbumsForm := tview.NewForm()
for _, album := range albums {
selectAlbumsForm.AddCheckbox(album, true, 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)
}
})
2020-08-30 23:46:17 +00:00
// 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))
2020-08-30 23:46:17 +00:00
sideBarMemoryTitle := tview.NewTextView().
SetText("Memory Use (Mb)").
SetTextColor(tcell.ColorYellow)
divisor := math.Pow(1024.0, 2.0)
sideBarMemoryStats := tview.NewTextView().
SetText(fmt.Sprintf(" %.1f / %.1f",
float64(meminfo.Used())/divisor,
float64(meminfo.Total())/divisor))
sideBarSwapTitle := tview.NewTextView().
SetText("Swap Use").
SetTextColor(tcell.ColorYellow)
sideBarSwapStats := tview.NewTextView().
SetText(fmt.Sprintf(" %d%%", meminfo.Swap()))
sideBarFilesystemTitle := tview.NewTextView().
SetText("Disk Use").
SetTextColor(tcell.ColorYellow)
var sideBarFilesystems []*tview.TextView
for _, i := range filesystems {
filesystemAsTextView := tview.NewTextView().
SetText(fmt.Sprintf(" %s", i))
sideBarFilesystems = append(sideBarFilesystems, filesystemAsTextView)
}
sideBarIPAddressesTitle := tview.NewTextView().
SetText("IP Addresses").
SetTextColor(tcell.ColorYellow)
var sideBarIPAddresses []*tview.TextView
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
log.Printf("Error getting interface addresses : %s", err)
continue
}
for _, a := range addrs {
ipAsTextView := tview.NewTextView().
SetText(fmt.Sprintf(" %v : %s", i.Name, a.String()))
sideBarIPAddresses = append(sideBarIPAddresses, ipAsTextView)
}
}
// Setup side bar (Right column)
sideBar := tview.NewFlex().
SetDirection(tview.FlexRow)
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)
2020-08-30 23:46:17 +00:00
sideBar.AddItem(sideBarMemoryTitle, 1, 1, false)
sideBar.AddItem(sideBarMemoryStats, 1, 1, false)
sideBar.AddItem(sideBarSwapTitle, 1, 1, false)
sideBar.AddItem(sideBarSwapStats, 1, 1, false)
sideBar.AddItem(sideBarFilesystemTitle, 1, 1, false)
for _, filesystemAsTextView := range sideBarFilesystems {
sideBar.AddItem(filesystemAsTextView, 1, 1, false)
}
sideBar.AddItem(sideBarIPAddressesTitle, 1, 1, false)
for _, ipAsTextView := range sideBarIPAddresses {
sideBar.AddItem(ipAsTextView, 1, 1, false)
}
// Pages
pages := tview.NewPages()
2020-08-30 23:46:17 +00:00
// Main UI
mainUI := tview.NewGrid().
2020-08-30 23:46:17 +00:00
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)
mainUI.AddItem(menu, 1, 0, 1, 1, 0, 100, true).
2020-08-30 23:46:17 +00:00
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
})
2020-08-30 23:46:17 +00:00
// Show UI and panic if there are any errors
if err := app.SetRoot(pages, true).SetFocus(primitivesThatCanFocus[currentFocus]).EnableMouse(false).Run(); err != nil {
log.Fatalf("Failed to run UI : ", err)
}
2020-08-25 22:55:05 +00:00
}