From 5ff9da79e207cb1e3472038c31381ca81102afb6 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Thu, 12 Dec 2024 10:50:21 +0800 Subject: [PATCH] MiSTer video support improvements (#120) * Initial video support refactor * Improve path matching functions * Add video metadata * Fix indexing zips bug * Improvements to mplayer launcher --- pkg/assets/systems/Video.json | 7 ++ pkg/database/gamesdb/gamesdb.go | 2 +- pkg/database/gamesdb/indexing.go | 8 +- pkg/database/gamesdb/systems.go | 4 + pkg/launcher/commands.go | 23 ------ pkg/platforms/mister/config.go | 90 +-------------------- pkg/platforms/mister/launchers.go | 126 ++++++++++++++++++++++++++++++ pkg/platforms/mister/methods.go | 19 ----- pkg/platforms/mister/platform.go | 23 +++++- pkg/platforms/mister/scripts.go | 48 +++++++++++- pkg/platforms/platforms.go | 39 +++------ pkg/service/readers.go | 42 +--------- pkg/service/tokens/tokens.go | 20 ++--- pkg/utils/paths.go | 95 ++++++++++++++++++++++ 14 files changed, 329 insertions(+), 217 deletions(-) create mode 100644 pkg/assets/systems/Video.json create mode 100644 pkg/utils/paths.go diff --git a/pkg/assets/systems/Video.json b/pkg/assets/systems/Video.json new file mode 100644 index 00000000..0e2bbc79 --- /dev/null +++ b/pkg/assets/systems/Video.json @@ -0,0 +1,7 @@ +{ + "id": "Video", + "name": "Video", + "category": "Other", + "releaseDate": "1888-01-01", + "manufacturer": "N/A" +} diff --git a/pkg/database/gamesdb/gamesdb.go b/pkg/database/gamesdb/gamesdb.go index b08e8b43..e976feef 100644 --- a/pkg/database/gamesdb/gamesdb.go +++ b/pkg/database/gamesdb/gamesdb.go @@ -240,7 +240,7 @@ func NewNamesIndex( update(status) for _, path := range systemPaths[k] { - pathFiles, err := GetFiles(platform, k, path) + pathFiles, err := GetFiles(cfg, platform, k, path) if err != nil { return status.Files, fmt.Errorf("error getting files: %s", err) } diff --git a/pkg/database/gamesdb/indexing.go b/pkg/database/gamesdb/indexing.go index 8a9fe11b..4d9163c9 100644 --- a/pkg/database/gamesdb/indexing.go +++ b/pkg/database/gamesdb/indexing.go @@ -2,6 +2,7 @@ package gamesdb import ( "fmt" + "github.com/wizzomafizzo/tapto/pkg/config" "io/fs" "os" "path/filepath" @@ -108,6 +109,7 @@ func (r *resultsStack) get() (*[]string, error) { // files. This function deep searches .zip files and handles symlinks at all // levels. func GetFiles( + cfg *config.UserConfig, platform platforms.Platform, systemId string, path string, @@ -195,14 +197,14 @@ func GetFiles( } for i := range zipFiles { - if platforms.MatchSystemFile(platform, (*system).Id, zipFiles[i]) { - abs := filepath.Join(path, zipFiles[i]) + abs := filepath.Join(path, zipFiles[i]) + if utils.MatchSystemFile(cfg, platform, (*system).Id, abs) { *results = append(*results, abs) } } } else { // regular files - if platforms.MatchSystemFile(platform, (*system).Id, path) { + if utils.MatchSystemFile(cfg, platform, (*system).Id, path) { *results = append(*results, path) } } diff --git a/pkg/database/gamesdb/systems.go b/pkg/database/gamesdb/systems.go index d10c960d..73aff4c3 100644 --- a/pkg/database/gamesdb/systems.go +++ b/pkg/database/gamesdb/systems.go @@ -164,6 +164,7 @@ const ( SystemArcade = "Arcade" SystemArduboy = "Arduboy" SystemChip8 = "Chip8" + SystemVideo = "Video" ) var Systems = map[string]System{ @@ -511,4 +512,7 @@ var Systems = map[string]System{ SystemChip8: { Id: SystemChip8, }, + SystemVideo: { + Id: SystemVideo, + }, } diff --git a/pkg/launcher/commands.go b/pkg/launcher/commands.go index 0a37e817..88b0f8d6 100644 --- a/pkg/launcher/commands.go +++ b/pkg/launcher/commands.go @@ -26,7 +26,6 @@ import ( "github.com/wizzomafizzo/tapto/pkg/service/tokens" "net/url" "os" - "os/exec" "path/filepath" "strings" @@ -58,7 +57,6 @@ var commandMappings = map[string]func(platforms.Platform, platforms.CmdEnv) erro "mister.core": forwardCmd, "mister.script": forwardCmd, "mister.mgl": forwardCmd, - "mister.video": cmdMisterVideo, "http.get": cmdHttpGet, "http.post": cmdHttpPost, @@ -220,24 +218,3 @@ func LaunchToken( CurrentIndex: currentIndex, }), true } - -func cmdMisterVideo(pl platforms.Platform, env platforms.CmdEnv) error { - if len(env.Args) == 0 { - return fmt.Errorf("URL argument missing for mister.video command") - } - - videoURL := env.Args - log.Info().Msgf("Playing video from URL: %s", videoURL) - - cmd := exec.Command("tmp/tapto/vplay.sh", videoURL) - - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error playing video: %v\nOutput: %s", err, output) - } - - log.Info().Msg("Video playing successfully") - return nil -} - - diff --git a/pkg/platforms/mister/config.go b/pkg/platforms/mister/config.go index c58a5431..5d3479ef 100644 --- a/pkg/platforms/mister/config.go +++ b/pkg/platforms/mister/config.go @@ -15,7 +15,6 @@ const ( FailSoundFile = TempFolder + "/fail.wav" SocketFile = TempFolder + "/tapto.sock" PidFile = TempFolder + "/tapto.pid" - vplayFilePath = TempFolder + "/vplay.sh" MappingsFile = "/media/fat/nfc.csv" TokenReadFile = "/tmp/TOKENREAD" ConfigFolder = mrextConfig.ScriptsConfigFolder + "/tapto" @@ -25,94 +24,7 @@ const ( ArcadeDbFile = ConfigFolder + "/ArcadeDatabase.csv" ScriptsFolder = mrextConfig.ScriptsFolder CmdInterface = "/dev/MiSTer_cmd" - vplayContent = `#!/bin/bash - -declare -g crtmode_640="video_mode=640,16,64,80,240,1,3,14,12380" -declare -g crtmode_320="video_mode=320,-16,32,32,240,1,3,13,5670" - -# Read the contents of the INI file -declare -g ini_file="/media/fat/MiSTer.ini" -declare -g ini_contents=$(cat "$ini_file") - -declare -g branch="main" -declare -g repository_url="https://github.com/mrchrisster/MiSTer_SAM" - -function curl_download() { # curl_download ${filepath} ${URL} - - curl \ - --connect-timeout 15 --max-time 600 --retry 3 --retry-delay 5 --silent --show-error \ - --insecure \ - --fail \ - --location \ - -o "${1}" \ - "${2}" -} - -function get_mplayer() { - echo " Downloading wizzo's mplayer for SAM..." - echo " Created for MiSTer by wizzo" - echo " https://github.com/wizzomafizzo/mrext" - latest_mplayer="${repository_url}/blob/${branch}/.MiSTer_SAM/mplayer.zip?raw=true" - latest_mbc="${repository_url}/blob/${branch}/.MiSTer_SAM/mbc?raw=true" - curl_download "/tmp/tapto/mplayer.zip" "${latest_mplayer}" - curl_download "/tmp/tapto/mbc" "${latest_mbc}" - unzip -ojq /tmp/tapto/mplayer.zip -d "/tmp/tapto" - chmod +x /tmp/tapto/mplayer - chmod +x /tmp/tapto/mbc - rm /tmp/tapto/mplayer.zip - echo " Done." -} - -if [ ! -f /tmp/tapto/mplayer ]; then - get_mplayer -fi - -function mplayer() { - LD_LIBRARY_PATH=/tmp/tapto /tmp/tapto/mplayer "$@" -} - -url=$1 - -res="$(mplayer -vo null -ao null -identify -frames 0 $url | grep "VIDEO:" | awk '{print $3}')" -res_comma=$(echo "$res" | tr 'x' ',') -res_space=$(echo "$res" | tr 'x' ' ') - -function change_menures() { -# Backup mister.ini -if [ -f "$ini_file" ]; then - cp "$ini_file" "${ini_file}".vpl -else - touch "$ini_file" -fi - -#append menu info -echo -e "\n[menu]" >> "$ini_file" -echo -e "$crtmode_320" >> "$ini_file" -echo "[menu] entry created." - -# Replace video_mode if it exists within [menu] entry -if [[ $ini_contents =~ \[menu\].*video_mode=([^,[:space:]]+) ]]; then - awk -v res_comma="$res_comma" '/\[menu\]/{p=1} p&&/video_mode/{sub(/=.*/, "="res_comma",60"); p=0} 1' "$ini_file" > "$ini_file.tmp" && mv "$ini_file.tmp" "$ini_file" - echo "video_mode replaced in [menu] entry." -fi -} - -## Play video -change_menures -echo load_core /media/fat/menu.rbf > /dev/MiSTer_cmd -sleep 2 -# open mister terminal -/media/fat/Scripts/.MiSTer_SAM/mbc raw_seq :43 -chvt 2 -vmode -r ${res_space} rgb32 -sleep 2 -mplayer -cache 8192 "$url" -cp "$ini_file.vpl" "$ini_file" -echo load_core /media/fat/menu.rbf > /dev/MiSTer_cmd -` - - - + LinuxFolder = "/media/fat/linux" ) func UserConfigToMrext(cfg *config.UserConfig) *mrextConfig.UserConfig { diff --git a/pkg/platforms/mister/launchers.go b/pkg/platforms/mister/launchers.go index 7727da30..4e21e36b 100644 --- a/pkg/platforms/mister/launchers.go +++ b/pkg/platforms/mister/launchers.go @@ -3,12 +3,18 @@ package mister import ( + "fmt" "github.com/rs/zerolog/log" "github.com/wizzomafizzo/mrext/pkg/games" "github.com/wizzomafizzo/mrext/pkg/mister" "github.com/wizzomafizzo/tapto/pkg/config" "github.com/wizzomafizzo/tapto/pkg/database/gamesdb" "github.com/wizzomafizzo/tapto/pkg/platforms" + "os" + "os/exec" + "path/filepath" + "strings" + "time" ) func launch(cfg *config.UserConfig, path string) error { @@ -49,7 +55,127 @@ func launchAltCore( } } +func killCore(_ *config.UserConfig) error { + return mister.LaunchMenu() +} + +func launchMPlayer(pl Platform) func(*config.UserConfig, string) error { + return func(_ *config.UserConfig, path string) error { + if len(path) == 0 { + return fmt.Errorf("no path specified") + } + + vt := "4" + + if pl.ActiveSystem() != "" { + + } + + //err := mister.LaunchMenu() + //if err != nil { + // return err + //} + //time.Sleep(3 * time.Second) + + err := cleanConsole(vt) + if err != nil { + return err + } + + err = openConsole(pl.kbd, vt) + if err != nil { + return err + } + + time.Sleep(500 * time.Millisecond) + err = mister.SetVideoMode(640, 480) + if err != nil { + return fmt.Errorf("error setting video mode: %w", err) + } + + cmd := exec.Command( + "nice", + "-n", + "-20", + filepath.Join(LinuxFolder, "mplayer"), + "-cache", + "8192", + path, + ) + cmd.Env = append(os.Environ(), "LD_LIBRARY_PATH="+LinuxFolder) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + restore := func() { + err := mister.LaunchMenu() + if err != nil { + log.Warn().Err(err).Msg("error launching menu") + } + + err = restoreConsole(vt) + if err != nil { + log.Warn().Err(err).Msg("error restoring console") + } + } + + err = cmd.Start() + if err != nil { + restore() + return err + } + + err = cmd.Wait() + if err != nil { + restore() + return err + } + + restore() + return nil + } +} + +func killMPlayer(_ *config.UserConfig) error { + psCmd := exec.Command("sh", "-c", "ps aux | grep mplayer | grep -v grep") + output, err := psCmd.Output() + if err != nil { + log.Info().Msgf("mplayer processes not detected.") + return nil + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if line == "" { + continue + } + + log.Debug().Msgf("processing line: %s", line) + + fields := strings.Fields(line) + if len(fields) < 2 { + log.Warn().Msgf("unexpected line format: %s", line) + continue + } + + pid := fields[0] + log.Info().Msgf("killing mplayer process with PID: %s", pid) + + killCmd := exec.Command("kill", "-9", pid) + if err := killCmd.Run(); err != nil { + log.Error().Msgf("failed to kill process %s: %v", pid, err) + } + } + + return nil +} + var Launchers = []platforms.Launcher{ + { + Id: "Generic", + Extensions: []string{".mgl", ".rbf", ".mra"}, + Launch: launch, + }, // Consoles { Id: gamesdb.SystemAdventureVision, diff --git a/pkg/platforms/mister/methods.go b/pkg/platforms/mister/methods.go index afb1fa71..d1dbb1c0 100644 --- a/pkg/platforms/mister/methods.go +++ b/pkg/platforms/mister/methods.go @@ -43,25 +43,6 @@ func Setup(tr *Tracker) error { } _ = ff.Close() - vplayFile, err := os.Create(vplayFilePath) - if err != nil { - log.Error().Msgf("error creating vplay.sh file: %s", err) - return err - } - - _, err = vplayFile.WriteString(vplayContent) - if err != nil { - log.Error().Msgf("error writing to vplay.sh file: %s", err) - return err - } - _ = vplayFile.Close() - - err = os.Chmod(vplayFilePath, 0755) - if err != nil { - log.Error().Msgf("error setting executable permissions for vplay.sh: %s", err) - return err - } - // attempt arcadedb update go func() { haveInternet := utils.WaitForInternet(30) diff --git a/pkg/platforms/mister/platform.go b/pkg/platforms/mister/platform.go index be7ffc15..5fb75588 100644 --- a/pkg/platforms/mister/platform.go +++ b/pkg/platforms/mister/platform.go @@ -5,11 +5,13 @@ package mister import ( "bufio" "encoding/xml" + "errors" "fmt" "github.com/wizzomafizzo/tapto/pkg/api/models" "github.com/wizzomafizzo/tapto/pkg/database/gamesdb" "github.com/wizzomafizzo/tapto/pkg/readers/optical_drive" "github.com/wizzomafizzo/tapto/pkg/service/tokens" + "github.com/wizzomafizzo/tapto/pkg/utils" "os" "os/exec" "path/filepath" @@ -292,7 +294,14 @@ func (p *Platform) LaunchSystem(cfg *config.UserConfig, id string) error { } func (p *Platform) LaunchFile(cfg *config.UserConfig, path string) error { - return mister.LaunchGenericFile(UserConfigToMrext(cfg), path) + launchers := utils.PathToLaunchers(cfg, p, path) + + if len(launchers) == 0 { + return errors.New("no launcher found") + } + + // just pick the first one for now + return launchers[0].Launch(cfg, path) } func (p *Platform) Shell(cmd string) error { @@ -480,7 +489,7 @@ func (p *Platform) Launchers() []platforms.Launcher { Id: gamesdb.SystemNeoGeo, SystemId: gamesdb.SystemNeoGeo, Folders: []string{"NEOGEO"}, - Extensions: []string{".neo"}, // TODO: .zip and folder support + Extensions: []string{".neo"}, Launch: launch, Scanner: func( cfg *config.UserConfig, @@ -544,8 +553,18 @@ func (p *Platform) Launchers() []platforms.Launcher { }, } + mplayerVideo := platforms.Launcher{ + Id: "MPlayerVideo", + SystemId: gamesdb.SystemVideo, + Folders: []string{"Video", "Movies", "TV"}, + Extensions: []string{".mp4", ".mkv", ".avi"}, + Launch: launchMPlayer(*p), + Kill: killMPlayer, + } + ls := Launchers ls = append(ls, amiga) ls = append(ls, neogeo) + ls = append(ls, mplayerVideo) return ls } diff --git a/pkg/platforms/mister/scripts.go b/pkg/platforms/mister/scripts.go index 584f228c..64f5a420 100644 --- a/pkg/platforms/mister/scripts.go +++ b/pkg/platforms/mister/scripts.go @@ -14,7 +14,7 @@ import ( "github.com/wizzomafizzo/mrext/pkg/input" ) -func openConsole(kbd input.Keyboard) error { +func openConsole(kbd input.Keyboard, vt string) error { getTty := func() (string, error) { sys := "/sys/devices/virtual/tty/tty0/active" if _, err := os.Stat(sys); err != nil { @@ -36,7 +36,7 @@ func openConsole(kbd input.Keyboard) error { // which sets tty to 1 on success, then check in a loop if it actually did change to 1 and keep pressing F9 // until it's switched - err := exec.Command("chvt", "3").Run() + err := exec.Command("chvt", vt).Run() if err != nil { return err } @@ -78,7 +78,7 @@ func runScript(pl Platform, bin string, args string, hidden bool) error { } if !hidden { - err := openConsole(pl.kbd) + err := openConsole(pl.kbd, "3") if err != nil { hidden = true log.Warn().Msg("error opening console, running script headless") @@ -135,3 +135,45 @@ cd $(dirname "%s") return cmd.Run() } } + +func echoFile(path string, s string) error { + f, err := os.OpenFile(path, os.O_WRONLY, 0) + if err != nil { + return err + } + + _, err = f.WriteString(s) + if err != nil { + return err + } + + return f.Close() +} + +func writeTty(id string, s string) error { + tty := "/dev/tty" + id + return echoFile(tty, s) +} + +func cleanConsole(vt string) error { + err := writeTty(vt, "\033[?25l") + if err != nil { + return err + } + + err = echoFile("/sys/class/graphics/fbcon/cursor_blink", "0") + if err != nil { + return err + } + + return writeTty(vt, "\033[?17;0;0c") +} + +func restoreConsole(vt string) error { + err := writeTty(vt, "\033[?25h") + if err != nil { + return err + } + + return echoFile("/sys/class/graphics/fbcon/cursor_blink", "1") +} diff --git a/pkg/platforms/platforms.go b/pkg/platforms/platforms.go index 905d52d1..ac1a4a3a 100644 --- a/pkg/platforms/platforms.go +++ b/pkg/platforms/platforms.go @@ -2,13 +2,10 @@ package platforms import ( "github.com/wizzomafizzo/tapto/pkg/api/models" - "github.com/wizzomafizzo/tapto/pkg/service/playlists" - "github.com/wizzomafizzo/tapto/pkg/service/tokens" - "path/filepath" - "strings" - "github.com/wizzomafizzo/tapto/pkg/config" "github.com/wizzomafizzo/tapto/pkg/readers" + "github.com/wizzomafizzo/tapto/pkg/service/playlists" + "github.com/wizzomafizzo/tapto/pkg/service/tokens" ) type CmdEnv struct { @@ -42,6 +39,8 @@ type Launcher struct { Schemes []string // Launch function, takes a direct as possible path/ID media file. Launch func(*config.UserConfig, string) error + // Kill function kills the current active launcher, if possible. + Kill func(*config.UserConfig) error // Optional function to perform custom media scanning. Takes the list of // results from the standard scan, if any, and returns the final list. Scanner func(*config.UserConfig, string, []ScanResult) ([]ScanResult, error) @@ -50,31 +49,6 @@ type Launcher struct { AllowListOnly bool } -// MatchSystemFile returns true if a given file's extension is valid for a system. -func MatchSystemFile(pl Platform, systemId string, path string) bool { - var launchers []Launcher - for _, l := range pl.Launchers() { - if l.SystemId == systemId { - launchers = append(launchers, l) - } - } - - // ignore dot files - if strings.HasPrefix(filepath.Base(path), ".") { - return false - } - - for _, l := range launchers { - for _, ext := range l.Extensions { - if strings.HasSuffix(strings.ToLower(path), ext) { - return true - } - } - } - - return false -} - type Platform interface { // Unique ID of the platform. Id() string @@ -132,3 +106,8 @@ type Platform interface { LookupMapping(tokens.Token) (string, bool) Launchers() []Launcher } + +type LaunchToken struct { + Token tokens.Token + Launcher Launcher +} diff --git a/pkg/service/readers.go b/pkg/service/readers.go index 81721d99..3b994535 100644 --- a/pkg/service/readers.go +++ b/pkg/service/readers.go @@ -5,7 +5,6 @@ import ( "github.com/wizzomafizzo/tapto/pkg/service/tokens" "strings" "time" - "os/exec" "github.com/rs/zerolog/log" "github.com/wizzomafizzo/tapto/pkg/config" @@ -126,39 +125,6 @@ func connectReaders( return nil } -func stopMPlayer() { - psCmd := exec.Command("sh", "-c", "ps aux | grep mplayer | grep -v grep") - output, err := psCmd.Output() - if err != nil { - log.Info().Msgf("mplayer processes not detected.") - return - } - - lines := strings.Split(string(output), "\n") - for _, line := range lines { - if line == "" { - continue - } - - log.Debug().Msgf("Processing line: %s", line) - - fields := strings.Fields(line) - if len(fields) < 2 { - log.Warn().Msgf("Unexpected line format: %s", line) - continue - } - - pid := fields[0] - log.Info().Msgf("Killing mplayer process with PID: %s", pid) - - killCmd := exec.Command("kill", "-9", pid) - if err := killCmd.Run(); err != nil { - log.Error().Msgf("Failed to kill process %s: %v", pid, err) - } - } -} - - func readerManager( pl platforms.Platform, cfg *config.UserConfig, @@ -166,7 +132,7 @@ func readerManager( itq chan<- tokens.Token, lsq chan *tokens.Token, ) { - inputQueue := make(chan readers.Scan) + scanQueue := make(chan readers.Scan) var err error var lastError time.Time @@ -229,7 +195,7 @@ func readerManager( } } - err := connectReaders(pl, cfg, st, inputQueue) + err := connectReaders(pl, cfg, st, scanQueue) if err != nil { log.Error().Msgf("error connecting rs: %s", err) } @@ -242,7 +208,7 @@ func readerManager( var scan *tokens.Token select { - case t := <-inputQueue: + case t := <-scanQueue: // a reader has sent a token for pre-processing log.Debug().Msgf("pre-processing token: %v", t) if t.Error != nil { @@ -304,7 +270,7 @@ func readerManager( } else { log.Info().Msg("token was removed") st.SetActiveCard(tokens.Token{}) - stopMPlayer() + //stopMPlayer() if shouldExit(cfg, pl, st) { startTimedExit() } diff --git a/pkg/service/tokens/tokens.go b/pkg/service/tokens/tokens.go index 1e68efbb..25c2f8c7 100644 --- a/pkg/service/tokens/tokens.go +++ b/pkg/service/tokens/tokens.go @@ -1,6 +1,16 @@ package tokens -import "time" +import ( + "time" +) + +const ( + TypeNTAG = "NTAG" + TypeMifare = "MIFARE" + TypeAmiibo = "Amiibo" + TypeLegoDimensions = "LegoDimensions" + SourcePlaylist = "Playlist" +) type Token struct { Type string @@ -11,11 +21,3 @@ type Token struct { Remote bool Source string } - -const ( - TypeNTAG = "NTAG" - TypeMifare = "MIFARE" - TypeAmiibo = "Amiibo" - TypeLegoDimensions = "LegoDimensions" - SourcePlaylist = "Playlist" -) diff --git a/pkg/utils/paths.go b/pkg/utils/paths.go new file mode 100644 index 00000000..6a775831 --- /dev/null +++ b/pkg/utils/paths.go @@ -0,0 +1,95 @@ +package utils + +import ( + "github.com/wizzomafizzo/tapto/pkg/config" + "github.com/wizzomafizzo/tapto/pkg/platforms" + "path/filepath" + "strings" +) + +// PathIsLauncher returns true if a given path matches against any of the +// criteria defined in a launcher. +func PathIsLauncher( + cfg *config.UserConfig, + pl platforms.Platform, + l platforms.Launcher, + path string, +) bool { + if len(path) == 0 { + return false + } + + lp := strings.ToLower(path) + + // ignore dot files + if strings.HasPrefix(filepath.Base(lp), ".") { + return false + } + + // check uri scheme + for _, scheme := range l.Schemes { + if strings.HasPrefix(lp, scheme+":") { + return true + } + } + + // check root folder if it's not a generic launcher + if len(l.Folders) > 0 { + inRoot := false + for _, folder := range pl.RootFolders(cfg) { + if strings.HasPrefix(lp, strings.ToLower(folder)) { + inRoot = true + break + } + } + + if !inRoot { + return false + } else if path[len(path)-1] == filepath.Separator { + // skip extension check if it's a folder + return true + } + } + + // check file extension + for _, ext := range l.Extensions { + if strings.HasSuffix(lp, ext) { + return true + } + } + + return false +} + +// MatchSystemFile returns true if a given path is for a given system. +func MatchSystemFile( + cfg *config.UserConfig, + pl platforms.Platform, + systemId string, + path string, +) bool { + for _, l := range pl.Launchers() { + if l.SystemId == systemId { + if PathIsLauncher(cfg, pl, l, path) { + return true + } + } + } + return false +} + +// PathToLaunchers is a reverse lookup to match a given path against all +// possible launchers in a platform. Returns all matched launchers. +func PathToLaunchers( + cfg *config.UserConfig, + pl platforms.Platform, + path string, +) []platforms.Launcher { + var launchers []platforms.Launcher + for _, l := range pl.Launchers() { + if PathIsLauncher(cfg, pl, l, path) { + launchers = append(launchers, l) + } + } + return launchers +}