11 Commits

28 changed files with 359 additions and 86 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# ---> jlauncher
# ---> jail-launcher
# Datos descargados en tiempo de ejecución (clones git + cache de metadata)
jlauncher_data/
jail_launcher_data/
# Preferencias locales del usuario
settings.json
*.dist/
+14 -14
View File
@@ -1,4 +1,4 @@
# jlauncher
# jail-launcher
Lanzador de juegos de **jailgames**. A partir de `games.toml`, lista los juegos, los
clona/actualiza desde sus repos Gitea, lee su icono y descripción, y los compila/ejecuta.
@@ -15,14 +15,14 @@ GUI en **Python + PySide6**, pensada para compilarse a binario nativo con **Nuit
```bash
pip install PySide6
python -m jlauncher
python -m jail_launcher
```
La app crea una carpeta `jlauncher_data/` junto al proyecto (o junto al binario, si está
La app crea una carpeta `jail_launcher_data/` junto al proyecto (o junto al binario, si está
compilado) con esta estructura **anidada (Versión 1)**:
```
jlauncher_data/
jail_launcher_data/
<id_juego>/
repo/ # git clone del juego
metadata/
@@ -40,7 +40,7 @@ jlauncher_data/
- icono desde `release/icons/icon.png`.
- **Juga**: si hay `build_cmd`, compila primero; luego ejecuta `run_cmd`. Para estos
juegos basta `run_cmd = "make run"` (compila y ejecuta), con `build_cmd` vacío.
- **Esborra**: elimina la descarga local (carpeta del juego en `jlauncher_data/`),
- **Esborra**: elimina la descarga local (carpeta del juego en `jail_launcher_data/`),
sin quitar el juego del `games.toml`.
Menú **Opcions**: *Amaga els jocs no descarregats* (filtro persistente) y
@@ -55,7 +55,7 @@ Una entrada `[[game]]` por juego. Campos:
| Campo | Obligatorio | Descripción |
|---------------|-------------|--------------------------------------------------------------------|
| `id` | sí | slug → nombre de carpeta en `jlauncher_data/` |
| `id` | sí | slug → nombre de carpeta en `jail_launcher_data/` |
| `name` | sí | nombre visible |
| `clone_url` | sí | URL de git clone / pull |
| `run_cmd` | sí | comando que ejecuta el juego (cwd = repo) |
@@ -68,16 +68,16 @@ Una entrada `[[game]]` por juego. Campos:
`build.sh` lo hace todo: crea el `.venv`, instala dependencias (PySide6 + Nuitka +
zstandard) y compila un único ejecutable comprimido, empaquetándolo en
`dist/jlauncher-v<versión>-<os>-<arch>.tar.gz` junto a `games.toml`.
`dist/jail-launcher-v<versión>-<os>-<arch>.tar.gz` junto a `games.toml`.
```bash
./build.sh
# binario: dist/jlauncher (+ dist/games.toml)
# binario: dist/jail-launcher (+ dist/games.toml)
```
El binario crea `jlauncher_data/` y `settings.json` **junto a sí mismo** (resuelto vía
El binario crea `jail_launcher_data/` y `settings.json` **junto a sí mismo** (resuelto vía
`NUITKA_ONEFILE_DIRECTORY`). El punto de entrada para empaquetar es `app.py` (desde
fuente se ejecuta con `python -m jlauncher`).
fuente se ejecuta con `python -m jail_launcher`).
### Prerequisitos del sistema (no los instala el script)
@@ -95,8 +95,8 @@ binario suelto sino una **app nativa**:
```bash
./build.sh
# -> dist/jlauncher.app
# -> dist/jlauncher-v<versión>-macos-<arch>.dmg (arrastrar la app a Aplicaciones)
# -> dist/Jail Launcher.app
# -> dist/jail-launcher-v<versión>-macos-<arch>.dmg (arrastrar la app a Aplicaciones)
```
El icono vive en `icon/`: el build usa `icon/icon.icns` para el bundle y copia
@@ -105,9 +105,9 @@ sustituye esos ficheros (regenerables con `icon/create_icons.py`).
A diferencia del onefile, la `.app` **no** escribe junto a sí misma (rompería al moverla a
`/Applications`): guarda sus datos en
`~/Library/Application Support/jailgames/jlauncher/` (`jlauncher_data/`, `settings.json` y
`~/Library/Application Support/jailgames/jail-launcher/` (`jail_launcher_data/`, `settings.json` y
una copia editable de `games.toml`, sembrada la primera vez desde el bundle).
La app va **sin firma Developer ID** (firma ad-hoc), así que Gatekeeper avisará la primera
vez: ábrela con **clic derecho → Abrir**, o ejecuta
`xattr -dr com.apple.quarantine /Applications/jlauncher.app`.
`xattr -dr com.apple.quarantine "/Applications/Jail Launcher.app"`.
+3 -3
View File
@@ -1,11 +1,11 @@
"""Punto de entrada para empaquetar con Nuitka.
Importa el paquete ``jlauncher`` con imports absolutos para que los imports relativos
Importa el paquete ``jail_launcher`` con imports absolutos para que los imports relativos
internos (``from .config import …``) resuelvan correctamente al compilar. Para ejecutar
desde fuente sigue valiendo ``python -m jlauncher``.
desde fuente sigue valiendo ``python -m jail_launcher``.
"""
from jlauncher.__main__ import main
from jail_launcher.__main__ import main
if __name__ == "__main__":
raise SystemExit(main())
+138
View File
@@ -0,0 +1,138 @@
# Compila jail-launcher con Nuitka y empaqueta un release para Windows.
# - jail-launcher.exe (onefile, GUI sin consola, con icono) + games.toml + .zip.
# Equivalente nativo de build.sh (que cubre Linux/macOS).
# Requisitos del sistema (no los instala el script):
# - Python 3.10+ (con el launcher «py» o «python» en el PATH).
# - Un compilador C: Nuitka usa MSVC (Build Tools de Visual Studio) si existe;
# si no, ofrece descargar MinGW automáticamente (-AssumeYesForDownloads).
# - git en el PATH (lo usa la app en tiempo de ejecución, no el build).
#
# Uso: pwsh -File build.ps1 (o: .\build.ps1 desde PowerShell)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# Situarnos en la carpeta del script.
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $Here
# --- Versión (leída de jail_launcher/__init__.py) ---------------------------------
$initPath = Join-Path $Here 'jail_launcher\__init__.py'
$verMatch = Select-String -Path $initPath -Pattern '^__version__\s*=\s*"([^"]*)"' |
Select-Object -First 1
if (-not $verMatch) {
Write-Error '[build] no se pudo leer __version__ de jail_launcher/__init__.py'
}
$Version = $verMatch.Matches[0].Groups[1].Value
# Arquitectura al estilo uname -m (x86_64 / arm64).
$Arch = switch ($env:PROCESSOR_ARCHITECTURE) {
'AMD64' { 'x86_64' }
'ARM64' { 'arm64' }
default { $env:PROCESSOR_ARCHITECTURE.ToLower() }
}
# --- Intérprete de Python -----------------------------------------------------
# Preferimos el launcher «py -3»; si no, «python».
function Resolve-Python {
if (Get-Command py -ErrorAction SilentlyContinue) { return @('py', '-3') }
if (Get-Command python -ErrorAction SilentlyContinue) { return @('python') }
Write-Error '[build] no se encontró Python (ni «py» ni «python» en el PATH).'
}
$Py = Resolve-Python
# Comprueba si un módulo se puede importar en el venv, SIN abortar el script:
# bajo $ErrorActionPreference='Stop', un código de salida ≠ 0 (módulo ausente)
# se trataría como error terminante. Aquí relajamos eso solo para la sonda.
function Test-PyModule([string]$Module) {
$prev = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
& $VenvPython -c "import $Module" 2>&1 | Out-Null
return ($LASTEXITCODE -eq 0)
}
finally {
$ErrorActionPreference = $prev
}
}
# --- venv (en Windows los ejecutables viven en .venv\Scripts) -----------------
$VenvPython = Join-Path $Here '.venv\Scripts\python.exe'
if (-not (Test-Path $VenvPython)) {
Write-Host '[build] creando venv…'
& $Py[0] $Py[1..($Py.Count - 1)] -m venv .venv
& $VenvPython -m pip install --quiet --upgrade pip
}
Write-Host '[build] sincronizando dependencias…'
& $VenvPython -m pip install --quiet -r requirements.txt
# Nuitka + zstandard (compresión del onefile → binario más pequeño).
if (-not (Test-PyModule 'nuitka')) {
Write-Host '[build] instalando nuitka en el venv…'
& $VenvPython -m pip install --quiet 'nuitka[onefile]'
}
if (-not (Test-PyModule 'zstandard')) {
Write-Host '[build] instalando zstandard (compresión onefile)…'
& $VenvPython -m pip install --quiet zstandard
}
Write-Host "[build] versión: v$Version"
Write-Host '[build] limpiando artefactos previos…'
foreach ($d in 'dist', 'build', 'app.build', 'app.dist', 'app.onefile-build') {
if (Test-Path $d) { Remove-Item -Recurse -Force $d }
}
$IconIco = 'icon\icon.ico'
$IconPng = 'icon\icon.png'
if (-not (Test-Path $IconIco)) {
Write-Error "[build] falta $IconIco"
}
# --- Compilación onefile ------------------------------------------------------
Write-Host '[build] compilando jail-launcher.exe (PySide6 onefile; puede tardar varios minutos)…'
& $VenvPython -m nuitka `
--onefile `
--assume-yes-for-downloads `
--enable-plugin=pyside6 `
--include-package=jail_launcher `
--windows-icon-from-ico=$IconIco `
--windows-console-mode=disable `
--company-name=jailgames `
--product-name=jail-launcher `
--product-version=$Version `
--file-version=$Version `
--output-dir=dist `
--output-filename=jail-launcher `
--remove-output `
app.py
if ($LASTEXITCODE -ne 0) {
Write-Error '[build] Nuitka falló.'
}
$Exe = 'dist\jail-launcher.exe'
if (-not (Test-Path $Exe)) {
Write-Error '[build] Nuitka no produjo dist\jail-launcher.exe'
}
# games.toml junto al .exe (la app lo lee desde ahí: base_dir junto al binario).
Write-Host '[build] copiando games.toml junto al ejecutable…'
Copy-Item games.toml dist\games.toml -Force
# icon.png para el diálogo «Quant a» (paths.app_icon_path → base_dir()\icon\icon.png).
Write-Host '[build] sembrando icon\icon.png para el diálogo «Quant a»…'
New-Item -ItemType Directory -Force -Path dist\icon | Out-Null
Copy-Item $IconPng dist\icon\icon.png -Force
# --- Empaquetado .zip ---------------------------------------------------------
$ReleaseName = "jail-launcher-v$Version-windows-$Arch"
$Zip = "dist\$ReleaseName.zip"
Write-Host "[build] empaquetando release $ReleaseName.zip…"
Compress-Archive -Path dist\jail-launcher.exe, dist\games.toml, dist\icon `
-DestinationPath $Zip -Force
Write-Host '[build] hecho:'
Get-Item dist\jail-launcher.exe, dist\games.toml, $Zip |
Format-Table -AutoSize Name, @{Name = 'Size'; Expression = { '{0:N0} B' -f $_.Length } }
Write-Host '[build] el binario crea jail_launcher_data\ y settings.json junto a sí mismo.'
Write-Host '[build] distribuir: descomprimir el .zip (jail-launcher.exe + games.toml + icon\ juntos).'
+24 -23
View File
@@ -1,16 +1,16 @@
#!/usr/bin/env bash
# Compila jlauncher con Nuitka y empaqueta un release.
# Compila jail-launcher con Nuitka y empaqueta un release.
# - Linux: binario onefile + tar.gz.
# - macOS: jlauncher.app (con icono) dentro de un .dmg arrastrable a /Applications.
# - macOS: «Jail Launcher.app» (con icono) dentro de un .dmg arrastrable a /Applications.
# Requisitos del sistema (no los instala el script): ver README (compilador C, git…).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
VERSION="$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' jlauncher/__init__.py | head -n1)"
VERSION="$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' jail_launcher/__init__.py | head -n1)"
if [ -z "$VERSION" ]; then
echo "[build] no se pudo leer __version__ de jlauncher/__init__.py" >&2
echo "[build] no se pudo leer __version__ de jail_launcher/__init__.py" >&2
exit 1
fi
ARCH="$(uname -m)"
@@ -52,28 +52,29 @@ if [ "$OS" = "darwin" ]; then
exit 1
fi
echo "[build] compilando jlauncher.app (PySide6; puede tardar varios minutos)…"
echo "[build] compilando «Jail Launcher.app» (PySide6; puede tardar varios minutos)…"
.venv/bin/python -m nuitka \
--standalone \
--macos-create-app-bundle \
--macos-app-icon="$ICON_ICNS" \
--macos-app-name=jlauncher \
--macos-app-name="Jail Launcher" \
--macos-app-version="$VERSION" \
--macos-signed-app-name=com.jailgames.jlauncher \
--macos-signed-app-name=com.jailgames.jail-launcher \
--company-name=jailgames \
--product-name=jlauncher \
--product-name=jail-launcher \
--product-version="$VERSION" \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--include-package=jlauncher \
--include-package=jail_launcher \
--output-dir=dist \
--output-filename=jlauncher \
--output-filename=jail-launcher \
--remove-output \
app.py
# Según la versión, Nuitka nombra el bundle a partir del script de entrada
# (app.py -> app.app). Lo normalizamos a jlauncher.app.
APP="dist/jlauncher.app"
# (app.py -> app.app). Lo normalizamos a «Jail Launcher.app» (nombre visible en
# Finder/Dock; el ejecutable interno sigue siendo jail-launcher).
APP="dist/Jail Launcher.app"
if [ ! -d "$APP" ]; then
PRODUCED="$(find dist -maxdepth 1 -name '*.app' | head -n1)"
if [ -z "$PRODUCED" ]; then
@@ -91,35 +92,35 @@ if [ "$OS" = "darwin" ]; then
# Bundle ad-hoc (sin Developer ID): quitamos quarantine para abrir sin fricción local.
xattr -dr com.apple.quarantine "$APP" 2>/dev/null || true
DMG="dist/jlauncher-v${VERSION}-macos-${ARCH}.dmg"
DMG="dist/jail-launcher-v${VERSION}-macos-${ARCH}.dmg"
echo "[build] empaquetando ${DMG}"
STAGE="$(mktemp -d)"
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
rm -f "$DMG"
hdiutil create -volname "jlauncher" -srcfolder "$STAGE" \
hdiutil create -volname "Jail Launcher" -srcfolder "$STAGE" \
-ov -format UDZO "$DMG" >/dev/null
rm -rf "$STAGE"
echo "[build] hecho:"
du -sh "$APP" | sed 's/^/[build] /'
ls -lh "$DMG"
echo "[build] la app guarda datos en ~/Library/Application Support/jailgames/jlauncher/."
echo "[build] distribuir: el .dmg (arrastrar jlauncher.app a Aplicaciones)."
echo "[build] la app guarda datos en ~/Library/Application Support/jailgames/jail-launcher/."
echo "[build] distribuir: el .dmg (arrastrar «Jail Launcher.app» a Aplicaciones)."
echo "[build] sin firma Developer ID: primera apertura con clic derecho → Abrir."
else
# -------------------------------------------------------------------------
# Linux (y resto): binario onefile + tar.gz
# -------------------------------------------------------------------------
RELEASE_NAME="jlauncher-v${VERSION}-${OS}-${ARCH}"
RELEASE_NAME="jail-launcher-v${VERSION}-${OS}-${ARCH}"
echo "[build] compilando (PySide6 onefile; puede tardar varios minutos)…"
.venv/bin/python -m nuitka \
--onefile \
--assume-yes-for-downloads \
--enable-plugin=pyside6 \
--include-package=jlauncher \
--include-package=jail_launcher \
--output-dir=dist \
--output-filename=jlauncher \
--output-filename=jail-launcher \
--remove-output \
--lto=yes \
app.py
@@ -128,10 +129,10 @@ else
cp games.toml dist/games.toml
echo "[build] empaquetando release ${RELEASE_NAME}.tar.gz…"
tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jlauncher games.toml
tar -czf "dist/${RELEASE_NAME}.tar.gz" -C dist jail-launcher games.toml
echo "[build] hecho:"
ls -lh "dist/jlauncher" "dist/games.toml" "dist/${RELEASE_NAME}.tar.gz"
echo "[build] el binario crea jlauncher_data/ y settings.json junto a sí mismo."
echo "[build] distribuir: descomprimir el tar.gz (jlauncher + games.toml juntos)."
ls -lh "dist/jail-launcher" "dist/games.toml" "dist/${RELEASE_NAME}.tar.gz"
echo "[build] el binario crea jail_launcher_data/ y settings.json junto a sí mismo."
echo "[build] distribuir: descomprimir el tar.gz (jail-launcher + games.toml juntos)."
fi
+13 -13
View File
@@ -1,7 +1,7 @@
# Configuración de jlauncher — lista de juegos.
# Configuración de jail-launcher — lista de juegos.
#
# Campos por juego ([[game]]):
# id (obligatorio) slug interno → nombre de carpeta en jlauncher_data/
# id (obligatorio) slug interno → nombre de carpeta en jail_launcher_data/
# name (obligatorio) nombre visible en la lista
# clone_url (obligatorio) URL para git clone / git pull
# run_cmd (obligatorio) comando que ejecuta el juego (cwd = repo clonado)
@@ -15,30 +15,30 @@
# Otros pills (topics, descripción, fecha de lanzamiento, versión) salen
# automáticamente de Gitea / git; no hace falta escribirlos aquí.
data_dir = "jlauncher_data"
data_dir = "jail_launcher_data"
[[game]]
id = "coffee_crisis"
id = "coffee-crisis"
name = "Coffee Crisis"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/coffee_crisis.git"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/coffee-crisis.git"
build_cmd = ""
run_cmd = "make run"
players = "1-2 jugadors"
author = "JailDesigner"
[[game]]
id = "coffee_crisis_arcade_edition"
id = "coffee-crisis-ae"
name = "Coffee Crisis Arcade Edition"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/coffee_crisis_arcade_edition.git"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/coffee-crisis-ae.git"
build_cmd = ""
run_cmd = "make run"
players = "1-2 jugadors"
author = "JailDesigner"
[[game]]
id = "jaildoctors_dilemma"
id = "jaildoctors-dilemma"
name = "JailDoctor's Dilemma"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/jaildoctors_dilemma.git"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/jaildoctors-dilemma.git"
build_cmd = ""
run_cmd = "make run"
players = "1 jugador"
@@ -54,18 +54,18 @@ players = "1 jugador"
author = "JailDesigner"
[[game]]
id = "orni_attack"
id = "orni-attack"
name = "Orni Attack"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/orni_attack.git"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/orni-attack.git"
build_cmd = ""
run_cmd = "make run"
players = "1-2 jugadors"
author = "JailDesigner"
[[game]]
id = "projecte_2026"
id = "projecte-2026"
name = "Projecte 2026"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/projecte_2026.git"
clone_url = "https://gitea.sustancia.synology.me/jaildesigner-jailgames/projecte-2026.git"
build_cmd = ""
run_cmd = "make run"
players = "1 jugador"
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
"""jail-launcher — lanzador de juegos jailgames."""
__version__ = "1.0.5"
@@ -15,7 +15,10 @@ from .ui.theme import apply_theme
def main() -> int:
app = QApplication(sys.argv)
app.setApplicationName("jlauncher")
app.setApplicationName("jail-launcher")
# No fixem applicationDisplayName: a Windows/X11 Qt l'afegeix al títol de la
# finestra ("títol - displayName") i duplicaria «Jail Launcher», que ja surt
# a WINDOW_TITLE. El nom de l'app al menú de macOS ve del bundle (Nuitka).
icon_path = app_icon_path()
if icon_path is not None:
app.setWindowIcon(QIcon(str(icon_path)))
@@ -31,7 +31,7 @@ class Game:
@dataclass
class Config:
data_dir: str = "jlauncher_data"
data_dir: str = "jail_launcher_data"
games: list[Game] = field(default_factory=list)
@@ -80,4 +80,4 @@ def load_config(path: Path) -> Config:
if not games:
raise ValueError("games.toml no define ningún [[game]]")
return Config(data_dir=raw.get("data_dir", "jlauncher_data"), games=games)
return Config(data_dir=raw.get("data_dir", "jail_launcher_data"), games=games)
@@ -10,7 +10,9 @@ import datetime as _dt
import json
import os
import shutil
import stat
import subprocess
import sys
import urllib.error
import urllib.request
from collections.abc import Callable
@@ -44,11 +46,41 @@ class NetConfig:
DEFAULT_NET = NetConfig()
# Windows: evita que cada `git` (aplicación de consola) abra una ventana negra
# cuando el lanzador corre como GUI sin consola (el .exe compilado con Nuitka
# --windows-console-mode=disable). En el resto de SO es 0 (sin efecto).
_NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
def _noop(_: str) -> None:
pass
def _force_rmtree(path: Path, log: LogFn = _noop) -> None:
"""Esborra un arbre de fitxers, fins i tot amb fitxers de només-lectura.
A Windows els objectes de ``.git`` es creen com a només-lectura i fan que
``shutil.rmtree`` falli amb ``PermissionError``; cal llevar el bit d'escriptura
i reintentar. A diferència de ``ignore_errors=True``, aquí els errors que no
puguem resoldre es registren al log en lloc d'empassar-se en silenci.
"""
if not path.exists():
return
def handle(func: Callable, p: str, _exc: object) -> None:
try:
os.chmod(p, stat.S_IWRITE)
func(p) # reintenta l'operació (unlink/rmdir) que havia fallat
except OSError as exc:
log(f"No s'ha pogut esborrar {p}: {exc}")
# Python 3.12 va renombrar el paràmetre `onerror` a `onexc`; suportem tots dos.
if sys.version_info >= (3, 12):
shutil.rmtree(path, onexc=handle)
else:
shutil.rmtree(path, onerror=handle)
def _auth_args(token: str) -> list[str]:
"""Args -c para autenticar git ante Gitea con un token, sin tocar .git/config."""
if not token:
@@ -99,6 +131,7 @@ def _run_git(
text=True,
env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
timeout=timeout,
creationflags=_NO_WINDOW,
)
except subprocess.TimeoutExpired:
emit(
@@ -166,7 +199,7 @@ def delete_local(root: Path, game: Game, log: LogFn = _noop) -> None:
log(f"{game.name}: no hi ha res a esborrar")
return
log(f"Esborrant la descàrrega local de {game.name}")
shutil.rmtree(target, ignore_errors=True)
_force_rmtree(target, log)
def download(
@@ -201,7 +234,7 @@ def download(
else:
log(f"Clonant {game.name}")
if repo.exists(): # carpeta a medias sin .git: limpiarla
shutil.rmtree(repo, ignore_errors=True)
_force_rmtree(repo, log)
_run_git(
["clone", game.clone_url, str(repo)],
None,
@@ -272,6 +305,7 @@ def _read_version(game: Game, repo: Path, log: LogFn) -> str:
capture_output=True,
text=True,
timeout=20,
creationflags=_NO_WINDOW,
)
except (OSError, subprocess.SubprocessError) as exc:
log(f"version_cmd ha fallat: {exc}")
@@ -3,7 +3,7 @@
Compilado con Nuitka, ``__compiled__`` existe. En modo ``--onefile`` la carpeta del
binario real la expone ``NUITKA_ONEFILE_DIRECTORY`` (Nuitka 4.x); con versiones que usan
``NUITKA_ONEFILE_BINARY`` tomamos su carpeta; si no, ``sys.executable`` (standalone).
Ejecutando desde fuente usamos la raíz del proyecto (la carpeta que contiene ``jlauncher``).
Ejecutando desde fuente usamos la raíz del proyecto (la carpeta que contiene ``jail_launcher``).
"""
from __future__ import annotations
@@ -17,7 +17,7 @@ CONFIG_NAME = "games.toml"
# Carpeta de soporte en macOS cuando corremos como .app: ~/Library/Application Support/…
APP_SUPPORT_VENDOR = "jailgames"
APP_SUPPORT_APP = "jlauncher"
APP_SUPPORT_APP = "jail-launcher"
def is_compiled() -> bool:
@@ -26,7 +26,7 @@ def is_compiled() -> bool:
def base_dir() -> Path:
"""Carpeta base junto a la que viven games.toml y jlauncher_data."""
"""Carpeta base junto a la que viven games.toml y jail_launcher_data."""
if is_compiled():
directory = os.environ.get("NUITKA_ONEFILE_DIRECTORY")
if directory:
@@ -35,14 +35,14 @@ def base_dir() -> Path:
if binary:
return Path(binary).resolve().parent
return Path(sys.executable).resolve().parent
# Desde fuente: raíz del proyecto = padre del paquete jlauncher/
# Desde fuente: raíz del proyecto = padre del paquete jail_launcher/
return Path(__file__).resolve().parent.parent
def macos_app_bundle() -> Path | None:
"""Ruta del .app si corremos como bundle de macOS (``…/jlauncher.app``); si no, None.
"""Ruta del .app si corremos como bundle de macOS (``…/jail-launcher.app``); si no, None.
En un app bundle el ejecutable vive en ``/jlauncher.app/Contents/MacOS/jlauncher``,
En un app bundle el ejecutable vive en ``/jail-launcher.app/Contents/MacOS/jail-launcher``,
dentro de un árbol no escribible al instalarse en ``/Applications``. Detectarlo nos
deja redirigir datos y settings a ``~/Library/Application Support``.
"""
@@ -106,7 +106,7 @@ def config_file() -> Path:
return base_dir() / CONFIG_NAME
def data_root(data_dir: str = "jlauncher_data") -> Path:
def data_root(data_dir: str = "jail_launcher_data") -> Path:
"""Carpeta raíz de datos; se crea si no existe."""
root = writable_base() / data_dir
root.mkdir(parents=True, exist_ok=True)
@@ -4,6 +4,7 @@ from __future__ import annotations
import os
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
@@ -12,6 +13,11 @@ from .paths import repo_dir
LogFn = Callable[[str], None]
# Windows: evita que la compilación/ejecución (shell + make/gcc, apps de consola)
# abra ventanas negras cuando el lanzador corre como GUI sin consola (el .exe).
# En el resto de SO es 0 (sin efecto).
_NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
# Directorios habituales de herramientas de línea de comandos (Homebrew, MacPorts…).
# Una .app de macOS lanzada desde Finder/Dock NO hereda el PATH del shell de login,
# así que herramientas como cmake, instaladas con Homebrew en /opt/homebrew/bin
@@ -26,6 +32,17 @@ _EXTRA_PATH_DIRS = (
"/opt/local/bin",
)
# Prefijos de Homebrew/MacPorts. Una .app lanzada desde Finder/Dock tampoco
# hereda CPATH/LIBRARY_PATH/PKG_CONFIG_PATH del shell, así que el compilador no
# encuentra las cabeceras (p.ej. <SDL3/SDL_filesystem.h>) ni las librerías
# instaladas con `brew`. Añadimos sus include/lib explícitamente. En Linux estos
# paths no existen y se filtran solos.
_EXTRA_PREFIXES = (
"/opt/homebrew",
"/usr/local",
"/opt/local",
)
def _noop(_: str) -> None:
pass
@@ -41,9 +58,32 @@ def _launch_env() -> dict[str, str]:
if d not in parts and os.path.isdir(d):
parts.append(d)
env["PATH"] = os.pathsep.join(parts)
# Rutas de cabeceras (CPATH), librerías (LIBRARY_PATH) y pkg-config para que
# el compilador encuentre dependencias instaladas con brew/MacPorts.
includes, libs, pkgconfigs = [], [], []
for prefix in _EXTRA_PREFIXES:
inc, lib = f"{prefix}/include", f"{prefix}/lib"
if os.path.isdir(inc):
includes.append(inc)
if os.path.isdir(lib):
libs.append(lib)
pkgconfigs.append(f"{lib}/pkgconfig")
_prepend(env, "CPATH", includes)
_prepend(env, "LIBRARY_PATH", libs)
_prepend(env, "PKG_CONFIG_PATH", pkgconfigs)
return env
def _prepend(env: dict[str, str], var: str, dirs: list[str]) -> None:
"""Antepone dirs al valor de env[var] (lista separada por os.pathsep), sin
duplicar entradas ya presentes."""
existing = [p for p in env.get(var, "").split(os.pathsep) if p]
new = [d for d in dirs if d not in existing]
if new:
env[var] = os.pathsep.join(new + existing)
def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
"""Ejecuta un comando de shell en cwd, retransmitiendo stdout/err línea a línea."""
log(f"$ {cmd} (cwd={cwd})")
@@ -56,6 +96,7 @@ def _stream(cmd: str, cwd: Path, log: LogFn) -> int:
text=True,
bufsize=1,
env=_launch_env(),
creationflags=_NO_WINDOW,
)
assert proc.stdout is not None
for line in proc.stdout:
@@ -26,6 +26,14 @@ def _valid_console_mode(value) -> str:
return value if value in _CONSOLE_MODES else "auto"
_SORT_ORDERS = ("default", "name")
def _valid_sort_order(value) -> str:
"""Normaliza el orden de la lista; cae a 'default' (orden del TOML) si es desconocido."""
return value if value in _SORT_ORDERS else "default"
@dataclass
class Settings:
hide_not_downloaded: bool = False
@@ -34,6 +42,7 @@ class Settings:
check_updates_on_start: bool = False # comprobar updates automáticamente al iniciar
theme: str = "system" # tema de la UI: "system" | "light" | "dark"
console_mode: str = "auto" # consola de log: "show" | "auto" | "hide"
sort_order: str = "default" # orden de la lista: "default" (TOML) | "name"
# Tolerancia a repos offline/inalcanzables (segundos, salvo stall_limit en bytes/s).
git_fetch_timeout: int = 60 # techo para fetch / comprobar update
git_clone_timeout: int = 900 # techo para clone (repo grande)
@@ -61,6 +70,7 @@ def load_settings() -> Settings:
check_updates_on_start=bool(data.get("check_updates_on_start", False)),
theme=_valid_theme(data.get("theme", "system")),
console_mode=_valid_console_mode(data.get("console_mode", "auto")),
sort_order=_valid_sort_order(data.get("sort_order", "default")),
git_fetch_timeout=int(data.get("git_fetch_timeout", 60)),
git_clone_timeout=int(data.get("git_clone_timeout", 900)),
http_timeout=int(data.get("http_timeout", 15)),
+1
View File
@@ -0,0 +1 @@
"""Componentes de interfaz de jail-launcher."""
@@ -36,12 +36,15 @@ WINDOW_TITLE = f"© 2026 {APP_NAME} — JailDesigner"
CONSOLE_HEIGHT = 150 # alçada de la consola desplegada (px)
CONSOLE_ANIM_MS = 220 # durada de l'animació de desplegar/replegar
CONSOLE_IDLE_MS = 4000 # marge sense activitat abans de replegar en mode auto
CONSOLE_IDLE_MS = 3000 # marge sense activitat abans de replegar en mode auto
CONSOLE_SHOW = "show"
CONSOLE_AUTO = "auto"
CONSOLE_HIDE = "hide"
SORT_DEFAULT = "default" # ordre del games.toml
SORT_NAME = "name" # alfabètic pel nom
class MainWindow(QMainWindow):
def __init__(self, config: Config, root: Path, parent=None) -> None:
@@ -76,16 +79,15 @@ class MainWindow(QMainWindow):
# --- Lista de juegos con scroll ---
list_container = QWidget()
list_layout = QVBoxLayout(list_container)
list_layout.setContentsMargins(6, 6, 6, 6)
list_layout.setSpacing(6)
self.list_layout = QVBoxLayout(list_container)
self.list_layout.setContentsMargins(6, 6, 6, 6)
self.list_layout.setSpacing(6)
for game in config.games:
row = GameRow(game, root)
row.activated.connect(self._on_activate)
row.delete_requested.connect(self._on_delete)
self.rows[game.id] = row
list_layout.addWidget(row)
list_layout.addStretch(1)
self._populate_list()
scroll = QScrollArea()
scroll.setWidgetResizable(True)
@@ -238,6 +240,7 @@ class MainWindow(QMainWindow):
menu.addAction(self.action_check_on_start)
menu.addSeparator()
self._build_sort_menu(menu)
self._build_theme_menu(menu)
self._build_console_menu(menu)
@@ -293,13 +296,12 @@ class MainWindow(QMainWindow):
name.setFont(nf)
name.setStyleSheet("color: #7c4dff;")
lay.addWidget(name)
lay.addSpacing(4)
lay.addSpacing(2)
# Versió en cursiva i atenuada.
ver = QLabel(f"Versió {__version__}", alignment=Qt.AlignCenter)
# Versió: petita i atenuada, just davall del nom (estil macOS).
ver = QLabel(f"v{__version__}", alignment=Qt.AlignCenter)
vf = ver.font()
vf.setItalic(True)
vf.setPointSize(vf.pointSize() + 1)
vf.setPointSize(vf.pointSize() - 1)
ver.setFont(vf)
ver.setStyleSheet("color: #8a8a8a;")
lay.addWidget(ver)
@@ -330,6 +332,27 @@ class MainWindow(QMainWindow):
dlg.exec()
def _build_sort_menu(self, parent_menu) -> None:
"""Submenú Ordena amb dues opcions exclusives: Per defecte / Per nom."""
submenu = parent_menu.addMenu("Ordena")
group = QActionGroup(self)
group.setExclusive(True)
options = [
("Per defecte", SORT_DEFAULT),
("Per nom", SORT_NAME),
]
for label, mode in options:
action = QAction(label, self, checkable=True)
action.setChecked(self.settings.sort_order == mode)
action.triggered.connect(lambda _checked, m=mode: self._on_sort_selected(m))
group.addAction(action)
submenu.addAction(action)
def _on_sort_selected(self, mode: str) -> None:
self.settings.sort_order = mode
save_settings(self.settings)
self._populate_list()
def _build_theme_menu(self, parent_menu) -> None:
"""Submenú Tema amb tres opcions exclusives: Sistema / Clar / Fosc."""
submenu = parent_menu.addMenu("Tema")
@@ -413,6 +436,23 @@ class MainWindow(QMainWindow):
self.settings.check_updates_on_start = checked
save_settings(self.settings)
def _ordered_games(self) -> list[Game]:
"""Jocs en l'ordre triat: alfabètic pel nom, o l'ordre original del games.toml."""
if self.settings.sort_order == SORT_NAME:
return sorted(self.config.games, key=lambda g: g.name.casefold())
return list(self.config.games)
def _populate_list(self) -> None:
"""(Re)col·loca les files al layout segons l'ordre triat, sense destruir-les."""
while self.list_layout.count():
item = self.list_layout.takeAt(0)
w = item.widget() if item else None
if w is not None:
w.setParent(None) # treu del layout però conserva la fila (viu a self.rows)
for game in self._ordered_games():
self.list_layout.addWidget(self.rows[game.id])
self.list_layout.addStretch(1)
def _apply_filter(self) -> None:
hide = self.action_hide.isChecked()
for row in self.rows.values():
@@ -463,7 +503,13 @@ class MainWindow(QMainWindow):
# --------------------------------------------------------------- helpers
def _log(self, text: str) -> None:
# Autoscroll intel·ligent: només seguim el final si la barra ja hi estava.
# Si l'usuari ha pujat a llegir una línia anterior, no l'arrosseguem avall.
bar = self.log_view.verticalScrollBar()
at_bottom = bar.value() >= bar.maximum() - 4
self.log_view.appendPlainText(text)
if at_bottom:
bar.setValue(bar.maximum())
# En mode auto, qualsevol línia desplega la consola; si no hi ha cap worker
# actiu (p.ex. un missatge solt), arrenca el compte enrere per replegar-la.
if self.settings.console_mode == CONSOLE_AUTO:
-3
View File
@@ -1,3 +0,0 @@
"""jlauncher — lanzador de juegos jailgames."""
__version__ = "1.0.2"
-1
View File
@@ -1 +0,0 @@
"""Componentes de interfaz de jlauncher."""
+4 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "jlauncher"
version = "1.0.1"
name = "jail-launcher"
version = "1.0.4"
description = "Lanzador de juegos jailgames: clona, compila y ejecuta repos Gitea"
requires-python = ">=3.11"
dependencies = [
@@ -8,11 +8,11 @@ dependencies = [
]
[project.scripts]
jlauncher = "jlauncher.__main__:main"
jail-launcher = "jail_launcher.__main__:main"
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["jlauncher", "jlauncher.ui"]
packages = ["jail_launcher", "jail_launcher.ui"]