forked from Mirrors/RGSX
v2.2.2.2
- 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:
214
README_CLI.md
214
README_CLI.md
@@ -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 l’aide 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 d’options (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 d’un 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 s’affichent 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 n’est pas actif, un tableau similaire est affiché (sans le prompt) suivi d’un conseil.
|
||||
|
||||
## Recherche améliorée (multi‑tokens) pour `games`
|
||||
L’option `--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 l’ordre (non contiguë) — priorité 1 (écart le plus faible)
|
||||
3. Tous les tokens présents dans n’importe 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 l’ordre 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 n’est trouvé, seul l’en-tête est affiché puis un message.
|
||||
|
||||
## Prérequis
|
||||
- Python installé et accessible (le projet utilise un mode headless; aucune fenêtre ne s’ouvrira).
|
||||
- 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 d’erreur.
|
||||
- `--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 d’une plateforme
|
||||
### 2) games (`games` / `g`) — lister les jeux d’une 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 l’avertissement si l’extension du fichier n’est 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 l’avertissement d’extension non supportée.
|
||||
- `--interactive | -i`: choisir un titre parmi des correspondances quand aucun exact n’est 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 n’est trouvé, le CLI n’autosé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 n’est trouvé et que vous êtes dans un terminal interactif (TTY), une liste numérotée s’affiche 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 l’extension 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) s’affiche. Le résultat final est également écrit dans l’historique.
|
||||
Pendant le téléchargement: progression %, taille (MB), vitesse (MB/s). Résultat final aussi dans l’historique.
|
||||
|
||||
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 l’extension, 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 l’extension, un avertissement apparaît (utiliser `--force`).
|
||||
|
||||
### 4) history — afficher l’historique
|
||||
- Options:
|
||||
@@ -98,62 +197,55 @@ python rgsx_cli.py history --tail 20
|
||||
python rgsx_cli.py history --json
|
||||
```
|
||||
|
||||
### 5) clear-history — vider l’historique
|
||||
### 5) clear-history (`clear-history` / `clear`) — vider l’historique
|
||||
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 n’est 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 l’historique (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
|
||||
```
|
||||
|
||||
174
README_CLI_EN.md
174
README_CLI_EN.md
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
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}")
|
||||
print(f"Controller detected: {name}")
|
||||
break
|
||||
elif "playstation" in lname:
|
||||
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"Xbox Controller detected : {name}")
|
||||
print(f"Controller detected : {name}")
|
||||
break
|
||||
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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user