2020-09-01 02:57:23 +00:00
package ui
import (
"context"
"io"
2020-09-05 06:08:57 +00:00
"io/ioutil"
2020-09-01 02:57:23 +00:00
"log"
2020-09-05 06:08:57 +00:00
"os"
2020-09-01 02:57:23 +00:00
"os/exec"
2020-09-03 05:04:08 +00:00
"syscall"
2020-09-01 02:57:23 +00:00
"time"
"github.com/eiannone/keyboard"
2020-09-04 01:01:13 +00:00
"github.com/knadh/koanf"
2020-09-05 03:28:01 +00:00
2020-09-05 06:08:57 +00:00
"git.kemonine.info/PiFrame/config"
2020-09-01 02:57:23 +00:00
)
const (
2020-09-05 06:08:57 +00:00
PATH_TEMP_FOR_SLIDESHOW = "/run/piframe/fim"
2020-09-01 02:57:23 +00:00
)
2020-09-05 21:49:55 +00:00
// fim placeholders so we can reset them as needed
var fim * exec . Cmd = nil
var stdin io . WriteCloser = nil
func setupFim ( PATH_TEMP_FOR_SLIDESHOW string ) {
// Prep slideshow command and arguments
// NOTE: The random flag is seeded with time() ; this is bad as we will be restarting the slideshow at about the same time per the configurd schedule
// We use the non-seeded form to ensure that it's a little more random (or at least hope it's a little more random)
CMD_FIM := "/usr/bin/fim"
ARGS_FIM := [ ] string { "--no-commandline" , "--no-history" , "--etc-fimrc" , "/usr/local/etc/fimrc" ,
"--device" , "/dev/fb0" , "--vt" , "1" ,
"--execute-commands-early" , "\"clear\"" , "--final-commands" , "\"clear\"" ,
2020-09-05 21:50:33 +00:00
"--autozoom" , "--random" , "--recursive" , "--cd-and-readdir" ,
2020-09-05 21:49:55 +00:00
PATH_TEMP_FOR_SLIDESHOW }
// fim command that'll be executed
fim = exec . Command ( CMD_FIM , ARGS_FIM ... )
// Put fim into a process group so ALL processes that may be executed are exited when main process exits
fim . SysProcAttr = & syscall . SysProcAttr { Setpgid : true }
// Setup stdin for fim to control slideshow
stdinLocal , err := fim . StdinPipe ( )
if err != nil {
log . Fatalf ( "Error getting fim stdin : %s" , err )
}
stdin = stdinLocal
}
2020-09-05 06:08:57 +00:00
func Slideshow ( pfconfig * koanf . Koanf ) {
// Prep folder setup needed for fim
// fim does NOT allow multiple folders passed on the CLI (as far as KemoNine can tell)
// We build a temp folder stup in /run/piframe/fim that has symlinks to selected albums
// After we build up this directory setup we'll kick off fim against the temp dir with recursive
// /run is a tmpfs so this won't wear on the sd card storage
// Create temp folder
_ , err := os . Stat ( PATH_TEMP_FOR_SLIDESHOW )
if os . IsNotExist ( err ) {
errDir := os . MkdirAll ( PATH_TEMP_FOR_SLIDESHOW , 0755 )
if errDir != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
}
// Cleanup temp folder if it already existed
dirRead , err := os . Open ( PATH_TEMP_FOR_SLIDESHOW )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
dirFiles , err := dirRead . Readdir ( 0 )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
2020-09-05 21:49:55 +00:00
// Loop over the directory's files.
for index := range dirFiles {
fileHere := dirFiles [ index ]
2020-09-05 06:08:57 +00:00
2020-09-05 21:49:55 +00:00
// Get name of file and its full path.
nameHere := fileHere . Name ( )
fullPath := PATH_TEMP_FOR_SLIDESHOW + "/" + nameHere
2020-09-05 06:08:57 +00:00
2020-09-05 21:49:55 +00:00
// Remove the file.
2020-09-05 06:08:57 +00:00
err = os . Remove ( fullPath )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
}
2020-09-05 21:49:55 +00:00
2020-09-05 06:08:57 +00:00
// Setup symlinks to selected albums to be used with slideshow
// Add albums full paths to command line args for fim
albumRootPath := pfconfig . String ( config . CONFIG_KEY_ALBUMS_ROOT )
for _ , album := range pfconfig . Strings ( config . CONFIG_KEY_ALBUMS_SELECTED ) {
source := albumRootPath + album
destination := PATH_TEMP_FOR_SLIDESHOW + album
if album == "/" {
files , err := ioutil . ReadDir ( albumRootPath )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
for _ , file := range files {
filePath := albumRootPath + "/" + file . Name ( )
fileStat , err := os . Stat ( filePath )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
mode := fileStat . Mode ( )
if mode . IsRegular ( ) {
2020-09-05 21:49:55 +00:00
err = os . Symlink ( filePath , PATH_TEMP_FOR_SLIDESHOW + "/" + file . Name ( ) )
2020-09-05 06:08:57 +00:00
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
}
}
continue
}
err = os . Symlink ( source , destination )
if err != nil {
log . Fatalf ( "Error setting up slideshow : %s" , err )
}
}
2020-09-05 21:49:55 +00:00
// Setup fim for run
// Commands can't be re-used so we move this to a function to support the restart interval cleanly
setupFim ( PATH_TEMP_FOR_SLIDESHOW )
2020-09-01 02:57:23 +00:00
// Advance slideshow every interval as defined in const()
2020-09-06 01:27:33 +00:00
slideshowAdvanceDurationString := pfconfig . String ( config . CONFIG_KEY_SLIDESHOW_INTERVAL )
slideshowAdvanceDuration , err := time . ParseDuration ( slideshowAdvanceDurationString )
if err != nil {
log . Fatalf ( "Error parsing slide duration : %s" , err )
}
ticker := time . NewTicker ( slideshowAdvanceDuration )
2020-09-01 02:57:23 +00:00
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
2020-09-05 21:49:55 +00:00
// Keep running fim until we're signaled by the user to do otherwise
// Common reasons that we MUST do this kind of goofy (bugs / behavior)
// - fim doesn't quite see the /run changes where the albums are linked and bails out
// - OOM kicks in and kills fim (it's kinda RAM heavy sometimes)
// - Corrupt or invalid image causes fim to crash
// - The timeout for re-randomizing kicked free and fim needs to restart
// - fim thinks the slideshow has 'ended' for whatever reason and we really want to always show no matter what
STOP_SLIDESHOW := false
2020-09-01 02:57:23 +00:00
// 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 )
}
// 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 {
2020-09-05 21:49:55 +00:00
// We are being told to stop the slideshow, oblidge the user
STOP_SLIDESHOW = true
2020-09-01 02:57:23 +00:00
if fim != nil { // Just in case someone lays on exit key or similar during startup
2020-09-03 05:04:08 +00:00
pgid , err := syscall . Getpgid ( fim . Process . Pid )
if err == nil {
if err := syscall . Kill ( - pgid , 9 ) ; err != nil {
log . Fatalf ( "failed to kill fim : %s" , err )
}
2020-09-01 02:57:23 +00:00
}
}
break
}
// Control fim if we received a valid key for next/previous slide
if fimKey != "" {
_ , err = io . WriteString ( stdin , fimKey )
if err != nil {
log . Fatalf ( "Error controlling fim : %s" , err )
}
2020-09-05 06:08:57 +00:00
ticker . Reset ( pfconfig . Duration ( config . CONFIG_KEY_SLIDESHOW_INTERVAL ) )
2020-09-01 02:57:23 +00:00
}
}
}
} ( keyboardCtx )
2020-09-05 21:49:55 +00:00
// Restart fim after configured timeout ; This is setup as a ticker due to KemoNine not getting CommandWithContext stuff to work properly (lots of pointer related crashes and the like)
2020-09-06 01:27:33 +00:00
fimRestartDurationString := pfconfig . String ( config . CONFIG_KEY_SLIDESHOW_RESTART_INTERVAL )
fimRestartDuration , err := time . ParseDuration ( fimRestartDurationString )
if err != nil {
log . Fatalf ( "Error parsing restart duration : %S" , err )
}
fimTicker := time . NewTicker ( fimRestartDuration )
2020-09-05 21:49:55 +00:00
stop_fim_ticker := make ( chan struct { } )
go func ( ) {
for {
select {
case <- fimTicker . C :
if fim != nil { // Just in case someone lays on exit key or similar during startup
pgid , err := syscall . Getpgid ( fim . Process . Pid )
if err == nil {
if err := syscall . Kill ( - pgid , 9 ) ; err != nil {
log . Fatalf ( "failed to kill fim : %s" , err )
}
}
}
case <- stop_fim_ticker :
fimTicker . Stop ( )
return
}
}
} ( )
2020-09-01 02:57:23 +00:00
// Run fim
2020-09-05 21:49:55 +00:00
for ! STOP_SLIDESHOW {
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 {
// We are NOT going to fatal here as there are many 'valid' reasons fim would exit for no good reason ;)
log . Printf ( "Error running fim : %s" , err )
}
2020-09-03 00:40:28 +00:00
}
2020-09-05 21:49:55 +00:00
setupFim ( PATH_TEMP_FOR_SLIDESHOW )
2020-09-03 00:40:28 +00:00
}
2020-09-01 02:57:23 +00:00
// Stop fim slideshow advancing go routine
close ( stop_ticker )
2020-09-05 21:49:55 +00:00
// Stop restart go routine
close ( stop_fim_ticker )
2020-09-01 02:57:23 +00:00
// Stop listening to keyboard events
keyboard . Close ( )
keyboardCancel ( )
}