diff --git a/cmd/ui/README.md b/cmd/gui/README.md similarity index 96% rename from cmd/ui/README.md rename to cmd/gui/README.md index 77303a1..1c51795 100644 --- a/cmd/ui/README.md +++ b/cmd/gui/README.md @@ -1,3 +1,3 @@ -# ui +# gui This is the main source code for the custom UI used by PiFrame. This is responsible for things like restart, WiFi config, ensuring the slide show is running and more. diff --git a/cmd/gui/gui.go b/cmd/gui/gui.go new file mode 100644 index 0000000..c73fc42 --- /dev/null +++ b/cmd/gui/gui.go @@ -0,0 +1,10 @@ +package main + +import ( + "git.kemonine.info/PiFrame/ui" +) + +func main() { + ui.Slideshow() + ui.ConfigGui() +} diff --git a/ui/README.md b/ui/README.md new file mode 100644 index 0000000..0882e3c --- /dev/null +++ b/ui/README.md @@ -0,0 +1,3 @@ +# ui + +This is the main implementation for various aspects of the main PiFrame [G]UI diff --git a/cmd/ui/ui.go b/ui/config.go similarity index 96% rename from cmd/ui/ui.go rename to ui/config.go index cf836c1..a9b8d53 100644 --- a/cmd/ui/ui.go +++ b/ui/config.go @@ -1,422 +1,422 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "math" - "net" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - - "github.com/gdamore/tcell" - "github.com/guillermo/go.procmeminfo" - "github.com/rivo/tview" - - "git.kemonine.info/PiFrame/wifi" -) - -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() { - // 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 - 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() - - // 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) - 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) - - // Setup main panel (Center column) - 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) - } - }) - - // 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) - 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(pages, true).SetFocus(primitivesThatCanFocus[currentFocus]).EnableMouse(false).Run(); err != nil { - log.Fatalf("Failed to run UI : ", err) - } -} +package ui + +import ( + "fmt" + "io/ioutil" + "log" + "math" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/gdamore/tcell" + "github.com/guillermo/go.procmeminfo" + "github.com/rivo/tview" + + "git.kemonine.info/PiFrame/wifi" +) + +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 ConfigGui() { + // 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 + 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() + + // 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) + 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) + + // Setup main panel (Center column) + 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) + } + }) + + // 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) + 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(pages, true).SetFocus(primitivesThatCanFocus[currentFocus]).EnableMouse(false).Run(); err != nil { + log.Fatalf("Failed to run UI : ", err) + } +} diff --git a/cmd/ui/slideshow.go b/ui/slideshow.go similarity index 87% rename from cmd/ui/slideshow.go rename to ui/slideshow.go index bf52a25..0bdd4e6 100644 --- a/cmd/ui/slideshow.go +++ b/ui/slideshow.go @@ -100,13 +100,13 @@ func Slideshow() { }(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) - // } - // } + 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)