1
0
forked from Mirrors/RGSX
- add new instructions on menus to describe each function
- upgrade controller_debug.pygame file to create a controller support
- update command-line interface to be more effiscient and readable
This commit is contained in:
skymike03
2025-09-12 17:00:51 +02:00
parent 3c36dd2e02
commit 45f5d8bf7b
14 changed files with 925 additions and 162 deletions

View File

@@ -2,11 +2,114 @@
Ce guide couvre toutes les commandes disponibles du CLI et fournit des exemples prêts à copier (Windows PowerShell).
## Nouveau: mode interactif
Vous pouvez maintenant lancer une session interactive et enchaîner les commandes sans retaper `python rgsx_cli.py` à chaque fois :
```powershell
python rgsx_cli.py
```
Vous verrez :
```
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
rgsx>
```
Dans cette session tapez directement les sous-commandes :
```
rgsx> platforms
rgsx> games --platform snes --search mario
rgsx> download --platform snes --game "Super Mario World (USA).zip"
rgsx> history --tail 10
rgsx> exit
```
Extras :
- `help` ou `?` affiche laide globale.
- `exit` ou `quit` quitte la session.
- `--verbose` une fois active les logs détaillés pour toute la session.
## Tableau formaté (platforms)
La commande `platforms` affiche maintenant un tableau ASCII à largeur fixe (sauf avec `--json`) :
```
+--------------------------------+-----------------+
| Nom de plateforme | Dossier |
+--------------------------------+-----------------+
| Nintendo Entertainment System | nes |
| Super Nintendo Entertainment.. | snes |
| Sega Mega Drive | megadrive |
+--------------------------------+-----------------+
```
Colonnes : 30 caractères pour le nom, 15 pour le dossier (troncature par `...`).
## Aliases & synonymes doptions (mis à jour)
Aliases des sous-commandes :
- `platforms``p`
- `games``g`
- `download``dl`
- `clear-history``clear`
Options équivalentes (toutes les formes listées sont acceptées) :
- Plateforme : `--platform`, `--p`, `-p`
- Jeu : `--game`, `--g`, `-g`
- Recherche : `--search`, `--s`, `-s`
- Forcer (download) : `--force`, `-f`
- Mode interactif (download) : `--interactive`, `-i`
Exemples avec alias :
```powershell
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
python rgsx_cli.py g --p snes --s mario
python rgsx_cli.py p --json
python rgsx_cli.py clear
```
## Sélection ambiguë lors dun download (nouveau tableau)
Quand vous tentez un téléchargement avec un titre non exact et que le mode interactif est actif (TTY ou `--interactive`), les correspondances saffichent en tableau :
```
No exact result found for this game: mario super yoshi
Select a match to download:
+------+--------------------------------------------------------------+------------+
| # | Title | Size |
+------+--------------------------------------------------------------+------------+
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
+------+--------------------------------------------------------------+------------+
Enter number (or press Enter to cancel):
```
Si vous annulez ou que le mode interactif nest pas actif, un tableau similaire est affiché (sans le prompt) suivi dun conseil.
## Recherche améliorée (multitokens) pour `games`
Loption `--search` / `--s` / `-s` utilise maintenant la même logique de classement que les suggestions du download :
1. Correspondance sous-chaîne (position la plus tôt) — priorité 0
2. Séquence de tokens dans lordre (non contiguë) — priorité 1 (écart le plus faible)
3. Tous les tokens présents dans nimporte quel ordre — priorité 2 (ensemble de tokens plus petit privilégié)
Les doublons sont dédupliqués en gardant le meilleur score. Ainsi une requête :
```powershell
python rgsx_cli.py games --p snes --s "super mario yoshi"
```
affiche toutes les variantes pertinentes de "Super Mario World 2 - Yoshi's Island" même si lordre des mots diffère.
Exemple de sortie :
```
+--------------------------------------------------------------+------------+
| Game Title | Size |
+--------------------------------------------------------------+------------+
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
+--------------------------------------------------------------+------------+
```
Si aucun résultat nest trouvé, seul len-tête est affiché puis un message.
## Prérequis
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne souvrira).
- Exécuter depuis le dossier contenant `rgsx_cli.py`.
## Syntaxe générale
## Syntaxe générale (mode classique)
Les options globales peuvent être placées avant ou après la sous-commande.
- Forme 1:
@@ -18,73 +121,69 @@ Les options globales peuvent être placées avant ou après la sous-commande.
python rgsx_cli.py <commande> [options] [--verbose] [--force-update|-force-update]
```
- `--verbose` active les logs détaillés (DEBUG) sur la sortie standard derreur.
- `--verbose` active les logs détaillés (DEBUG) sur stderr.
- `--force-update` (ou `-force-update`) purge les données locales et force le re-téléchargement du pack de données (systems_list, games/*.json, images).
Lorsque les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec une barre de progression).
Quand les données sources sont manquantes, le CLI télécharge et extrait automatiquement le pack (avec progression).
## Commandes
### 1) platforms — lister les plateformes
### 1) platforms (`platforms` / `p`) — lister les plateformes
- Options:
- `--json`: sortie JSON (objets `{ name, folder }`).
Exemples:
```powershell
python rgsx_cli.py platforms
python rgsx_cli.py platforms --json
python rgsx_cli.py --verbose platforms
python rgsx_cli.py platforms --verbose
python rgsx_cli.py p --json
python rgsx_cli.py --verbose p
python rgsx_cli.py p --verbose
```
Sortie texte: une ligne par plateforme, au format `Nom<TAB>Dossier`.
### 2) games — lister les jeux dune plateforme
### 2) games (`games` / `g`) — lister les jeux dune plateforme
- Options:
- `--platform <nom_ou_dossier>` (ex: `n64` ou "Nintendo 64").
- `--search <texte>`: filtre par sous-chaîne dans le nom du jeu.
- `--platform | --p | -p <nom_ou_dossier>` (ex: `n64` ou "Nintendo 64").
- `--search | --s | -s <texte>`: filtre par sous-chaîne.
Exemples:
```powershell
python rgsx_cli.py games --platform n64
python rgsx_cli.py games --platform "Nintendo 64" --search zelda
python rgsx_cli.py games --platform n64 --verbose
python rgsx_cli.py g --p "Nintendo 64" --s zelda
python rgsx_cli.py g -p n64 --verbose
```
Remarques:
- La plateforme est résolue par nom affiché (platform_name) ou par dossier (folder), sans tenir compte de la casse.
- La plateforme est résolue par nom affiché (platform_name) ou dossier, insensible à la casse.
### 3) download — télécharger un jeu
### 3) download (`download` / `dl`) — télécharger un jeu
- Options:
- `--platform <nom_ou_dossier>`
- `--game "<titre exact ou partiel>"`
- `--force`: ignorer lavertissement si lextension du fichier nest pas répertoriée comme supportée pour la plateforme.
- `--platform | --p | -p <nom_ou_dossier>`
- `--game | --g | -g "<titre exact ou partiel>"`
- `--force | -f`: ignorer lavertissement dextension non supportée.
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact nest trouvé.
Exemples:
```powershell
# Titre exact
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Correspondance partielle
# Si aucun titre exact nest trouvé, le CLI nautosélectionne plus. Il affiche des correspondances possibles.
python rgsx_cli.py download --platform n64 --game "Ocarina of Time (Beta)"
# ➜ Le CLI proposera une liste de titres potentiels (à relancer ensuite avec le titre exact).
# Titre partiel (sélection numérotée si aucun exact)
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
Mode interactif par défaut:
- Si aucun titre exact nest trouvé et que vous êtes dans un terminal interactif (TTY), une liste numérotée saffiche automatiquement pour choisir un match et lancer le téléchargement.
# Forcer malgré extension
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
# Forcer si lextension semble non supportée (ex: .rar)
python rgsx_cli.py download --platform snes --game "pack_roms.rar" --force
# Verbose positionné après la sous-commande
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
# Verbose après sous-commande
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
```
Pendant le téléchargement, une progression en pourcentage, taille (MB) et vitesse (MB/s) saffiche. Le résultat final est également écrit dans lhistorique.
Pendant le téléchargement: progression %, taille (MB), vitesse (MB/s). Résultat final aussi dans lhistorique.
Notes:
- Les ROMs sont enregistrées dans le dossier de la plateforme correspondante (ex: `R:\roms\n64`).
- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas lextension, un avertissement est affiché (vous pouvez utiliser `--force`).
- Les ROMs sont enregistrées dans le dossier plateforme correspondant (ex: `R:\roms\n64`).
- Si le fichier est une archive (zip/rar) et que la plateforme ne supporte pas lextension, un avertissement apparaît (utiliser `--force`).
### 4) history — afficher lhistorique
- Options:
@@ -98,62 +197,55 @@ python rgsx_cli.py history --tail 20
python rgsx_cli.py history --json
```
### 5) clear-history — vider lhistorique
### 5) clear-history (`clear-history` / `clear`) — vider lhistorique
Exemple:
```powershell
python rgsx_cli.py clear-history
python rgsx_cli.py clear
```
### Option globale: --force-update — purge + re-téléchargement des données
- Supprime `systems_list.json`, le dossier `games/` et `images/`, puis télécharge/extrait à nouveau le pack de données.
- Supprime `systems_list.json`, `games/`, `images/` puis retélécharge/extrait le pack.
Exemples:
```powershell
# Sans sous-commande: purge + re-téléchargement puis sortie
python rgsx_cli.py --force-update
# Placé après une sous-commande (accepté aussi)
python rgsx_cli.py platforms --force-update
python rgsx_cli.py p --force-update
```
## Comportements et conseils
- Résolution de plateforme: par nom affiché ou dossier, insensible à la casse. Pour la commande `games` et `download`, une recherche par sous-chaîne est utilisée si la correspondance exacte nest pas trouvée.
- Logs `--verbose`: principalement utiles lors des téléchargements/extractions; émis en DEBUG.
- Téléchargement de données manquantes: automatique avec progression harmonisée (téléchargement puis extraction).
- Résolution plateforme: par nom affiché ou dossier, insensible à la casse.
- `--verbose`: utile surtout pour téléchargements/extractions.
- Données manquantes: téléchargement + extraction automatiques.
- Codes de sortie (indicatif):
- `0`: succès
- `1`: échec du téléchargement/erreur générique
- `1`: échec téléchargement/erreur générique
- `2`: plateforme introuvable
- `3`: jeu introuvable
- `4`: extension non supportée (sans `--force`)
## Exemples rapides (copier-coller)
```powershell
# Lister plateformes (texte)
python rgsx_cli.py platforms
# Démarrer le shell interactif
python rgsx_cli.py
# Lister plateformes (alias)
python rgsx_cli.py p
# Lister plateformes (JSON)
python rgsx_cli.py platforms --json
python rgsx_cli.py p --json
# Lister jeux N64 avec filtre
python rgsx_cli.py games --platform n64 --search zelda
# Lister jeux N64 avec filtre (synonymes)
python rgsx_cli.py g --p n64 --s zelda
# Télécharger un jeu N64 (titre exact)
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Télécharger un jeu N64 (titre exact) avec alias
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Télécharger un jeu N64 (titre aproximatif)
python rgsx_cli.py download --platform n64 --game "Ocarina of Time"
Resultat (exemple) :
No exact result found for this game: Ocarina of Time
Select a match to download:
1. Legend of Zelda, The - Ocarina of Time (Europe) (Beta) (2003-02-13) (GameCube).zip
2. Legend of Zelda, The - Ocarina of Time (Europe) (Beta) (2003-02-21) (GameCube) (Debug).zip
...
15. F-Zero X (USA) (Beta) (The Legend of Zelda - Ocarina of Time leftover data).zip
# Télécharger (titre partiel) + sélection
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
# Voir lhistorique (20 dernières entrées)
# Historique (20 dernières entrées)
python rgsx_cli.py history --tail 20
# Purger et recharger les données de listes des systèmes et des jeux
# Purger et recharger le pack
python rgsx_cli.py --force-update
```

View File

@@ -6,7 +6,110 @@ This guide covers all available CLI commands with copy-ready Windows PowerShell
- Python installed and on PATH (the app runs in headless mode; no window will open).
- Run commands from the folder that contains `rgsx_cli.py`.
## General syntax
## Quick interactive mode (new)
You can now start an interactive shell once and issue multiple commands without retyping `python rgsx_cli.py` each time:
```powershell
python rgsx_cli.py
```
You will see a prompt like:
```
RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.
rgsx>
```
Inside this shell type subcommands exactly as you would after `python rgsx_cli.py`:
```
rgsx> platforms
rgsx> games --platform snes --search mario
rgsx> download --platform snes --game "Super Mario World (USA).zip"
rgsx> history --tail 10
rgsx> exit
```
Extras:
- `help` or `?` prints the global help.
- `exit` or `quit` leaves the shell.
- `--verbose` once sets persistent verbose logging for the rest of the session.
## Formatted table output (platforms)
The `platforms` command now renders a fixed-width ASCII table (unless `--json` is used):
```
+--------------------------------+-----------------+
| Platform Name | Folder |
+--------------------------------+-----------------+
| Nintendo Entertainment System | nes |
| Super Nintendo Entertainment.. | snes |
| Sega Mega Drive | megadrive |
+--------------------------------+-----------------+
```
Columns: 30 chars for name, 15 for folder (values longer are truncated with `...`).
## Aliases & option synonyms (updated)
Subcommand aliases:
- `platforms``p`
- `games``g`
- `download``dl`
- `clear-history``clear`
Option aliases (all shown forms are accepted; they are equivalent):
- Platform: `--platform`, `--p`, `-p`
- Game: `--game`, `--g`, `-g`
- Search: `--search`, `--s`, `-s`
- Force (download): `--force`, `-f`
- Interactive (download): `--interactive`, `-i`
Examples with aliases:
```powershell
python rgsx_cli.py dl -p snes -g "Super Mario World (USA).zip"
python rgsx_cli.py g --p snes --s mario
python rgsx_cli.py p --json
python rgsx_cli.py clear
```
## Ambiguous download selection (new table)
When you attempt a download with a non-exact title and interactive mode is active (TTY or `--interactive`), matches are displayed in a table:
```
No exact result found for this game: mario super yoshi
Select a match to download:
+------+--------------------------------------------------------------+------------+
| # | Title | Size |
+------+--------------------------------------------------------------+------------+
| 1 | Super Mario - Yoshi Island (Japan).zip | 3.2M |
| 2 | Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| 3 | Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
| 4 | Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| 5 | Super Mario - Yoshi Island (Japan) (Beta) (1995-07-10).zip | 3.1M |
+------+--------------------------------------------------------------+------------+
Enter number (or press Enter to cancel):
```
If you cancel or are not in interactive mode, a similar table is still shown (without the prompt) followed by a tip.
## Improved fuzzy search for games (multi-token)
The `--search` / `--s` / `-s` option now uses the same multi-strategy ranking as the download suggestion logic:
1. Substring match (position-based) — highest priority
2. Ordered non-contiguous token sequence (smallest gap wins)
3. All tokens present in any order (smaller token set size wins)
Duplicate titles are deduplicated by keeping the best scoring strategy. This means queries like:
```powershell
python rgsx_cli.py games --p snes --s "super mario yoshi"
```
will surface all relevant "Super Mario World 2 - Yoshi's Island" variants even if the word order differs.
Example output:
```
+--------------------------------------------------------------+------------+
| Game Title | Size |
+--------------------------------------------------------------+------------+
| Super Mario World 2 - Yoshi's Island (USA).zip | 3.3M |
| Super Mario World 2 - Yoshi's Island (Europe) (En,Fr,De).zip | 3.3M |
| Super Mario - Yoshi Island (Japan).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 1).zip | 3.2M |
| Super Mario - Yoshi Island (Japan) (Rev 2).zip | 3.2M |
+--------------------------------------------------------------+------------+
```
If no results are found the table displays only headers followed by a message.
## General syntax (non-interactive)
Global options can be placed before or after the subcommand.
- Form 1:
@@ -25,59 +128,55 @@ When source data is missing, the CLI will automatically download and extract the
## Commands
### 1) platforms — list platforms
### 1) platforms (`platforms` / `p`) — list platforms
- Options:
- `--json`: JSON output (objects `{ name, folder }`).
Examples:
```powershell
python rgsx_cli.py platforms
python rgsx_cli.py platforms --json
python rgsx_cli.py --verbose platforms
python rgsx_cli.py platforms --verbose
python rgsx_cli.py p --json
python rgsx_cli.py --verbose p
python rgsx_cli.py p --verbose
```
Text output: one line per platform, formatted as `Name<TAB>Folder`.
### 2) games — list games for a platform
### 2) games (`games` / `g`) — list games for a platform
- Options:
- `--platform <name_or_folder>` (e.g., `n64` or "Nintendo 64").
- `--search <text>`: filter by substring in game title.
- `--platform | --p | -p <name_or_folder>` (e.g., `n64` or "Nintendo 64").
- `--search | --s | -s <text>`: filter by substring in game title.
Examples:
```powershell
python rgsx_cli.py games --platform n64
python rgsx_cli.py games --platform "Nintendo 64" --search zelda
python rgsx_cli.py games --platform n64 --verbose
python rgsx_cli.py g --p "Nintendo 64" --s zelda
python rgsx_cli.py g -p n64 --verbose
```
Notes:
- The platform is resolved by display name (platform_name) or folder, case-insensitively.
### 3) download — download a game
### 3) download (`download` / `dl`) — download a game
- Options:
- `--platform <name_or_folder>`
- `--game "<exact or partial title>"`
- `--force`: ignore unsupported-extension warning for the platform.
- `--platform | --p | -p <name_or_folder>`
- `--game | --g | -g "<exact or partial title>"`
- `--force | -f`: ignore unsupported-extension warning for the platform.
- `--interactive | -i`: prompt to choose from matches when no exact title is found.
Examples:
```powershell
# Exact title
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Partial match
# If no exact title is found, the CLI no longer auto-selects; it displays suggestions.
python rgsx_cli.py download --platform n64 --game "Ocarina of Time (Beta)"
# ➜ The CLI shows a list of candidates (then run again with the exact title).
# Partial match (interactive numbered selection if no exact match)
python rgsx_cli.py dl -p n64 -g "Ocarina of Time (Beta)"
Interactive mode by default:
- If no exact title is found and you are in an interactive terminal (TTY), a numbered list is shown automatically so you can pick and start the download.
# Forced despite extension
python rgsx_cli.py dl -p snes -g "pack_roms.rar" -f
# Force if the file extension seems unsupported (e.g., .rar)
python rgsx_cli.py download --platform snes --game "pack_roms.rar" --force
# Verbose placed after the subcommand
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
# Verbose after subcommand
python rgsx_cli.py dl -p n64 -g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip" --verbose
```
During download, progress %, size (MB) and speed (MB/s) are shown. The final result is also written to history.
@@ -98,10 +197,10 @@ python rgsx_cli.py history --tail 20
python rgsx_cli.py history --json
```
### 5) clear-history — clear history
### 5) clear-history (`clear-history` / `clear`) — clear history
Example:
```powershell
python rgsx_cli.py clear-history
python rgsx_cli.py clear
```
### Global option: --force-update — purge + re-download data
@@ -113,7 +212,7 @@ Examples:
python rgsx_cli.py --force-update
# Placed after a subcommand (also accepted)
python rgsx_cli.py platforms --force-update
python rgsx_cli.py p --force-update
```
## Behavior and tips
@@ -129,20 +228,23 @@ python rgsx_cli.py platforms --force-update
## Quick examples (copy/paste)
```powershell
# Start interactive shell
python rgsx_cli.py
# List platforms (text)
python rgsx_cli.py platforms
python rgsx_cli.py p
# List platforms (JSON)
python rgsx_cli.py platforms --json
python rgsx_cli.py p --json
# List N64 games with filter
python rgsx_cli.py games --platform n64 --search zelda
# List N64 games with filter (using alias synonyms)
python rgsx_cli.py g --p n64 --s zelda
# Download an N64 game (exact title)
python rgsx_cli.py download --platform n64 --game "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Download an N64 game (exact title) using aliases
python rgsx_cli.py dl --p n64 --g "Legend of Zelda, The - Ocarina of Time (USA) (Beta).zip"
# Download with approximate title (suggestions + interactive pick)
python rgsx_cli.py download --platform n64 --game "Ocarina of Time"
python rgsx_cli.py dl -p n64 -g "Ocarina of Time"
# View last 20 history entries
python rgsx_cli.py history --tail 20

View File

@@ -215,38 +215,38 @@ else:
# Détection spécifique Elite AVANT la détection générique Xbox
if ("microsoft xbox controller" in lname):
config.xbox_elite_controller = True
logger.debug(f"Controller detected (Xbox Elite): {name}")
print(f"Controller detected (Xbox Elite): {name}")
logger.debug(f"Controller detected: {name}")
print(f"Controller detected: {name}")
break
if ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname):
config.xbox_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Xbox Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "playstation" in lname:
elif "playstation" in lname or "ps3" in lname or "sony" in lname:
config.playstation_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Playstation Controller detected : {name}")
print(f"Controller detected : {name}")
break
elif "nintendo" in lname:
config.nintendo_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Nintendo Controller detected : {name}")
print(f"Controller detected : {name}")
elif "trimui" in lname:
config.trimui_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Trimui Controller detected : {name}")
print(f"Controller detected : {name}")
elif "logitech" in lname:
config.logitech_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Logitech Controller detected : {name}")
print(f"Controller detected : {name}")
elif "8bitdo" in lname or "8-bitdo" in lname:
config.eightbitdo_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"8bitdoController detected : {name}")
print(f"Controller detected : {name}")
elif "steam" in lname:
config.steam_controller = True
logger.debug(f"Controller detected : {name}")
logger.debug(f"Steam Controller detected : {name}")
print(f"Controller detected : {name}")
# Note: virtual keyboard display now depends on controller presence (config.joystick)
logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}")

View File

@@ -13,7 +13,7 @@ except Exception:
pygame = None # type: ignore
# Version actuelle de l'application
app_version = "2.2.2.1"
app_version = "2.2.2.2"
def get_application_root():
@@ -173,14 +173,9 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en
batch_in_progress = False # Indique qu'un lot est en cours
batch_pending_game = None # Données du jeu en attente de confirmation d'extension
# --- Premium systems filtering ---
# Liste des marqueurs (substrings) indiquant qu'un système/plateforme requiert un compte premium ou une clé API.
# On teste la présence (case-insensitive) de ces marqueurs dans le nom du système (ex: "Microsoft Windows (1Fichier)").
# Ajoutez librement d'autres valeurs (ex: 'RealDebrid', 'AllDebrid') si de futurs systèmes nécessitent un compte.
PREMIUM_HOST_MARKERS = [
"1Fichier",
]
# Flag runtime contrôlant le masquage des systèmes premium dans le menu pause > games.
hide_premium_systems = False
# Indicateurs d'entrée (détectés au démarrage)

View File

@@ -1389,6 +1389,28 @@ def draw_language_menu(screen):
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y))
screen.blit(instruction_surface, instruction_rect)
def draw_menu_instruction(screen, instruction_text, last_button_bottom=None):
"""Dessine une ligne d'instruction centrée au-dessus du footer.
- Réserve une zone footer (72px) + marge bas.
- Si last_button_bottom est fourni, s'assure d'un écart minimal (16px).
- Utilise la petite police et couleurs du thème.
"""
if not instruction_text:
return
try:
instruction_surface = config.small_font.render(instruction_text, True, THEME_COLORS["text"])
footer_reserved = 72
bottom_margin = 12
instruction_y = config.screen_height - footer_reserved - bottom_margin
min_gap = 16
if last_button_bottom is not None and instruction_y - last_button_bottom < min_gap:
instruction_y = last_button_bottom + min_gap
instruction_rect = instruction_surface.get_rect(center=(config.screen_width // 2, instruction_y))
screen.blit(instruction_surface, instruction_rect)
except Exception as e:
logger.error(f"Erreur draw_menu_instruction: {e}")
def draw_display_menu(screen):
"""Affiche le sous-menu Affichage (layout, taille de police, systèmes non supportés)."""
screen.blit(OVERLAY, (0, 0))
@@ -1494,6 +1516,28 @@ def draw_pause_menu(screen, selected_option):
)
config.pause_menu_total_options = len(options)
# Instruction contextuelle pour l'option sélectionnée
# Mapping des clés i18n parallèles à la liste options (même ordre)
instruction_keys = [
"instruction_pause_language",
"instruction_pause_controls",
"instruction_pause_display",
"instruction_pause_games",
"instruction_pause_settings",
"instruction_pause_restart",
"instruction_pause_quit",
]
try:
key = instruction_keys[selected_option]
instruction_text = _(key)
except Exception:
instruction_text = "" # Sécurité si index hors borne
if instruction_text:
# Calcul de la position du dernier bouton pour éviter chevauchement
last_button_bottom = menu_y + margin_top_bottom + (len(options) - 1) * (button_height + 12) + button_height
draw_menu_instruction(screen, instruction_text, last_button_bottom)
def _draw_submenu_generic(screen, title, options, selected_index):
"""Helper générique pour dessiner un sous-menu hiérarchique."""
screen.blit(OVERLAY, (0, 0))
@@ -1529,6 +1573,57 @@ def draw_pause_controls_menu(screen, selected_index):
_("menu_back") if _ else "Back"
]
_draw_submenu_generic(screen, _("menu_controls") if _ else "Controls", options, selected_index)
# Instructions contextuelles
instruction_keys = [
"instruction_controls_help", # pour menu_controls (afficher l'aide)
"instruction_controls_remap", # remap
"instruction_generic_back", # retour
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
last_button_bottom = None # recalculer via géométrie si nécessaire; ici on réutilise calcul simple
# Reconstituer la position du dernier bouton comme dans _draw_submenu_generic
menu_width = int(config.screen_width * 0.72)
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
# Title height approximatif
title_surface = config.font.render("X", True, THEME_COLORS["text"]) # hauteur représentative
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10 # approx: title center adjust + bottom spacing
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
text = _(key)
if key == "instruction_display_hide_premium":
# Inject dynamic list of premium providers from config.PREMIUM_HOST_MARKERS
try:
from config import PREMIUM_HOST_MARKERS
# Clean, preserve order, remove duplicates (case-insensitive)
seen = set()
providers_clean = []
for p in PREMIUM_HOST_MARKERS:
if not p: continue
norm = p.strip()
if not norm: continue
low = norm.lower()
if low in seen: continue
seen.add(low)
providers_clean.append(norm)
providers_str = ", ".join(providers_clean)
if not providers_str:
providers_str = "-"
if "{providers}" in text:
try:
text = text.format(providers=providers_str)
except Exception:
# Fallback if formatting fails
text = f"{text.replace('{providers}','').strip()} {providers_str}".strip()
else:
# Append providers if placeholder missing (backward compatibility)
text = f"{text} : {providers_str}" if providers_str else text
except Exception:
pass
draw_menu_instruction(screen, text, last_button_bottom)
def draw_pause_display_menu(screen, selected_index):
from rgsx_settings import (
@@ -1583,6 +1678,28 @@ def draw_pause_display_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [layout_txt, font_txt, font_family_txt, unsupported_txt, unknown_txt, hide_premium_txt, filter_txt, back_txt]
_draw_submenu_generic(screen, _("menu_display"), options, selected_index)
instruction_keys = [
"instruction_display_layout",
"instruction_display_font_size",
"instruction_display_font_family",
"instruction_display_show_unsupported",
"instruction_display_unknown_ext",
"instruction_display_hide_premium",
"instruction_display_filter_platforms",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
menu_width = int(config.screen_width * 0.72)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_games_menu(screen, selected_index):
from rgsx_settings import get_sources_mode
@@ -1594,6 +1711,23 @@ def draw_pause_games_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [history_txt, source_txt, update_txt, back_txt]
_draw_submenu_generic(screen, _("menu_games") if _ else "Games", options, selected_index)
instruction_keys = [
"instruction_games_history",
"instruction_games_source_mode",
"instruction_games_update_cache",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_settings_menu(screen, selected_index):
from rgsx_settings import get_symlink_option
@@ -1618,6 +1752,23 @@ def draw_pause_settings_menu(screen, selected_index):
back_txt = _("menu_back") if _ else "Back"
options = [music_option, symlink_option, api_keys_txt, back_txt]
_draw_submenu_generic(screen, _("menu_settings_category") if _ else "Settings", options, selected_index)
instruction_keys = [
"instruction_settings_music",
"instruction_settings_symlink",
"instruction_settings_api_keys",
"instruction_generic_back",
]
key = instruction_keys[selected_index] if 0 <= selected_index < len(instruction_keys) else None
if key:
button_height = int(config.screen_height * 0.045)
margin_top_bottom = 26
menu_height = (len(options)+1) * (button_height + 10) + 2 * margin_top_bottom
menu_y = (config.screen_height - menu_height) // 2
title_surface = config.font.render("X", True, THEME_COLORS["text"])
title_rect_height = title_surface.get_height()
start_y = menu_y + margin_top_bottom//2 + title_rect_height + 10 + 10
last_button_bottom = start_y + (len(options)-1) * (button_height + 10) + button_height
draw_menu_instruction(screen, _(key), last_button_bottom)
def draw_pause_api_keys_status(screen):
screen.blit(OVERLAY, (0,0))

View File

@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Premium-Systeme sichtbar"
,"submenu_display_font_family": "Schrift"
,"popup_font_family_changed": "Schrift geändert: {0}"
,"instruction_pause_language": "Sprache der Oberfläche ändern"
,"instruction_pause_controls": "Steuerungsübersicht ansehen oder neu zuordnen"
,"instruction_pause_display": "Layout, Schriften und Systemsichtbarkeit konfigurieren"
,"instruction_pause_games": "Verlauf öffnen, Quelle wechseln oder Liste aktualisieren"
,"instruction_pause_settings": "Musik, Symlink-Option & API-Schlüsselstatus"
,"instruction_pause_restart": "RGSX neu starten um Konfiguration neu zu laden"
,"instruction_pause_quit": "RGSX Anwendung beenden"
,"instruction_controls_help": "Komplette Referenz für Controller & Tastatur anzeigen"
,"instruction_controls_remap": "Tasten / Buttons neu zuordnen"
,"instruction_generic_back": "Zum vorherigen Menü zurückkehren"
,"instruction_display_layout": "Rasterabmessungen (Spalten × Zeilen) durchschalten"
,"instruction_display_font_size": "Schriftgröße für bessere Lesbarkeit anpassen"
,"instruction_display_font_family": "Zwischen verfügbaren Schriftarten wechseln"
,"instruction_display_show_unsupported": "Nicht in es_systems.cfg definierte Systeme anzeigen/ausblenden"
,"instruction_display_unknown_ext": "Warnung für in es_systems.cfg fehlende Dateiendungen an-/abschalten"
,"instruction_display_hide_premium": "Systeme ausblenden, die Premiumzugang erfordern über API: {providers}"
,"instruction_display_filter_platforms": "Manuell wählen welche Systeme sichtbar sind"
,"instruction_games_history": "Vergangene Downloads und Status anzeigen"
,"instruction_games_source_mode": "Zwischen RGSX oder eigener Quellliste wechseln"
,"instruction_games_update_cache": "Aktuelle Spieleliste erneut herunterladen & aktualisieren"
,"instruction_settings_music": "Hintergrundmusik aktivieren oder deaktivieren"
,"instruction_settings_symlink": "Verwendung von Symlinks für Installationen umschalten"
,"instruction_settings_api_keys": "Gefundene Premium-API-Schlüssel ansehen"
}

View File

@@ -154,5 +154,28 @@
,"popup_hide_premium_on": "Premium systems hidden"
,"popup_hide_premium_off": "Premium systems visible"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font changed: {0}"
,"popup_font_family_changed": "Font changed: {0}",
"instruction_pause_language": "Change the interface language",
"instruction_pause_controls": "View control layout or start remapping",
"instruction_pause_display": "Configure layout, fonts and system visibility",
"instruction_pause_games": "Open history, switch source or refresh list",
"instruction_pause_settings": "Music, symlink option & API keys status",
"instruction_pause_restart": "Restart RGSX to reload configuration"
,"instruction_pause_quit": "Exit the RGSX application"
,"instruction_controls_help": "Show full controller & keyboard reference"
,"instruction_controls_remap": "Change button / key bindings"
,"instruction_generic_back": "Return to the previous menu"
,"instruction_display_layout": "Cycle grid dimensions (columns × rows)"
,"instruction_display_font_size": "Adjust text scale for readability"
,"instruction_display_font_family": "Switch between available font families"
,"instruction_display_show_unsupported": "Show/hide systems not defined in es_systems.cfg"
,"instruction_display_unknown_ext": "Enable/disable warning for file extensions absent from es_systems.cfg"
,"instruction_display_hide_premium": "Hide systems requiring premium access via API: {providers}"
,"instruction_display_filter_platforms": "Manually choose which systems are visible"
,"instruction_games_history": "List past downloads and statuses"
,"instruction_games_source_mode": "Switch between RGSX or your own custom list source"
,"instruction_games_update_cache": "Redownload & refresh current games list"
,"instruction_settings_music": "Enable or disable background music playback"
,"instruction_settings_symlink": "Toggle using filesystem symlinks for installs"
,"instruction_settings_api_keys": "See detected premium provider API keys"
}

View File

@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemas Premium visibles"
,"submenu_display_font_family": "Fuente"
,"popup_font_family_changed": "Fuente cambiada: {0}"
,"instruction_pause_language": "Cambiar el idioma de la interfaz"
,"instruction_pause_controls": "Ver esquema de controles o remapear"
,"instruction_pause_display": "Configurar distribución, fuentes y visibilidad de sistemas"
,"instruction_pause_games": "Abrir historial, cambiar fuente o refrescar lista"
,"instruction_pause_settings": "Música, opción symlink y estado de claves API"
,"instruction_pause_restart": "Reiniciar RGSX para recargar configuración"
,"instruction_pause_quit": "Salir de la aplicación RGSX"
,"instruction_controls_help": "Mostrar referencia completa de mando y teclado"
,"instruction_controls_remap": "Cambiar asignación de botones / teclas"
,"instruction_generic_back": "Volver al menú anterior"
,"instruction_display_layout": "Alternar dimensiones de la cuadrícula (columnas × filas)"
,"instruction_display_font_size": "Ajustar tamaño del texto para mejor legibilidad"
,"instruction_display_font_family": "Cambiar entre familias de fuentes disponibles"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas no definidos en es_systems.cfg"
,"instruction_display_unknown_ext": "Activar/desactivar aviso para extensiones no presentes en es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que requieren acceso premium vía API: {providers}"
,"instruction_display_filter_platforms": "Elegir manualmente qué sistemas son visibles"
,"instruction_games_history": "Ver descargas pasadas y su estado"
,"instruction_games_source_mode": "Cambiar entre lista RGSX o fuente personalizada"
,"instruction_games_update_cache": "Volver a descargar y refrescar la lista de juegos"
,"instruction_settings_music": "Activar o desactivar música de fondo"
,"instruction_settings_symlink": "Alternar uso de symlinks en instalaciones"
,"instruction_settings_api_keys": "Ver claves API premium detectadas"
}

View File

@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Systèmes Premium visibles"
,"submenu_display_font_family": "Police"
,"popup_font_family_changed": "Police changée : {0}"
,"instruction_pause_language": "Changer la langue de l'interface"
,"instruction_pause_controls": "Afficher la configuration ou remapper"
,"instruction_pause_display": "Agencer l'affichage, polices et systèmes visibles"
,"instruction_pause_games": "Historique, source de liste ou rafraîchissement"
,"instruction_pause_settings": "Musique, option symlink & statut des clés API"
,"instruction_pause_restart": "Redémarrer RGSX pour recharger la configuration"
,"instruction_pause_quit": "Quitter l'application RGSX"
,"instruction_controls_help": "Afficher la référence complète manette & clavier"
,"instruction_controls_remap": "Modifier l'association boutons / touches"
,"instruction_generic_back": "Revenir au menu précédent"
,"instruction_display_layout": "Changer les dimensions de la grille"
,"instruction_display_font_size": "Ajuster la taille du texte pour la lisibilité"
,"instruction_display_font_family": "Basculer entre les polices disponibles"
,"instruction_display_show_unsupported": "Afficher/masquer systèmes absents de es_systems.cfg"
,"instruction_display_unknown_ext": "Avertir ou non pour extensions absentes de es_systems.cfg"
,"instruction_display_hide_premium": "Masquer les systèmes nécessitant un accès premium via API: {providers}"
,"instruction_display_filter_platforms": "Choisir manuellement les systèmes visibles"
,"instruction_games_history": "Lister les téléchargements passés et leur statut"
,"instruction_games_source_mode": "Basculer entre liste RGSX ou source personnalisée"
,"instruction_games_update_cache": "Retélécharger & rafraîchir la liste des jeux"
,"instruction_settings_music": "Activer ou désactiver la lecture musicale"
,"instruction_settings_symlink": "Basculer l'utilisation de symlinks pour l'installation"
,"instruction_settings_api_keys": "Voir les clés API détectées des services premium"
}

View File

@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemi Premium visibili"
,"submenu_display_font_family": "Font"
,"popup_font_family_changed": "Font cambiato: {0}"
,"instruction_pause_language": "Cambiare la lingua dell'interfaccia"
,"instruction_pause_controls": "Vedere schema controlli o avviare rimappatura"
,"instruction_pause_display": "Configurare layout, font e visibilità sistemi"
,"instruction_pause_games": "Aprire cronologia, cambiare sorgente o aggiornare elenco"
,"instruction_pause_settings": "Musica, opzione symlink e stato chiavi API"
,"instruction_pause_restart": "Riavvia RGSX per ricaricare la configurazione"
,"instruction_pause_quit": "Uscire dall'applicazione RGSX"
,"instruction_controls_help": "Mostrare riferimento completo controller & tastiera"
,"instruction_controls_remap": "Modificare associazione pulsanti / tasti"
,"instruction_generic_back": "Tornare al menu precedente"
,"instruction_display_layout": "Scorrere dimensioni griglia (colonne × righe)"
,"instruction_display_font_size": "Regolare dimensione testo per leggibilità"
,"instruction_display_font_family": "Cambiare famiglia di font disponibile"
,"instruction_display_show_unsupported": "Mostrare/nascondere sistemi non definiti in es_systems.cfg"
,"instruction_display_unknown_ext": "Attivare/disattivare avviso per estensioni assenti in es_systems.cfg"
,"instruction_display_hide_premium": "Nascondere sistemi che richiedono accesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Scegliere manualmente quali sistemi sono visibili"
,"instruction_games_history": "Elencare download passati e stato"
,"instruction_games_source_mode": "Passare tra elenco RGSX o sorgente personalizzata"
,"instruction_games_update_cache": "Riscaria e aggiorna l'elenco dei giochi"
,"instruction_settings_music": "Abilitare o disabilitare musica di sottofondo"
,"instruction_settings_symlink": "Abilitare/disabilitare uso symlink per installazioni"
,"instruction_settings_api_keys": "Mostrare chiavi API premium rilevate"
}

View File

@@ -155,4 +155,27 @@
"popup_hide_premium_off": "Sistemas Premium visíveis"
,"submenu_display_font_family": "Fonte"
,"popup_font_family_changed": "Fonte alterada: {0}"
,"instruction_pause_language": "Alterar o idioma da interface"
,"instruction_pause_controls": "Ver esquema de controles ou iniciar remapeamento"
,"instruction_pause_display": "Configurar layout, fontes e visibilidade de sistemas"
,"instruction_pause_games": "Abrir histórico, mudar fonte ou atualizar lista"
,"instruction_pause_settings": "Música, opção symlink e status das chaves API"
,"instruction_pause_restart": "Reiniciar RGSX para recarregar configuração"
,"instruction_pause_quit": "Sair da aplicação RGSX"
,"instruction_controls_help": "Mostrar referência completa de controle e teclado"
,"instruction_controls_remap": "Modificar associação de botões / teclas"
,"instruction_generic_back": "Voltar ao menu anterior"
,"instruction_display_layout": "Alternar dimensões da grade (colunas × linhas)"
,"instruction_display_font_size": "Ajustar tamanho do texto para legibilidade"
,"instruction_display_font_family": "Alternar entre famílias de fontes disponíveis"
,"instruction_display_show_unsupported": "Mostrar/ocultar sistemas não definidos em es_systems.cfg"
,"instruction_display_unknown_ext": "Ativar/desativar aviso para extensões ausentes em es_systems.cfg"
,"instruction_display_hide_premium": "Ocultar sistemas que exigem acesso premium via API: {providers}"
,"instruction_display_filter_platforms": "Escolher manualmente quais sistemas são visíveis"
,"instruction_games_history": "Listar downloads anteriores e status"
,"instruction_games_source_mode": "Alternar entre lista RGSX ou fonte personalizada"
,"instruction_games_update_cache": "Baixar novamente e atualizar a lista de jogos"
,"instruction_settings_music": "Ativar ou desativar música de fundo"
,"instruction_settings_symlink": "Ativar/desativar uso de symlinks para instalações"
,"instruction_settings_api_keys": "Ver chaves API premium detectadas"
}

View File

@@ -1233,6 +1233,7 @@ async def download_from_1fichier(url, platform, game_name, is_zip_non_supported=
cancel_events.pop(task_id, None)
logger.debug(f"Fin download_from_1fichier, résultat: success={result[0]}, message={result[1]}")
return result[0], result[1]
def is_1fichier_url(url):
"""Détecte si l'URL est un lien 1fichier."""
return "1fichier.com" in url

View File

@@ -24,6 +24,53 @@ from rgsx_settings import get_sources_zip_url
logger = logging.getLogger("rgsx.cli")
# Unified size display helper: preserve pre-formatted locale strings (MiB, GiB, Go, Mo, Ko, MB, KB, bytes).
# If numeric (int/float or pure digit string), convert to binary units with suffix B, KiB, MiB, GiB.
# Otherwise return original string.
def display_size(val):
try:
if val is None:
return ''
if isinstance(val, (list, tuple)):
return ''
s = str(val).strip()
if not s:
return ''
lower = s.lower()
# Already human formatted (English or French common units or contains a space + unit token)
known_tokens = ("mib", "gib", "kib", "kb", "mb", "gb", "bytes", " b", " mo", " go", " ko", "mb ", "gb ")
if any(tok in lower for tok in known_tokens):
return s
# Pure numeric => treat as bytes
import re as _re
if _re.fullmatch(r"\d+", s):
b = float(s)
else:
# Leading numeric? if not, return original
m = _re.match(r"^([0-9]+(?:\.[0-9]+)?)", s)
if not m:
return s
# If trailing unit unknown, assume already human string
if len(s) > len(m.group(0)):
return s
b = float(m.group(1))
if b < 1024:
return f"{int(b)} B"
kib = b / 1024
if kib < 1024:
return f"{kib:.2f} KiB"
mib = kib / 1024
if mib < 1024:
return f"{mib:.2f} MiB"
gib = mib / 1024
return f"{gib:.2f} GiB"
except Exception:
try:
return str(val)
except Exception:
return ''
def setup_logging(verbose: bool):
level = logging.DEBUG if verbose else logging.WARNING
logging.basicConfig(level=level, format='%(levelname)s: %(message)s')
@@ -153,9 +200,26 @@ def cmd_platforms(args):
if getattr(args, 'json', False):
print(json.dumps(items, ensure_ascii=False, indent=2))
else:
# Hint before table
print("hint: you can use either the exact platform name or folder in --platform (e.g. 'SNK Neo Geo' or 'neogeo')")
# ASCII table with fixed widths: name=35, folder=15
NAME_W = 35
FOLDER_W = 15
def fmt_cell(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (FOLDER_W + 2) + "+"
header = f"| {'Platform Name'.ljust(NAME_W)} | {'Folder'.ljust(FOLDER_W)} |"
print(border)
print(header)
print(border)
for it in items:
# name TAB folder (folder may be empty for BIOS/virtual)
print(f"{it['name']}\t{it['folder']}")
row = f"| {fmt_cell(it['name'], NAME_W)} | {fmt_cell(it['folder'], FOLDER_W)} |"
print(row)
print(border)
def _resolve_platform(sources, platform_name: str):
@@ -188,13 +252,100 @@ def cmd_games(args):
or args.platform
)
games = load_games(platform_id)
# Fuzzy ranking similar to download suggestions when --search provided
if args.search:
q = args.search.lower()
games = [g for g in games if q in (g[0] or '').lower()]
query_raw = args.search.strip()
def _strip_ext(name: str) -> str:
try:
base, _ = os.path.splitext(name)
return base
except Exception:
return name
def _tokens(s: str) -> list[str]:
return re.findall(r"[a-z0-9]+", s.lower())
q_lower = query_raw.lower()
q_no_ext = _strip_ext(query_raw).lower()
q_tokens = _tokens(query_raw)
suggestions = [] # (priority, score, game_obj)
# 1) Substring match (full or sans extension) priority 0, score = position
for g in games:
# games items can be (name, url) or (name, url, size)
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
t_lower = title.lower()
t_no_ext = _strip_ext(t_lower)
pos_full = t_lower.find(q_lower) if q_lower else -1
pos_noext = t_no_ext.find(q_no_ext) if q_no_ext else -1
if pos_full != -1 or pos_noext != -1:
pos = pos_full if pos_full != -1 else pos_noext
suggestions.append((0, max(0, pos), g))
# Helper for ordered gap score
def ordered_gap_score(qt: list[str], tt: list[str]):
pos = []
start = 0
for tok in qt:
try:
i = next(i for i in range(start, len(tt)) if tt[i] == tok)
except StopIteration:
return None
pos.append(i)
start = i + 1
gap = (pos[-1] - pos[0]) - (len(qt) - 1)
return max(0, gap)
# 2) Ordered non-contiguous tokens (priority 1)
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
tt = _tokens(title)
score = ordered_gap_score(q_tokens, tt)
if score is not None:
suggestions.append((1, score, g))
# 3) All tokens present, any order (priority 2), score = token set size
if q_tokens:
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else None
if not title:
continue
t_tokens = set(_tokens(title))
if all(tok in t_tokens for tok in q_tokens):
suggestions.append((2, len(t_tokens), g))
# Deduplicate by title keeping best (lowest priority, then score)
best = {}
for prio, score, g in suggestions:
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
print(title)
key = title.lower()
cur = best.get(key)
if cur is None or (prio, score) < (cur[0], cur[1]):
best[key] = (prio, score, g)
ranked = sorted(best.values(), key=lambda x: (x[0], x[1], (x[2][0] if isinstance(x[2], (list, tuple)) and x[2] else str(x[2])).lower()))
games = [g for _, _, g in ranked]
# Table: Name (60) | Size (12) to allow "xxxx.xx MiB"
NAME_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
border = "+" + "-" * (NAME_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'Game Title'.ljust(NAME_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for g in games:
title = g[0] if isinstance(g, (list, tuple)) and g else str(g)
size_val = ''
if isinstance(g, (list, tuple)) and len(g) >= 3:
size_val = display_size(g[2])
row = f"| {trunc(title, NAME_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if args.search and not games:
print("No results for search.")
def cmd_history(args):
@@ -382,10 +533,37 @@ def cmd_download(args):
interactive = bool(getattr(args, 'interactive', False))
if interactive:
print("Select a match to download:")
# Tableau formaté: # (4) | Title (60) | Size (12)
NUM_W = 4
TITLE_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
# Use shared display_size
border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for i, s in enumerate(shown, start=1):
print(f" {i}. {s[2]}")
title = s[2]
size_val = ''
size_raw = None
for g in games:
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
size_raw = g[2]
break
if size_raw is not None:
size_val = display_size(size_raw)
row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if len(suggestions) > limit:
print(f" ... and {len(suggestions) - limit} more not shown")
print(f"... {len(suggestions) - limit} more not shown")
try:
choice = input("Enter number (or press Enter to cancel): ").strip()
except EOFError:
@@ -400,15 +578,41 @@ def cmd_download(args):
pass
if not match:
print("Here are potential matches (use the exact title with --game):")
NUM_W = 4
TITLE_W = 60
SIZE_W = 12
def trunc(text, width):
if len(text) <= width:
return text + ' ' * (width - len(text))
if width <= 3:
return text[:width]
return text[:width-3] + '...'
# Use shared display_size
border = "+" + "-" * (NUM_W + 2) + "+" + "-" * (TITLE_W + 2) + "+" + "-" * (SIZE_W + 2) + "+"
header = f"| {'#'.ljust(NUM_W)} | {'Title'.ljust(TITLE_W)} | {'Size'.ljust(SIZE_W)} |"
print(border)
print(header)
print(border)
for i, s in enumerate(shown, start=1):
print(f" {i}. {s[2]}")
title = s[2]
size_val = ''
size_raw = None
for g in games:
if isinstance(g, (list, tuple)) and g and g[0] == title and len(g) >= 3:
size_raw = g[2]
break
if size_raw is not None:
size_val = display_size(size_raw)
row = f"| {str(i).ljust(NUM_W)} | {trunc(title, TITLE_W)} | {trunc(size_val, SIZE_W)} |"
print(row)
print(border)
if len(suggestions) > limit:
print(f" ... and {len(suggestions) - limit} more")
print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
print(f"... {len(suggestions) - limit} more")
print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
sys.exit(3)
else:
print("No similar titles found.")
print("Tip: list games with: python rgsx_cli.py games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
print("Tip: list games with: games --platform \"%s\" --search \"%s\"" % (args.platform, query_raw))
sys.exit(3)
title, url = match
@@ -454,33 +658,109 @@ def cmd_download(args):
sys.exit(exit_code)
def interactive_loop(parser):
"""Simple REPL so user can run multiple subcommands without retyping python rgsx_cli.py.
Rules:
- Empty line: ignore
- help / ?: show help
- exit / quit: leave loop
- Global flags like --verbose can be set per command; verbose persists for session once set.
"""
persistent_verbose = False
print("RGSX CLI interactive mode. Type 'help' for commands, 'exit' to quit.")
while True:
try:
line = input("rgsx> ").strip()
except EOFError:
print()
break
except KeyboardInterrupt:
print()
break
if not line:
continue
if line in {"exit", "quit"}:
break
if line in {"help", "?"}:
parser.print_help()
continue
# Tokenize respecting simple quotes
try:
import shlex
argv = shlex.split(line)
except Exception:
argv = line.split()
# Inject persistent verbose if previously enabled and not explicitly disabled
if persistent_verbose and "--verbose" not in argv:
argv.insert(0, "--verbose")
try:
args = parser.parse_args(argv)
except SystemExit as se:
# argparse already printed error; continue loop
continue
# Update persistent verbose state
if getattr(args, 'verbose', False):
persistent_verbose = True
# Dispatch
if not getattr(args, 'cmd', None):
# If user typed e.g. just global flags
print("No command provided. Type 'help' to list commands.")
continue
setup_logging(getattr(args, 'verbose', False))
# Global force-update handling (duplicate minimal logic to avoid leaving loop early)
if getattr(args, 'force_update', False):
try:
if os.path.exists(config.SOURCES_FILE):
os.remove(config.SOURCES_FILE)
except Exception:
pass
try:
shutil.rmtree(config.GAMES_FOLDER, ignore_errors=True)
except Exception:
pass
try:
shutil.rmtree(config.IMAGES_FOLDER, ignore_errors=True)
except Exception:
pass
ok = ensure_data_present(verbose=True)
if not ok:
print("force-update failed; aborting command.")
continue
try:
args.func(args)
except SystemExit:
# Subcommand may sys.exit on errors; swallow in REPL
continue
except Exception as e:
print(f"Error: {e}")
continue
def build_parser():
p = argparse.ArgumentParser(prog="rgsx-cli", description="RGSX headless CLI")
p.add_argument("--verbose", action="store_true", help="Verbose logging")
p.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sub = p.add_subparsers(dest="cmd")
sp = sub.add_parser("platforms", help="List available platforms")
sp = sub.add_parser("platforms", aliases=["p"], help="List available platforms")
sp.add_argument("--json", action="store_true", help="Output JSON with name and folder")
# Also accept global flags after the subcommand
sp.add_argument("--verbose", action="store_true", help="Verbose logging")
sp.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sp.set_defaults(func=cmd_platforms)
sg = sub.add_parser("games", help="List games for a platform")
sg.add_argument("--platform", required=True, help="Platform name or key")
sg.add_argument("--search", help="Filter by name contains")
# Also accept global flags after the subcommand
sg = sub.add_parser("games", aliases=["g"], help="List games for a platform")
sg.add_argument("--platform", "--p", "-p", required=True, help="Platform name or key")
sg.add_argument("--search", "--s", "-s", help="Filter by name contains")
sg.add_argument("--verbose", action="store_true", help="Verbose logging")
sg.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sg.set_defaults(func=cmd_games)
sd = sub.add_parser("download", help="Download a game by title")
sd.add_argument("--platform", required=True)
sd.add_argument("--game", required=True)
sd.add_argument("--force", action="store_true", help="Override unsupported extension warning")
sd = sub.add_parser("download", aliases=["dl"], help="Download a game by title")
sd.add_argument("--platform", "--p", "-p", required=True)
sd.add_argument("--game", "--g", "-g", required=True)
sd.add_argument("--force", "-f", action="store_true", help="Override unsupported extension warning")
sd.add_argument("--interactive", "-i", action="store_true", help="Prompt to choose from matches when no exact title is found")
# Also accept global flags after the subcommand
sd.add_argument("--verbose", action="store_true", help="Verbose logging")
sd.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sd.set_defaults(func=cmd_download)
@@ -488,13 +768,11 @@ def build_parser():
sh = sub.add_parser("history", help="Show recent history")
sh.add_argument("--tail", type=int, default=50, help="Last N entries")
sh.add_argument("--json", action="store_true")
# Also accept global flags after the subcommand
sh.add_argument("--verbose", action="store_true", help="Verbose logging")
sh.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sh.set_defaults(func=cmd_history)
sc = sub.add_parser("clear-history", help="Clear history")
# Also accept global flags after the subcommand
sc = sub.add_parser("clear-history", aliases=["clear"], help="Clear history")
sc.add_argument("--verbose", action="store_true", help="Verbose logging")
sc.add_argument("--force-update", "-force-update", action="store_true", help="Purge data (games/images/systems_list) and redownload")
sc.set_defaults(func=cmd_clear_history)
@@ -507,6 +785,12 @@ def main(argv=None):
# Force headless mode for CLI
os.environ.setdefault("RGSX_HEADLESS", "1")
parser = build_parser()
if not argv:
# Start interactive mode
try:
interactive_loop(parser)
finally:
return
args = parser.parse_args(argv)
setup_logging(args.verbose)

View File

@@ -14,34 +14,34 @@ except Exception as e:
PROMPTS = [
# Face buttons
"SOUTH_BUTTON", # A on Xbox
"EAST_BUTTON", # B on Xbox
"WEST_BUTTON", # X on Xbox
"NORTH_BUTTON", # Y on Xbox
"SOUTH_BUTTON - CONFIRM", # A on Xbox
"EAST_BUTTON - CANCEL", # B on Xbox
"WEST_BUTTON - CLEAR HISTORY / SELECT GAMES", # X on Xbox
"NORTH_BUTTON - HISTORY", # Y on Xbox
# Meta
"START",
"SELECT",
"START - PAUSE",
"SELECT - FILTER",
# D-Pad
"DPAD_UP",
"DPAD_DOWN",
"DPAD_LEFT",
"DPAD_RIGHT",
"DPAD_UP - MOVE UP",
"DPAD_DOWN - MOVE DOWN",
"DPAD_LEFT - MOVE LEFT",
"DPAD_RIGHT - MOVE RIGHT",
# Bumpers
"LEFT_BUMPER",
"RIGHT_BUMPER",
"LEFT_BUMPER - LB/L1 - Delete last char",
"RIGHT_BUMPER - RB/R1 - Add space",
# Triggers
"LEFT_TRIGGER",
"RIGHT_TRIGGER",
"LEFT_TRIGGER - LT/L2 - Page +",
"RIGHT_TRIGGER - RT/R2 - Page -",
# Left stick directions
"JOYSTICK_LEFT_UP",
"JOYSTICK_LEFT_DOWN",
"JOYSTICK_LEFT_LEFT",
"JOYSTICK_LEFT_RIGHT",
"JOYSTICK_LEFT_UP - MOVE UP",
"JOYSTICK_LEFT_DOWN - MOVE DOWN",
"JOYSTICK_LEFT_LEFT - MOVE LEFT",
"JOYSTICK_LEFT_RIGHT - MOVE RIGHT",
# Right stick directions
"JOYSTICK_RIGHT_UP",
"JOYSTICK_RIGHT_DOWN",
"JOYSTICK_RIGHT_LEFT",
"JOYSTICK_RIGHT_RIGHT",
"JOYSTICK_RIGHT_UP - MOVE U P",
"JOYSTICK_RIGHT_DOWN - MOVE DOWN",
"JOYSTICK_RIGHT_LEFT - MOVE LEFT",
"JOYSTICK_RIGHT_RIGHT - MOVE RIGHT",
]