package ui import ( "context" "io" "io/ioutil" "log" "os" "os/exec" "syscall" "time" "github.com/eiannone/keyboard" "github.com/knadh/koanf" "git.kemonine.info/PiFrame/config" ) const ( PATH_TEMP_FOR_SLIDESHOW = "/run/piframe/fim" ) // 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\"", "--autozoom", "--random", "--recursive", "--cd-and-readdir", 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 } 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) } // Loop over the directory's files. for index := range dirFiles { fileHere := dirFiles[index] // Get name of file and its full path. nameHere := fileHere.Name() fullPath := PATH_TEMP_FOR_SLIDESHOW + "/" + nameHere // Remove the file. err = os.Remove(fullPath) if err != nil { log.Fatalf("Error setting up slideshow : %s", err) } } // 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() { err = os.Symlink(filePath, PATH_TEMP_FOR_SLIDESHOW+"/"+file.Name()) 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) } } // 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) // Advance slideshow every interval as defined in const() 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) 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 // 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 // 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 { // We are being told to stop the slideshow, oblidge the user STOP_SLIDESHOW = true 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) } } } 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) } ticker.Reset(pfconfig.Duration(config.CONFIG_KEY_SLIDESHOW_INTERVAL)) } } } }(keyboardCtx) // 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) 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) 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 } } }() // Run fim 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) } } setupFim(PATH_TEMP_FOR_SLIDESHOW) } // Stop fim slideshow advancing go routine close(stop_ticker) // Stop restart go routine close(stop_fim_ticker) // Stop listening to keyboard events keyboard.Close() keyboardCancel() }