2020-09-03 00:40:28 +00:00
package ui
import (
"fmt"
"log"
"math"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/gdamore/tcell"
"github.com/guillermo/go.procmeminfo"
2020-09-04 01:01:13 +00:00
"github.com/knadh/koanf"
2020-09-03 00:40:28 +00:00
"github.com/rivo/tview"
2020-09-04 18:34:35 +00:00
"git.kemonine.info/PiFrame/utils"
2020-09-03 00:40:28 +00:00
"git.kemonine.info/PiFrame/wifi"
)
const (
CMD_SYSTEMCTL = "/usr/bin/systemctl"
CMD_FINDMNT = "/usr/bin/findmnt"
SYNCTHING_FOLDER_SKIP = ".stfolder"
)
const (
PAGE_MAIN_UI = "PAGE_MAIN_UI"
PAGE_EXIT = "PAGE_EXIT"
PAGE_REBOOT = "PAGE_REBOOT"
PAGE_POWEROFF = "PAGE_POWEROFF"
)
2020-09-04 01:01:13 +00:00
func ConfigGui ( config * koanf . Koanf ) {
2020-09-03 00:40:28 +00:00
// Memory info for status panel
meminfo := & procmeminfo . MemInfo { }
err := meminfo . Update ( )
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 )
return
}
// Disk use
findmntOut , err := exec . Command ( CMD_FINDMNT , "-n" , "-l" ,
"-o" , "TARGET,USE%" ,
"-t" , "ext4,exfat,vfat,btrfs,zfs,xfs" ) . Output ( )
if err != nil {
log . Fatalf ( "Error getting disk use : %s" , err )
}
filesystems := strings . Split ( strings . Trim ( string ( findmntOut ) , "\n" ) , "\n" )
// GPU Temp
2020-09-04 18:34:35 +00:00
gpuTemp := fmt . Sprintf ( "%.2f'C" , utils . GetGPUTemp ( ) )
2020-09-03 00:40:28 +00:00
// CPU Temp
2020-09-04 18:34:35 +00:00
cpuTemp := fmt . Sprintf ( "%.2f'C" , utils . GetCPUTemp ( ) )
2020-09-03 00:40:28 +00:00
// Get list of all folders that can be used as albums
var albums [ ] string
2020-09-04 01:01:13 +00:00
err = filepath . Walk ( config . String ( CONFIG_KEY_ALBUMS_ROOT ) , func ( path string , fi os . FileInfo , err error ) error {
2020-09-03 00:40:28 +00:00
if err != nil {
return err
}
if fi . IsDir ( ) {
if strings . Contains ( path , SYNCTHING_FOLDER_SKIP ) {
return nil
}
2020-09-04 01:01:13 +00:00
albumName := strings . TrimPrefix ( path , config . String ( CONFIG_KEY_ALBUMS_ROOT ) )
2020-09-03 00:40:28 +00:00
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 ( )
// 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 )
exitButton . SetLabelColor ( tcell . ColorBlack ) .
SetBorder ( true ) .
SetBorderColor ( tcell . ColorBlack ) .
SetBackgroundColor ( tcell . ColorGreen ) .
SetRect ( 0 , 0 , 22 , 3 )
rebootButton := tview . NewButton ( "Reboot" ) .
SetBackgroundColorActivated ( tcell . ColorGray )
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 )
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" ) .
SetTitleColor ( tcell . ColorAqua )
menu . AddItem ( "Select Albums" , "" , '1' , nil )
2020-09-04 01:01:13 +00:00
menu . AddItem ( "Intervals" , "" , '2' , nil )
menu . AddItem ( "WiFi" , "" , '3' , nil )
menu . AddItem ( "HDMI On/Off" , "" , '5' , nil )
2020-09-03 00:40:28 +00:00
// Setup base var for main column so the menu setup is easier to manage
main := tview . NewFlex ( ) .
SetDirection ( tview . FlexRow )
// Setup main panel (Center column)
main . SetTitle ( "" ) .
SetBorder ( true ) .
SetTitleColor ( tcell . ColorAqua )
2020-09-04 01:01:13 +00:00
// Select Albums Form
selectAlbumsForm := tview . NewForm ( )
configSelectedAlbums := config . Strings ( CONFIG_KEY_ALBUMS_SELECTED )
for _ , album := range albums {
albumSelected := false
for _ , configSelectedAlbum := range configSelectedAlbums {
if configSelectedAlbum == "/" {
configSelectedAlbum = "Main Folder"
}
if album == configSelectedAlbum {
albumSelected = true
}
}
selectAlbumsForm . AddCheckbox ( album , albumSelected , nil )
}
selectAlbumsForm . AddButton ( "Apply" , nil )
selectAlbumsForm . AddButton ( "Cancel" , func ( ) {
main . Clear ( )
app . SetFocus ( menu )
} )
// Slide Interval Form
intervalsForm := tview . NewForm ( )
configSlideInterval := config . String ( CONFIG_KEY_SLIDESHOW_INTERVAL )
configRestartInterval := config . String ( CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL )
2020-09-04 18:34:35 +00:00
intervalsForm . AddInputField ( "Slide" , configSlideInterval , 0 , nil , func ( value string ) {
configSlideInterval = value
} )
intervalsForm . AddInputField ( "Restart/Reshuffle" , configRestartInterval , 0 , nil , func ( value string ) {
configRestartInterval = value
} )
2020-09-04 01:01:13 +00:00
intervalsForm . AddButton ( "Apply" , nil )
intervalsForm . AddButton ( "Cancel" , func ( ) {
main . Clear ( )
app . SetFocus ( menu )
} )
2020-09-03 00:40:28 +00:00
// 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 )
} )
2020-09-04 01:01:13 +00:00
// HDMI On/Off Form
hdmiForm := tview . NewForm ( )
configHDMIOff := config . String ( CONFIG_KEY_HDMI_OFF )
configHDMIOn := config . String ( CONFIG_KEY_HDMI_ON )
2020-09-04 18:34:35 +00:00
hdmiForm . AddInputField ( "HDMI Off Schedule" , configHDMIOff , 0 , nil , func ( value string ) {
configHDMIOff = value
} )
hdmiForm . AddInputField ( "HDMI On Schedule" , configHDMIOn , 0 , nil , func ( value string ) {
configHDMIOn = value
} )
2020-09-04 01:01:13 +00:00
hdmiForm . AddButton ( "Apply" , nil )
hdmiForm . AddButton ( "Cancel" , func ( ) {
2020-09-03 00:40:28 +00:00
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 )
}
2020-09-04 01:01:13 +00:00
if title == "WiFi" {
2020-09-03 00:40:28 +00:00
main . SetTitle ( "Configure WiFi" )
main . Clear ( )
main . AddItem ( wifiConfigForm , 0 , 1 , true )
app . SetFocus ( wifiConfigForm )
}
2020-09-04 01:01:13 +00:00
if title == "Intervals" {
main . SetTitle ( "Configure Intervals" )
main . Clear ( )
main . AddItem ( intervalsForm , 0 , 1 , true )
2020-09-04 22:50:57 +00:00
main . AddItem ( tview . NewTextView ( ) . SetText ( "Intervals are a number + letter\n\nUse\ns for seconds\nm for minutes\nh for hours\nd for days\nw for weeks" ) , 0 , 1 , false )
2020-09-04 01:01:13 +00:00
app . SetFocus ( intervalsForm )
}
if title == "HDMI On/Off" {
main . SetTitle ( "Configure HDMI On/Off" )
main . Clear ( )
main . AddItem ( hdmiForm , 0 , 1 , true )
2020-09-04 22:47:50 +00:00
main . AddItem ( tview . NewTextView ( ) . SetText ( "These values are date+time combos\n\nPlease see https://www.freedesktop.org/software/systemd/man/systemd.time.html for details\n\nONLY adjust the times (24h format) if unsure of what to change" ) , 0 , 1 , false )
2020-09-04 01:01:13 +00:00
app . SetFocus ( hdmiForm )
}
2020-09-03 00:40:28 +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 ) )
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 )
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 ( )
// Main UI
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 )
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)
albumField , albumButton := selectAlbumsForm . GetFocusedItemIndex ( )
2020-09-04 01:01:13 +00:00
intervalField , intervalButton := intervalsForm . GetFocusedItemIndex ( )
wifiField , wifiButton := wifiConfigForm . GetFocusedItemIndex ( )
hdmiField , hdmiButton := hdmiForm . GetFocusedItemIndex ( )
2020-09-04 18:34:35 +00:00
if wifiField != - 1 || wifiButton != - 1 ||
2020-09-04 01:01:13 +00:00
albumField != - 1 || albumButton != - 1 ||
intervalField != - 1 || intervalButton != - 1 ||
2020-09-04 18:34:35 +00:00
hdmiField != - 1 || hdmiButton != - 1 {
2020-09-03 00:40:28 +00:00
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 ( pages , true ) . SetFocus ( primitivesThatCanFocus [ currentFocus ] ) . EnableMouse ( false ) . Run ( ) ; err != nil {
log . Fatalf ( "Failed to run UI : " , err )
}
}