mirror of
https://github.com/skoelle/marcer-gamedvd-launcher.git
synced 2026-09-18 03:00:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d3530b86 | ||
|
|
94b4ed7fb3 | ||
|
|
87720e6db4 | ||
|
|
d06ee381be | ||
|
|
ea66414496 | ||
|
|
7360687da7 | ||
|
|
ca2e46a8a6 | ||
|
|
a9e9aecf3e |
@@ -41,19 +41,20 @@ jobs:
|
|||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
|
working-directory: src/MarcerGameDvdLauncher
|
||||||
run: dotnet publish -c Release -r ${{ matrix.rid }} -p:Version=${{ steps.version.outputs.version }} --self-contained false
|
run: dotnet publish -c Release -r ${{ matrix.rid }} -p:Version=${{ steps.version.outputs.version }} --self-contained false
|
||||||
|
|
||||||
- name: Create ZIP (Windows)
|
- name: Create ZIP (Windows)
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Compress-Archive -Path "MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/*" -DestinationPath "MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip"
|
Compress-Archive -Path "src/MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/publish/*" -DestinationPath "MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip"
|
||||||
|
|
||||||
- name: Create ZIP (Linux/macOS)
|
- name: Create ZIP (Linux/macOS)
|
||||||
if: matrix.os != 'windows-latest'
|
if: matrix.os != 'windows-latest'
|
||||||
run: |
|
run: |
|
||||||
cd MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}
|
cd src/MarcerGameDvdLauncher/bin/Release/net10.0/${{ matrix.rid }}/publish
|
||||||
zip -r ../../../MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip .
|
zip -r $GITHUB_WORKSPACE/MarcerGameDvdLauncher-v${{ steps.version.outputs.version }}-${{ matrix.artifact_name }}.zip .
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -19,3 +19,6 @@ Thumbs.db
|
|||||||
# User-specific configuration (real config, not example)
|
# User-specific configuration (real config, not example)
|
||||||
launcher.config.json
|
launcher.config.json
|
||||||
favorites.txt
|
favorites.txt
|
||||||
|
|
||||||
|
# Demo directory (generated by demo.sh)
|
||||||
|
.demo/
|
||||||
|
|||||||
+27
-20
@@ -4,7 +4,7 @@ applyTo: '**'
|
|||||||
|
|
||||||
## Module Overview (Marcer GameDVD Launcher)
|
## Module Overview (Marcer GameDVD Launcher)
|
||||||
|
|
||||||
The implementation is split into focused modules (files) under the `MarcerGameDvdLauncher/` folder. Keep this section up to date when files are added, removed or responsibilities change.
|
The implementation is split into focused modules (files) under the `src/MarcerGameDvdLauncher/` folder. Keep this section up to date when files are added, removed or responsibilities change.
|
||||||
|
|
||||||
- MarcerGameDvdLauncher/Program.cs: Minimal entry point. Sets console title and starts the application by creating `LauncherApp`.
|
- MarcerGameDvdLauncher/Program.cs: Minimal entry point. Sets console title and starts the application by creating `LauncherApp`.
|
||||||
- MarcerGameDvdLauncher/LauncherApp.cs: Application lifecycle host — loads configuration, initializes components and runs the main directory navigation loop (contains `AppHost` internal class).
|
- MarcerGameDvdLauncher/LauncherApp.cs: Application lifecycle host — loads configuration, initializes components and runs the main directory navigation loop (contains `AppHost` internal class).
|
||||||
@@ -12,19 +12,19 @@ The implementation is split into focused modules (files) under the `MarcerGameDv
|
|||||||
- MarcerGameDvdLauncher/ProgramHelpers.cs: Small shared helpers (resolve relative paths, centralized console message helper) used across modules.
|
- MarcerGameDvdLauncher/ProgramHelpers.cs: Small shared helpers (resolve relative paths, centralized console message helper) used across modules.
|
||||||
- MarcerGameDvdLauncher/OverlayDirectoryBrowser.cs: Filesystem overlay and browsing logic — merges root and patch directories, enumerates folders and ZIPs, protects against path traversal and ensures navigation cannot leave the configured roots.
|
- MarcerGameDvdLauncher/OverlayDirectoryBrowser.cs: Filesystem overlay and browsing logic — merges root and patch directories, enumerates folders and ZIPs, protects against path traversal and ensures navigation cannot leave the configured roots.
|
||||||
- MarcerGameDvdLauncher/NavigationController.cs: Encapsulates selection, scrolling and relative-path navigation logic (cursor, page up/down, per-directory remembered selection/state).
|
- MarcerGameDvdLauncher/NavigationController.cs: Encapsulates selection, scrolling and relative-path navigation logic (cursor, page up/down, per-directory remembered selection/state).
|
||||||
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering and color selection according to overlay rules.
|
- MarcerGameDvdLauncher/MenuRenderer.cs: Console rendering logic — efficient per-line redraw, double-buffering, color selection according to overlay rules, and the help box overlay.
|
||||||
- MarcerGameDvdLauncher/HatariLauncher.cs: Responsible for validating the Hatari executable and starting Hatari with the configured argument template (replaces `{cfg}` and `{zip}`).
|
- MarcerGameDvdLauncher/HatariLauncher.cs: Responsible for validating the Hatari executable and starting Hatari with the configured argument template (replaces `{cfg}` and `{zip}`).
|
||||||
- MarcerGameDvdLauncher/UIErrorService.cs: Centralized UI error presentation using the console message helper.
|
- MarcerGameDvdLauncher/UIErrorService.cs: Centralized UI error presentation using the console message helper.
|
||||||
|
|
||||||
Note: This overview is intentionally concise. For behavioral changes (navigation, color scheme, launch command or config schema), update this file (agents.md) and README.md as required by project policy.
|
Note: This overview is intentionally concise. For behavioral changes (navigation, color scheme, launch command or config schema), update this file (AGENTS.md) and README.md as required by project policy.
|
||||||
**Note for Automated Tests/CI:**
|
**Note for Automated Tests/CI:**
|
||||||
The Launcher cannot be executed or tested via `start.cmd` from this environment (build system, automation agent) since no Windows console environment is present. For release workflows and developer validation, it is ALWAYS required to do a manual test run via start.cmd per documentation and policy before delivery.
|
The Launcher cannot be executed or tested via `scripts/start.cmd` from this environment (build system, automation agent) since no Windows console environment is present. For release workflows and developer validation, it is ALWAYS required to do a manual test run via scripts/start.cmd per documentation and policy before delivery.
|
||||||
|
|
||||||
**Release Process (automated via GitHub Actions):**
|
**Release Process (automated via GitHub Actions):**
|
||||||
- Pushing a tag (`v*`) triggers the GitHub Action workflow (`.github/workflows/release.yml`).
|
- Pushing a tag (`v*`) triggers the GitHub Action workflow (`.github/workflows/release.yml`).
|
||||||
- The workflow builds platform-specific artifacts (Windows, Linux, macOS), generates release notes from git log, and creates a GitHub Release with all ZIPs attached.
|
- The workflow builds platform-specific artifacts (Windows, Linux, macOS), generates release notes from git log, and creates a GitHub Release with all ZIPs attached.
|
||||||
- Developer steps for a release:
|
- Developer steps for a release:
|
||||||
1. Ensure `README.md` and `agents.md` are up to date.
|
1. Ensure `README.md` and `AGENTS.md` are up to date.
|
||||||
2. Commit all changes.
|
2. Commit all changes.
|
||||||
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
|
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
|
||||||
4. The GitHub Action handles the rest (build, ZIP, release notes, GitHub Release).
|
4. The GitHub Action handles the rest (build, ZIP, release notes, GitHub Release).
|
||||||
@@ -32,14 +32,14 @@ The Launcher cannot be executed or tested via `start.cmd` from this environment
|
|||||||
|
|
||||||
|
|
||||||
Additional policy:
|
Additional policy:
|
||||||
- README.md must be written in English. Any functional change that affects usage, configuration, or behavior MUST update README.md in English immediately after the change. If there are consequential changes to developer-facing policies, build steps, or requirements, `agents.md` must be updated as well.
|
- README.md must be written in English. Any functional change that affects usage, configuration, or behavior MUST update README.md in English immediately after the change. If there are consequential changes to developer-facing policies, build steps, or requirements, AGENTS.md must be updated as well.
|
||||||
|
|
||||||
Developer note: Visual Studio Solution
|
Developer note: Visual Studio Solution
|
||||||
- A Visual Studio solution file exists at the repository root: `marcer-gamedvd-launcher.sln`. Developers may open this solution in Visual Studio to work on the project, debug and build from the IDE. The solution references `MarcerGameDvdLauncher\MarcerGameDvdLauncher.csproj` and includes Debug and Release configurations. Use `build.cmd` (Windows) or `build.sh` (Linux) and `start.cmd` for consistent command-line builds/releases as described elsewhere in this document.
|
- A Visual Studio solution file exists at `src/marcer-gamedvd-launcher.sln`. Developers may open this solution in Visual Studio to work on the project, debug and build from the IDE. The solution references `MarcerGameDvdLauncher\MarcerGameDvdLauncher.csproj` and includes Debug and Release configurations. Use `scripts/build.cmd` (Windows) or `scripts/build.sh` (Linux) and `scripts/start.cmd` for consistent command-line builds/releases as described elsewhere in this document.
|
||||||
|
|
||||||
With this, it is ensured that binary/release files never end up in git, and the release process is always traceable and performed exclusively manually in the web interface.
|
With this, it is ensured that binary/release files never end up in git, and the release process is always traceable and performed exclusively manually in the web interface.
|
||||||
|
|
||||||
# Requirements for the Marcer GameDVD Launcher (agents.md)
|
# Requirements for the Marcer GameDVD Launcher (AGENTS.md)
|
||||||
|
|
||||||
## Basic Function / Purpose
|
## Basic Function / Purpose
|
||||||
The console launcher is meant for browsing a games directory and can launch ZIP files with the Hatari emulator under Windows. Control is exclusively via keyboard in the console window.
|
The console launcher is meant for browsing a games directory and can launch ZIP files with the Hatari emulator under Windows. Control is exclusively via keyboard in the console window.
|
||||||
@@ -57,6 +57,7 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
- Backspace: jump to parent directory (never outside root)
|
- Backspace: jump to parent directory (never outside root)
|
||||||
- ESC: exit the program
|
- ESC: exit the program
|
||||||
- PageUp/PageDown: jump by one page up/down through the file list
|
- PageUp/PageDown: jump by one page up/down through the file list
|
||||||
|
- `?`: show a help box with key bindings
|
||||||
- The file list always shows exactly as many lines as fit the screen – ALWAYS **one line less** than the console height (`Console.WindowHeight - 1`). This avoids overflow at the bottom and ensures the selection never enters the non-visible area.
|
- The file list always shows exactly as many lines as fit the screen – ALWAYS **one line less** than the console height (`Console.WindowHeight - 1`). This avoids overflow at the bottom and ensures the selection never enters the non-visible area.
|
||||||
Rationale: writing to the very last console line can cause the Windows console to auto-scroll or produce visual jumps when the cursor reaches the bottom row. Reserving one line prevents unintended scrolling/flicker and keeps the selection cursor strictly within the visible area.
|
Rationale: writing to the very last console line can cause the Windows console to auto-scroll or produce visual jumps when the cursor reaches the bottom row. Reserving one line prevents unintended scrolling/flicker and keeps the selection cursor strictly within the visible area.
|
||||||
Maintenance: when changing rendering or navigation logic, always compute the displayed page size as `availableLines = Console.WindowHeight - 1` and keep this value consistent across MenuRenderer, NavigationController and any other code that references the console height.
|
Maintenance: when changing rendering or navigation logic, always compute the displayed page size as `availableLines = Console.WindowHeight - 1` and keep this value consistent across MenuRenderer, NavigationController and any other code that references the console height.
|
||||||
@@ -75,17 +76,17 @@ The console launcher is meant for browsing a games directory and can launch ZIP
|
|||||||
- Empty directories must be displayed correctly (or reported correctly).
|
- Empty directories must be displayed correctly (or reported correctly).
|
||||||
- In the root directory, Backspace must have no effect (no error, do not leave the program).
|
- In the root directory, Backspace must have no effect (no error, do not leave the program).
|
||||||
- Navigation (Backspace, Enter, etc.) must remain robust even for very deep or large directory trees.
|
- Navigation (Backspace, Enter, etc.) must remain robust even for very deep or large directory trees.
|
||||||
- Hatari.Executable is validated during startup: the path is resolved (relative to the EXE directory when applicable) and must point to an existing .exe file. If validation fails the program must present a clear error and exit.
|
- Hatari.Executable is validated during startup: the path is resolved (relative to the EXE directory when applicable) and must point to an existing file. If validation fails the program must present a clear error and exit.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
- Optional: Build and start scripts (build.cmd / build.sh / start.cmd) are present, adapt as needed.
|
- Optional: Build and start scripts (`scripts/build.cmd` / `scripts/build.sh` / `scripts/start.cmd` / `scripts/start.sh`) are present, adapt as needed.
|
||||||
- For ALL builds, tests, and releases, ONLY the platform build script may be used: `build.cmd` (Windows) or `build.sh` (Linux). Direct `dotnet build`/`dotnet run` calls are NOT allowed, as they can lead to version/runtime conflicts. The application must always be started and tested using `start.cmd`.
|
- For ALL builds, tests, and releases, ONLY the platform build script may be used: `scripts/build.cmd` (Windows) or `scripts/build.sh` (Linux). Direct `dotnet build`/`dotnet run` calls are NOT allowed, as they can lead to version/runtime conflicts. The application must always be started and tested using `scripts/start.cmd`.
|
||||||
- After making any code changes that affect behavior or touch source files, run the platform build script (`build.cmd` on Windows, `build.sh` on Linux) and ensure the build completes successfully before committing. Additionally, perform a manual functional test using `start.cmd` on a Windows machine prior to pushing a release.
|
- After making any code changes that affect behavior or touch source files, run the platform build script (`scripts/build.cmd` on Windows, `scripts/build.sh` on Linux) and ensure the build completes successfully before committing. Additionally, perform a manual functional test using `scripts/start.cmd` on a Windows machine prior to pushing a release.
|
||||||
- The console window can have any number of lines; display/navigation must adapt dynamically.
|
- The console window can have any number of lines; display/navigation must adapt dynamically.
|
||||||
- After each build for a release, the entire build output directory (`bin/Release/net10.0/`) must be zipped in the `release/` directory, and the ZIP must be uploaded as a release asset in Gitea.
|
- After each build for a release, the entire build output directory (`bin/Release/net10.0/`) must be zipped in the `release/` directory, and the ZIP must be uploaded as a release asset in Gitea.
|
||||||
- For every release, a Release Notes file must be maintained that summarizes all changes, bugfixes, and new features in that version; Release Notes must be provided with the release asset.
|
- For every release, a Release Notes file must be maintained that summarizes all changes, bugfixes, and new features in that version; Release Notes must be provided with the release asset.
|
||||||
|
|
||||||
- **IMPORTANT:** With any functional change to the launcher, BOTH this file (agents.md) AND the README.md must always be updated and kept current. Immediately after, a successful build must be executed. This is mandatory for all development on the project.
|
- **IMPORTANT:** With any functional change to the launcher, BOTH this file (AGENTS.md) AND the README.md must always be updated and kept current. Immediately after, a successful build must be executed. This is mandatory for all development on the project.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -106,13 +107,19 @@ Note on PatchDirectory semantics:
|
|||||||
Implementation note (input flushing):
|
Implementation note (input flushing):
|
||||||
- To avoid undesired key-repeat / input "afterglow" when the user holds navigation keys, the application performs a best-effort flush of the console input buffer after navigation events. This is implemented by ProgramHelpers.FlushInputBuffer(), which uses the Win32 FlushConsoleInputBuffer API on Windows. This behaviour is intentional and required to provide a responsive navigation experience.
|
- To avoid undesired key-repeat / input "afterglow" when the user holds navigation keys, the application performs a best-effort flush of the console input buffer after navigation events. This is implemented by ProgramHelpers.FlushInputBuffer(), which uses the Win32 FlushConsoleInputBuffer API on Windows. This behaviour is intentional and required to provide a responsive navigation experience.
|
||||||
|
|
||||||
### Color Scheme
|
### Color Scheme and Layer Labels
|
||||||
- Folder in both layers: **ConsoleColor.Yellow**
|
Each entry is displayed with a left label indicating its layer status:
|
||||||
- Folder only in patch layer: **ConsoleColor.DarkYellow**
|
- **`[BOTH]`**: Entry exists in both main and patch layer
|
||||||
- Folder only in main layer: **ConsoleColor.Gray**
|
- **`[ROOT]`**: Entry exists only in main (root) layer
|
||||||
- ZIP in both layers: **ConsoleColor.Green**
|
- **`[PTCH]`**: Entry exists only in patch layer
|
||||||
- ZIP only in main layer: **ConsoleColor.DarkGreen**
|
|
||||||
- ZIP only in patch layer: **ConsoleColor.Magenta**
|
Color mapping:
|
||||||
|
- Folder in both layers: **ConsoleColor.Yellow** (`[BOTH]`)
|
||||||
|
- Folder only in patch layer: **ConsoleColor.DarkYellow** (`[PTCH]`)
|
||||||
|
- Folder only in main layer: **ConsoleColor.Gray** (`[ROOT]`)
|
||||||
|
- ZIP in both layers: **ConsoleColor.Green** (`[BOTH]`)
|
||||||
|
- ZIP only in main layer: **ConsoleColor.DarkGreen** (`[ROOT]`)
|
||||||
|
- ZIP only in patch layer: **ConsoleColor.Magenta** (`[PTCH]`)
|
||||||
|
|
||||||
Note: The ConsoleColor mapping above is authoritative for the application. If you change color values in code (MenuRenderer/GetColorForEntry), update this section to keep documentation and implementation in sync.
|
Note: The ConsoleColor mapping above is authoritative for the application. If you change color values in code (MenuRenderer/GetColorForEntry), update this section to keep documentation and implementation in sync.
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Ziel: Das Repo aufräumen (Doku, Code, Config)
|
|||||||
- [ ] `MenuRenderer` bekommt optional passende `AppColorConfig` (Konstruktor-Injection);
|
- [ ] `MenuRenderer` bekommt optional passende `AppColorConfig` (Konstruktor-Injection);
|
||||||
`GetColorForEntry`/`GetColors` nutzen Config statt Konstanten.
|
`GetColorForEntry`/`GetColors` nutzen Config statt Konstanten.
|
||||||
- [ ] Falls Config-Werte fehlen → heutiges Verhalten beibehalten (fallback).
|
- [ ] Falls Config-Werte fehlen → heutiges Verhalten beibehalten (fallback).
|
||||||
- [ ] Doku synchronisieren: README + agents.md Farbtabellen auf Config-Felder verlinken.
|
- [ ] Doku synchronisieren: README + AGENTS.md Farbtabellen auf Config-Felder verlinken.
|
||||||
- [ ] Beispielwerte ins `launcher.config.example.json` aufnehmen.
|
- [ ] Beispielwerte ins `launcher.config.example.json` aufnehmen.
|
||||||
|
|
||||||
### 2b. Benutzer-Config (User-Config) – entschieden
|
### 2b. Benutzer-Config (User-Config) – entschieden
|
||||||
@@ -94,4 +94,4 @@ externe Anpassungsmöglichkeit dafür.
|
|||||||
## 4. Abschlusskriterien
|
## 4. Abschlusskriterien
|
||||||
|
|
||||||
- [ ] Commit mit aussagekräftiger Message (nur echter Autor, kein Co-Author).
|
- [ ] Commit mit aussagekräftiger Message (nur echter Autor, kein Co-Author).
|
||||||
- [ ] Nach Doku- und Code-Änderungen: `build.cmd` (Windows) bzw. `build.sh` (Linux) läuft fehlerfrei.
|
- [ ] Nach Doku- und Code-Änderungen: `scripts/build.cmd` (Windows) bzw. `scripts/build.sh` (Linux) läuft fehlerfrei.
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ A performant, consistent console launcher for the Hatari emulator on Windows. Co
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- **Overlay/Patch Union:** Recursively merges main and patch directory at every level. Each object/name is shown only once (patch takes precedence).
|
- **Overlay/Patch Union:** Recursively merges main and patch directory at every level. Each object/name is shown only once (patch takes precedence).
|
||||||
- **Dynamic, practical color scheme:**
|
- **Dynamic, practical color scheme with layer labels:**
|
||||||
|
|
||||||
| Entry | ConsoleColor | Meaning
|
| Entry | Label | ConsoleColor | Meaning
|
||||||
|------------------------|--------------|---------------------------------------------|
|
|------------------------|-----------|--------------|---------------------------------------------|
|
||||||
| Folder in both | ConsoleColor.Yellow | Directory in both layers (patch/main)
|
| Folder in both | `[BOTH]` | Yellow | Directory in both layers (patch/main)
|
||||||
| Patch-only folder | ConsoleColor.DarkYellow | Directory only in patch layer
|
| Patch-only folder | `[PTCH]` | DarkYellow | Directory only in patch layer
|
||||||
| Main-only folder | ConsoleColor.Gray | Directory only in main layer
|
| Main-only folder | `[ROOT]` | Gray | Directory only in main layer
|
||||||
| ZIP in both | ConsoleColor.Green | ZIP archive in both layers
|
| ZIP in both | `[BOTH]` | Green | ZIP archive in both layers
|
||||||
| Main-only ZIP | ConsoleColor.DarkGreen | ZIP archive only in main layer
|
| Main-only ZIP | `[ROOT]` | DarkGreen | ZIP archive only in main layer
|
||||||
| Patch-only ZIP | ConsoleColor.Magenta | ZIP archive only in patch layer
|
| Patch-only ZIP | `[PTCH]` | Magenta | ZIP archive only in patch layer
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -57,10 +57,14 @@ A performant, consistent console launcher for the Hatari emulator on Windows. Co
|
|||||||
|
|
||||||
- **Consistent navigation & controls:**
|
- **Consistent navigation & controls:**
|
||||||
- Arrow up/down: move selection (always visible)
|
- Arrow up/down: move selection (always visible)
|
||||||
|
- Arrow right: open folder / launch ZIP with Hatari (same as Enter)
|
||||||
|
- Arrow left: exactly one level up (same as Backspace)
|
||||||
- Enter: open folder / launch ZIP with Hatari (patch variant always preferred if present)
|
- Enter: open folder / launch ZIP with Hatari (patch variant always preferred if present)
|
||||||
- Backspace: exactly one level up (never exceeds root)
|
- Backspace: exactly one level up (never exceeds root)
|
||||||
- ESC: exit the program immediately
|
- ESC or Q: exit the program immediately
|
||||||
- PageUp/PageDown: jump exactly one screen full (window height - 1)
|
- PageUp/PageDown: jump exactly one screen full (window height - 1)
|
||||||
|
- `*`: toggle favorite on selected ZIP
|
||||||
|
- `?`: show a help box with key bindings
|
||||||
- Display always one line less than console height; no overflow/cut-off
|
- Display always one line less than console height; no overflow/cut-off
|
||||||
- **Cursor position saving per directory:**
|
- **Cursor position saving per directory:**
|
||||||
- The last position/selection of each directory is retained, even after Backspace
|
- The last position/selection of each directory is retained, even after Backspace
|
||||||
@@ -72,16 +76,15 @@ A performant, consistent console launcher for the Hatari emulator on Windows. Co
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
1. Edit `launcher.config.example.json` to set your `RootDirectory`, optional `PatchDirectory` and the `Hatari` settings, then copy it to `launcher.config.json` for local use. Relative paths are resolved against the EXE folder (build output).
|
1. Edit `launcher.config.example.json` to set your `RootDirectory`, optional `PatchDirectory` and the `Hatari` settings, then copy it to `launcher.config.json` for local use. Relative paths are resolved against the EXE folder (build output).
|
||||||
2. **Windows:** Build via `build.cmd`.
|
2. **Windows:** Build via `scripts/build.cmd`, start via `scripts/start.cmd`.
|
||||||
3. **Linux:** Build via `build.sh` (run `chmod +x build.sh` first to make it executable).
|
3. **Linux/macOS:** Build via `scripts/build.sh` (run `chmod +x scripts/*.sh` first to make it executable), start via `scripts/start.sh`.
|
||||||
4. **Always start using `start.cmd`.**
|
4. In the console, all subfolders and ZIPs in root (and recursively below) will be shown; other file types/hidden files are always ignored.
|
||||||
5. In the console, all subfolders and ZIPs in root (and recursively below) will be shown; other file types/hidden files are always ignored.
|
5. Complete navigation/control with arrow keys, Enter, Backspace, ESC, PgUp/PgDn, as described above.
|
||||||
6. Complete navigation/control with arrow keys, Enter, Backspace, ESC, PgUp/PgDn, as described above.
|
6. **IMPORTANT:** Navigation/scroll/backspace:
|
||||||
7. **IMPORTANT:** Navigation/scroll/backspace:
|
|
||||||
- Backspace never escapes the root
|
- Backspace never escapes the root
|
||||||
- In root, Backspace has no effect
|
- In root, Backspace has no effect
|
||||||
- Empty directories are reported (display stays stable)
|
- Empty directories are reported (display stays stable)
|
||||||
8. **Overlay/patch logic:**
|
7. **Overlay/patch logic:**
|
||||||
- If a ZIP/folder exists in both patch and main, always the patch version opens/launches
|
- If a ZIP/folder exists in both patch and main, always the patch version opens/launches
|
||||||
- All navigation is relative to root path—for consistent experience
|
- All navigation is relative to root path—for consistent experience
|
||||||
|
|
||||||
@@ -110,13 +113,13 @@ Example `launcher.config.example.json`:
|
|||||||
Fields:
|
Fields:
|
||||||
- RootDirectory: Absolute (or relative) path to the game root. Navigation must never leave this root directory.
|
- RootDirectory: Absolute (or relative) path to the game root. Navigation must never leave this root directory.
|
||||||
- PatchDirectory: Optional overlay/patch directory (merged with the main root at runtime).
|
- PatchDirectory: Optional overlay/patch directory (merged with the main root at runtime).
|
||||||
- Hatari.Executable: Full path to `hatari.exe`.
|
- Hatari.Executable: Full path to the Hatari executable.
|
||||||
- Hatari.ConfigFile: Full path to the Hatari configuration file.
|
- Hatari.ConfigFile: Full path to the Hatari configuration file.
|
||||||
- Hatari.ArgsTemplate: Argument template used to start Hatari. Use `{cfg}` for the Hatari config file path and `{zip}` for the ZIP file to launch.
|
- Hatari.ArgsTemplate: Argument template used to start Hatari. Use `{cfg}` for the Hatari config file path and `{zip}` for the ZIP file to launch.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Relative paths are resolved relative to the EXE directory (AppContext.BaseDirectory). This makes behavior consistent when running from the build output folder.
|
- Relative paths are resolved relative to the EXE directory (AppContext.BaseDirectory). This makes behavior consistent when running from the build output folder.
|
||||||
- Hatari.Executable is validated at startup: the file must exist and have an .exe extension. Relative paths for Hatari settings are resolved against the EXE folder.
|
- Hatari.Executable is validated at startup: the file must exist. Relative paths for Hatari settings are resolved against the EXE folder.
|
||||||
- `Hatari.ArgsTemplate` must contain at least the `{zip}` placeholder. Example: `-c "{cfg}" --disk-a "{zip}"`.
|
- `Hatari.ArgsTemplate` must contain at least the `{zip}` placeholder. Example: `-c "{cfg}" --disk-a "{zip}"`.
|
||||||
- The program performs a straight string substitution of `{cfg}` and `{zip}`; it does not add additional quoting logic. Therefore include quotes around placeholders in the template if your paths contain spaces (as in the example).
|
- The program performs a straight string substitution of `{cfg}` and `{zip}`; it does not add additional quoting logic. Therefore include quotes around placeholders in the template if your paths contain spaces (as in the example).
|
||||||
- `launcher.config.example.json` is copied to the output directory by the csproj (`CopyToOutputDirectory=PreserveNewest`).
|
- `launcher.config.example.json` is copied to the output directory by the csproj (`CopyToOutputDirectory=PreserveNewest`).
|
||||||
@@ -130,16 +133,20 @@ Releases are automated via GitHub Actions. When a tag matching `v*` is pushed, t
|
|||||||
3. Creates a GitHub Release with all ZIPs attached
|
3. Creates a GitHub Release with all ZIPs attached
|
||||||
|
|
||||||
**To create a release:**
|
**To create a release:**
|
||||||
1. Ensure `README.md` and `agents.md` are up to date.
|
1. Ensure `README.md` and `AGENTS.md` are up to date.
|
||||||
2. Commit all changes.
|
2. Commit all changes.
|
||||||
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
|
3. Create and push a tag: `git tag v{version} && git push origin v{version}`.
|
||||||
4. The GitHub Action handles the rest.
|
4. The GitHub Action handles the rest.
|
||||||
|
|
||||||
**Local builds** (for development/testing):
|
**Local builds** (for development/testing):
|
||||||
- **Windows:** `build.cmd`
|
- **Windows:** `scripts/build.cmd` to build, `scripts/start.cmd` to run
|
||||||
- **Linux/macOS:** `build.sh` (run `chmod +x build.sh` first)
|
- **Linux/macOS:** `scripts/build.sh` to build, `scripts/start.sh` to run (run `chmod +x scripts/*.sh` first)
|
||||||
|
|
||||||
|
## Community
|
||||||
|
|
||||||
|
This launcher was built for the [Marcer GameDVD](https://www.facebook.com/groups/360493904888475/) community on Facebook. If you have questions, suggestions, or want to discuss Hatari and Atari ST gaming, join the group!
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- Full requirements, features and build rules are always up to date in `agents.md`.
|
- Full requirements, features and build rules are always up to date in `AGENTS.md`.
|
||||||
- After every code or feature change and every release, README.md and agents.md must be reviewed and kept up to date.
|
- After every code or feature change and every release, README.md and AGENTS.md must be reviewed and kept up to date.
|
||||||
- For every release, release notes **must** be present listing all changes and bugfixes; this is required by agents.md!
|
- For every release, release notes **must** be present listing all changes and bugfixes; this is required by AGENTS.md!
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# === Build script for MarcerGameDvdLauncher (requires .NET SDK 6 or newer) ===
|
|
||||||
echo "Building MarcerGameDvdLauncher..."
|
|
||||||
|
|
||||||
if ! command -v dotnet &> /dev/null; then
|
|
||||||
echo "[ERROR] .NET SDK not found. Please install from https://dotnet.microsoft.com/download"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build in current directory (where build.sh is located)
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Change to MarcerGameDvdLauncher subdirectory
|
|
||||||
cd "MarcerGameDvdLauncher"
|
|
||||||
|
|
||||||
dotnet build -c Release
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "[ERROR] Build failed!"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check for the built .exe file
|
|
||||||
EXEPATH=$(find "bin/Release" -name "MarcerGameDvdLauncher*.exe" -print -quit 2>/dev/null)
|
|
||||||
if [ -n "$EXEPATH" ] && [ -f "$EXEPATH" ]; then
|
|
||||||
echo "[OK] Build complete. EXE: \"$EXEPATH\""
|
|
||||||
else
|
|
||||||
echo "[WARNING] Build appears successful but .exe not found!"
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# === Demo setup and run script for MarcerGameDvdLauncher ===
|
||||||
|
# Creates a structured test directory with main and patch layers,
|
||||||
|
# populates them with fake ZIPs, then launches the launcher.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
DEMO_DIR="$SCRIPT_DIR/.demo"
|
||||||
|
ROOT_DIR="$DEMO_DIR/root"
|
||||||
|
PATCH_DIR="$DEMO_DIR/patch"
|
||||||
|
CONFIG_FILE="$SCRIPT_DIR/launcher.config.json"
|
||||||
|
|
||||||
|
# --- Step 1: Build ---
|
||||||
|
echo "=== Building MarcerGameDvdLauncher ==="
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
dotnet build -c Release --verbosity quiet
|
||||||
|
echo "[OK] Build successful."
|
||||||
|
|
||||||
|
# --- Step 2: Create demo directory structure ---
|
||||||
|
echo ""
|
||||||
|
echo "=== Setting up demo directories ==="
|
||||||
|
|
||||||
|
# Clean previous demo if it exists
|
||||||
|
rm -rf "$DEMO_DIR"
|
||||||
|
mkdir -p "$ROOT_DIR" "$PATCH_DIR"
|
||||||
|
|
||||||
|
# Helper: create a fake ZIP (just an empty file with .zip extension)
|
||||||
|
fake_zip() {
|
||||||
|
touch "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- ROOT layer (main DVD content) ---
|
||||||
|
echo "Creating root layer..."
|
||||||
|
|
||||||
|
# === Folder A: TEST-ME — Atari Classics (shared with patch — rich mix + subdirs) ===
|
||||||
|
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Pac-Man.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Donkey Kong.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Galaga.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Space Invaders.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Frogger.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Bomberman.zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Tetris.zip"
|
||||||
|
|
||||||
|
# Subdirs: Original and Manual
|
||||||
|
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics/Original"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Original/Pac-Man (Original).zip"
|
||||||
|
fake_zip "$ROOT_DIR/A/TEST-ME Atari Classics/Original/Donkey Kong (Original).zip"
|
||||||
|
mkdir -p "$ROOT_DIR/A/TEST-ME Atari Classics/Manual"
|
||||||
|
touch "$ROOT_DIR/A/TEST-ME Atari Classics/Manual/Pac-Man.pdf" # PDF — should NOT appear
|
||||||
|
touch "$ROOT_DIR/A/TEST-ME Atari Classics/Manual/Donkey Kong.pdf" # PDF — should NOT appear
|
||||||
|
|
||||||
|
# === Folder B: Platformer (root only) ===
|
||||||
|
mkdir -p "$ROOT_DIR/B/Platformer"
|
||||||
|
fake_zip "$ROOT_DIR/B/Platformer/Super Mario Bros.zip"
|
||||||
|
fake_zip "$ROOT_DIR/B/Platformer/Sonic the Hedgehog.zip"
|
||||||
|
fake_zip "$ROOT_DIR/B/Platformer/Mega Man.zip"
|
||||||
|
fake_zip "$ROOT_DIR/B/Platformer/Castlevania.zip"
|
||||||
|
|
||||||
|
# === Folder C: Puzzle (root only) ===
|
||||||
|
mkdir -p "$ROOT_DIR/C/Puzzle"
|
||||||
|
fake_zip "$ROOT_DIR/C/Puzzle/Columns.zip"
|
||||||
|
fake_zip "$ROOT_DIR/C/Puzzle/Puyo Puyo.zip"
|
||||||
|
fake_zip "$ROOT_DIR/C/Puzzle/Klax.zip"
|
||||||
|
|
||||||
|
# === Folder D: Shoot'em'up (shared with patch — rich mix) ===
|
||||||
|
mkdir -p "$ROOT_DIR/D/Shoot'em'up"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/R-Type.zip"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/Gradius.zip"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/1942.zip"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/Defender.zip"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/Centipede.zip"
|
||||||
|
fake_zip "$ROOT_DIR/D/Shoot'em'up/Galaxian.zip"
|
||||||
|
|
||||||
|
# === Folder E: Racing (root only) ===
|
||||||
|
mkdir -p "$ROOT_DIR/E/Racing"
|
||||||
|
fake_zip "$ROOT_DIR/E/Racing/Pole Position.zip"
|
||||||
|
fake_zip "$ROOT_DIR/E/Racing/Out Run.zip"
|
||||||
|
fake_zip "$ROOT_DIR/E/Racing/Daytona USA.zip"
|
||||||
|
|
||||||
|
# --- PATCH layer (overlay/additions) ---
|
||||||
|
echo "Creating patch layer..."
|
||||||
|
|
||||||
|
# === Folder A: TEST-ME — Atari Classics — overrides + new games ===
|
||||||
|
mkdir -p "$PATCH_DIR/A/TEST-ME Atari Classics"
|
||||||
|
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Pac-Man.zip" # [BOTH] override
|
||||||
|
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Donkey Kong.zip" # [BOTH] override
|
||||||
|
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Pac-Man Championship.zip" # [PTCH] new
|
||||||
|
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Donkey Kong Jr.zip" # [PTCH] new
|
||||||
|
|
||||||
|
# Subdir Original in patch — adds one more
|
||||||
|
mkdir -p "$PATCH_DIR/A/TEST-ME Atari Classics/Original"
|
||||||
|
fake_zip "$PATCH_DIR/A/TEST-ME Atari Classics/Original/Galaga (Original).zip" # [PTCH] in subdir
|
||||||
|
|
||||||
|
# === Folder D: Shoot'em'up — overrides + new games ===
|
||||||
|
mkdir -p "$PATCH_DIR/D/Shoot'em'up"
|
||||||
|
fake_zip "$PATCH_DIR/D/Shoot'em'up/R-Type.zip" # [BOTH] override
|
||||||
|
fake_zip "$PATCH_DIR/D/Shoot'em'up/R-Type II.zip" # [PTCH] new
|
||||||
|
fake_zip "$PATCH_DIR/D/Shoot'em'up/Salamander.zip" # [PTCH] new
|
||||||
|
|
||||||
|
# === Folder F: Patch-only folder (not in root) ===
|
||||||
|
mkdir -p "$PATCH_DIR/F/Hack & Translation"
|
||||||
|
fake_zip "$PATCH_DIR/F/Hack & Translation/Pac-Man MSX.zip"
|
||||||
|
fake_zip "$PATCH_DIR/F/Hack & Translation/Donkey Kong Remix.zip"
|
||||||
|
fake_zip "$PATCH_DIR/F/Hack & Translation/Galaga Special.zip"
|
||||||
|
|
||||||
|
echo "[OK] Demo structure created."
|
||||||
|
echo ""
|
||||||
|
echo " ROOT (DVD) PATCH (Overlay)"
|
||||||
|
echo " ────────── ───────────────"
|
||||||
|
echo " A/TEST-ME Atari Classics/ A/TEST-ME Atari Classics/"
|
||||||
|
echo " ├── Pac-Man.zip [BOTH] ├── Pac-Man.zip"
|
||||||
|
echo " ├── Donkey Kong.zip [BOTH] ├── Donkey Kong.zip"
|
||||||
|
echo " ├── Galaga.zip [ROOT] ├── Pac-Man Championship.zip [PTCH]"
|
||||||
|
echo " ├── Space Invaders.zip [ROOT] ├── Donkey Kong Jr.zip [PTCH]"
|
||||||
|
echo " ├── Frogger.zip [ROOT] │"
|
||||||
|
echo " ├── Bomberman.zip [ROOT] └── Original/"
|
||||||
|
echo " ├── Tetris.zip [ROOT] └── Galaga (Original).zip [PTCH]"
|
||||||
|
echo " ├── Original/"
|
||||||
|
echo " │ ├── Pac-Man (Original).zip [ROOT]"
|
||||||
|
echo " │ └── Donkey Kong (Original).zip [ROOT]"
|
||||||
|
echo " └── Manual/ (PDFs — should NOT appear)"
|
||||||
|
echo " ├── Pac-Man.pdf"
|
||||||
|
echo " └── Donkey Kong.pdf"
|
||||||
|
echo " B/Platformer/ (no patch)"
|
||||||
|
echo " ├── Super Mario Bros.zip [ROOT]"
|
||||||
|
echo " ├── Sonic.zip [ROOT]"
|
||||||
|
echo " ├── Mega Man.zip [ROOT]"
|
||||||
|
echo " └── Castlevania.zip [ROOT]"
|
||||||
|
echo " C/Puzzle/ (no patch)"
|
||||||
|
echo " ├── Columns.zip [ROOT]"
|
||||||
|
echo " ├── Puyo Puyo.zip [ROOT]"
|
||||||
|
echo " └── Klax.zip [ROOT]"
|
||||||
|
echo " D/Shoot'em'up/ D/Shoot'em'up/"
|
||||||
|
echo " ├── R-Type.zip [BOTH] ├── R-Type.zip"
|
||||||
|
echo " ├── Gradius.zip [ROOT] ├── R-Type II.zip [PTCH]"
|
||||||
|
echo " ├── 1942.zip [ROOT] └── Salamander.zip [PTCH]"
|
||||||
|
echo " ├── Defender.zip [ROOT]"
|
||||||
|
echo " ├── Centipede.zip [ROOT]"
|
||||||
|
echo " └── Galaxian.zip [ROOT]"
|
||||||
|
echo " E/Racing/ (no patch)"
|
||||||
|
echo " ├── Pole Position.zip [ROOT]"
|
||||||
|
echo " ├── Out Run.zip [ROOT]"
|
||||||
|
echo " └── Daytona USA.zip [ROOT]"
|
||||||
|
echo " F/Hack & Translation/ [PTCH]"
|
||||||
|
echo " ├── Pac-Man MSX.zip"
|
||||||
|
echo " ├── DK Remix.zip"
|
||||||
|
echo " └── Galaga Special.zip"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# --- Step 3: Create launcher.config.json ---
|
||||||
|
echo "=== Writing launcher.config.json ==="
|
||||||
|
|
||||||
|
# Create a fake Hatari executable for demo (Linux: shell script with .exe extension)
|
||||||
|
HATARI_FAKE="$DEMO_DIR/hatari.exe"
|
||||||
|
cat > "$HATARI_FAKE" <<'HATEXEC'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "[DEMO] Hatari would launch with: $@"
|
||||||
|
HATEXEC
|
||||||
|
chmod +x "$HATARI_FAKE"
|
||||||
|
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"RootDirectory": "$ROOT_DIR",
|
||||||
|
"PatchDirectory": "$PATCH_DIR",
|
||||||
|
"Hatari": {
|
||||||
|
"Executable": "$HATARI_FAKE",
|
||||||
|
"ConfigFile": "",
|
||||||
|
"ArgsTemplate": "{zip}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo "[OK] Config written to $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Copy config to EXE output directory (app looks for it there)
|
||||||
|
EXE_DIR="$SCRIPT_DIR/src/MarcerGameDvdLauncher/bin/Release/net10.0"
|
||||||
|
cp "$CONFIG_FILE" "$EXE_DIR/launcher.config.json"
|
||||||
|
echo "[OK] Config copied to $EXE_DIR"
|
||||||
|
|
||||||
|
# --- Step 4: Launch the application ---
|
||||||
|
echo ""
|
||||||
|
echo "=== Launching MarcerGameDvdLauncher ==="
|
||||||
|
echo "Controls: Arrow keys, Enter, Backspace, ESC to exit"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
dotnet run --project src/MarcerGameDvdLauncher -c Release
|
||||||
@@ -8,9 +8,9 @@ if errorlevel 1 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
REM Im aktuellen Ordner (wo build.cmd liegt) bauen
|
REM Im src-Ordner bauen (build.cmd liegt in scripts/)
|
||||||
cd /d %~dp0
|
cd /d %~dp0
|
||||||
cd MarcerGameDvdLauncher
|
cd ..\src\MarcerGameDvdLauncher
|
||||||
|
|
||||||
dotnet build -c Release
|
dotnet build -c Release
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# === Build script for MarcerGameDvdLauncher (requires .NET SDK 6 or newer) ===
|
||||||
|
echo "Building MarcerGameDvdLauncher..."
|
||||||
|
|
||||||
|
if ! command -v dotnet &> /dev/null; then
|
||||||
|
echo "[ERROR] .NET SDK not found. Please install from https://dotnet.microsoft.com/download"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build in MarcerGameDvdLauncher subdirectory (script is in scripts/, code in src/)
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
cd "../src/MarcerGameDvdLauncher"
|
||||||
|
|
||||||
|
dotnet build -c Release
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "[ERROR] Build failed!"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for the built binary
|
||||||
|
BINPATH=$(find "bin/Release" \( -name "MarcerGameDvdLauncher" -o -name "MarcerGameDvdLauncher.exe" \) -print -quit 2>/dev/null)
|
||||||
|
if [ -n "$BINPATH" ] && [ -f "$BINPATH" ]; then
|
||||||
|
echo "[OK] Build complete. Binary: \"$BINPATH\""
|
||||||
|
else
|
||||||
|
echo "[WARNING] Build appears successful but binary not found!"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
|
||||||
|
REM Starts MarcerGameDvdLauncher.exe (script is in scripts/, code in src/)
|
||||||
|
setlocal
|
||||||
|
set EXE_PATH=%~dp0..\src\MarcerGameDvdLauncher\bin\Release\net10.0\MarcerGameDvdLauncher.exe
|
||||||
|
|
||||||
|
if not exist "%EXE_PATH%" (
|
||||||
|
echo [ERROR] Application not built. Please run build.cmd first.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
pushd "%~dp0..\src\MarcerGameDvdLauncher\bin\Release\net10.0"
|
||||||
|
"MarcerGameDvdLauncher.exe"
|
||||||
|
popd
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Starts MarcerGameDvdLauncher (script is in scripts/, code in src/)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
EXE_PATH="$SCRIPT_DIR/../src/MarcerGameDvdLauncher/bin/Release/net10.0/MarcerGameDvdLauncher"
|
||||||
|
|
||||||
|
if [ ! -f "$EXE_PATH" ]; then
|
||||||
|
echo "[ERROR] Application not built. Please run build.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR/../src/MarcerGameDvdLauncher/bin/Release/net10.0"
|
||||||
|
./MarcerGameDvdLauncher
|
||||||
+1
-3
@@ -11,11 +11,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
if (string.IsNullOrWhiteSpace(exePath))
|
if (string.IsNullOrWhiteSpace(exePath))
|
||||||
throw new ArgumentNullException(nameof(exePath));
|
throw new ArgumentNullException(nameof(exePath));
|
||||||
|
|
||||||
// Defensive validation: ensure the executable exists and looks like an .exe
|
// Defensive validation: ensure the executable exists
|
||||||
if (!File.Exists(exePath))
|
if (!File.Exists(exePath))
|
||||||
throw new ArgumentException($"Hatari executable not found: {exePath}", nameof(exePath));
|
throw new ArgumentException($"Hatari executable not found: {exePath}", nameof(exePath));
|
||||||
if (!string.Equals(Path.GetExtension(exePath), ".exe", StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new ArgumentException($"Hatari executable must be an .exe file: {exePath}", nameof(exePath));
|
|
||||||
|
|
||||||
_exePath = exePath;
|
_exePath = exePath;
|
||||||
_cfgPath = cfgPath;
|
_cfgPath = cfgPath;
|
||||||
@@ -142,6 +142,15 @@ namespace MarcerGameDvdLauncher
|
|||||||
}
|
}
|
||||||
|
|
||||||
var key = Console.ReadKey(intercept: true);
|
var key = Console.ReadKey(intercept: true);
|
||||||
|
if (key.KeyChar == '?')
|
||||||
|
{
|
||||||
|
_menuRenderer.ShowHelpBox(currentAvailableLines);
|
||||||
|
Console.ReadKey(intercept: true);
|
||||||
|
_menuRenderer.InvalidateCache();
|
||||||
|
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset, _navigationController.SelectedIndex, currentAvailableLines, isFav);
|
||||||
|
ProgramHelpers.FlushInputBuffer();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
switch (key.Key)
|
switch (key.Key)
|
||||||
{
|
{
|
||||||
case ConsoleKey.UpArrow:
|
case ConsoleKey.UpArrow:
|
||||||
@@ -174,6 +183,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.Enter:
|
case ConsoleKey.Enter:
|
||||||
|
case ConsoleKey.RightArrow:
|
||||||
var oldRelativePath = _navigationController.CurrentRelativePath;
|
var oldRelativePath = _navigationController.CurrentRelativePath;
|
||||||
var isDirectory = _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
var isDirectory = _gameEntries.Count > 0 && _gameEntries[_navigationController.SelectedIndex].Kind == EntryKind.Directory;
|
||||||
_navigationController.HandleEnter(_gameEntries);
|
_navigationController.HandleEnter(_gameEntries);
|
||||||
@@ -199,11 +209,11 @@ namespace MarcerGameDvdLauncher
|
|||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.Backspace:
|
case ConsoleKey.Backspace:
|
||||||
|
case ConsoleKey.LeftArrow:
|
||||||
_navigationController.GoUpDirectory();
|
_navigationController.GoUpDirectory();
|
||||||
ReloadGameEntries();
|
ReloadGameEntries();
|
||||||
_navigationController.UpdateScrollOffset(_gameEntries.Count, currentAvailableLines);
|
_navigationController.UpdateScrollOffset(_gameEntries.Count, currentAvailableLines);
|
||||||
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset, _navigationController.SelectedIndex, currentAvailableLines, isFav);
|
_menuRenderer.DrawMenu(_gameEntries, _navigationController.ScrollOffset, _navigationController.SelectedIndex, currentAvailableLines, isFav);
|
||||||
// flush input to avoid leftover key events after directory change
|
|
||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.PageDown:
|
case ConsoleKey.PageDown:
|
||||||
@@ -237,6 +247,7 @@ namespace MarcerGameDvdLauncher
|
|||||||
ProgramHelpers.FlushInputBuffer();
|
ProgramHelpers.FlushInputBuffer();
|
||||||
break;
|
break;
|
||||||
case ConsoleKey.Escape:
|
case ConsoleKey.Escape:
|
||||||
|
case ConsoleKey.Q:
|
||||||
exitRequested = true;
|
exitRequested = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
+120
-15
@@ -139,6 +139,93 @@ namespace MarcerGameDvdLauncher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidates the internal line cache so the next DrawMenu call
|
||||||
|
// performs a full redraw of every line. Useful after an overlay
|
||||||
|
// (e.g. help box) has overwritten the console directly.
|
||||||
|
public void InvalidateCache()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _cachedBuffer.Length; i++)
|
||||||
|
_cachedBuffer[i].Text = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a centered, bordered help box with key bindings inside the
|
||||||
|
// available console area. The caller is responsible for waiting on a
|
||||||
|
// key and redrawing the menu afterwards.
|
||||||
|
public void ShowHelpBox(int availableLines)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int width = Console.WindowWidth;
|
||||||
|
string[] helpLines = GetHelpLines();
|
||||||
|
int boxHeight = Math.Min(helpLines.Length + 2, Math.Max(3, availableLines));
|
||||||
|
int boxWidth = Math.Max(1, width);
|
||||||
|
int topRow = Math.Max(0, (availableLines - boxHeight) / 2);
|
||||||
|
|
||||||
|
Console.BackgroundColor = ConsoleColor.DarkGray;
|
||||||
|
Console.ForegroundColor = ConsoleColor.White;
|
||||||
|
|
||||||
|
string topBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
||||||
|
Console.SetCursorPosition(0, topRow);
|
||||||
|
Console.Write(topBorder);
|
||||||
|
|
||||||
|
for (int i = 0; i < boxHeight - 2; i++)
|
||||||
|
{
|
||||||
|
int row = topRow + 1 + i;
|
||||||
|
string content;
|
||||||
|
if (i < helpLines.Length)
|
||||||
|
{
|
||||||
|
content = PadToWidth(helpLines[i], boxWidth - 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
content = new string(' ', Math.Max(0, boxWidth - 2));
|
||||||
|
}
|
||||||
|
Console.SetCursorPosition(0, row);
|
||||||
|
Console.Write("|" + content + "|");
|
||||||
|
}
|
||||||
|
|
||||||
|
int bottomRow = topRow + boxHeight - 1;
|
||||||
|
if (bottomRow < Console.WindowHeight)
|
||||||
|
{
|
||||||
|
string bottomBorder = "+" + new string('-', Math.Max(0, boxWidth - 2)) + "+";
|
||||||
|
Console.SetCursorPosition(0, bottomRow);
|
||||||
|
Console.Write(bottomBorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.ResetColor();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetHelpLines()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
" Help — Key Bindings",
|
||||||
|
" ",
|
||||||
|
" ↑ / ↓ Move selection up / down",
|
||||||
|
" Enter / → Open folder / launch ZIP with Hatari",
|
||||||
|
" ← / BS Go up one directory (never exceeds root)",
|
||||||
|
" ESC / Q Exit the program",
|
||||||
|
" PgUp Jump one page up",
|
||||||
|
" PgDn Jump one page down",
|
||||||
|
" * Toggle favorite on selected ZIP",
|
||||||
|
" ? Show this help",
|
||||||
|
" ",
|
||||||
|
" Navigation is strictly limited to RootDirectory.",
|
||||||
|
" The overlay shows both root and patch layers combined.",
|
||||||
|
" ",
|
||||||
|
" Press any key to continue...",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PadToWidth(string text, int width)
|
||||||
|
{
|
||||||
|
if (text.Length > width) return text.Substring(0, width);
|
||||||
|
return text + new string(' ', width - text.Length);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns foreground and background colors for an entry depending on selection state
|
// Returns foreground and background colors for an entry depending on selection state
|
||||||
private (ConsoleColor fg, ConsoleColor bg) GetColors(GameEntry e, bool selected)
|
private (ConsoleColor fg, ConsoleColor bg) GetColors(GameEntry e, bool selected)
|
||||||
{
|
{
|
||||||
@@ -156,18 +243,8 @@ namespace MarcerGameDvdLauncher
|
|||||||
private string BuildLineText(GameEntry e, int width, bool isFavorite)
|
private string BuildLineText(GameEntry e, int width, bool isFavorite)
|
||||||
{
|
{
|
||||||
if (width <= 0) return string.Empty;
|
if (width <= 0) return string.Empty;
|
||||||
// Reserve 6 characters for the label area. For directories we show "[DIR] ",
|
|
||||||
// for ZIPs we use the same width and optionally show a leading '*' when favorited.
|
string label = GetLabel(e, isFavorite);
|
||||||
string label;
|
|
||||||
if (e.Kind == EntryKind.Directory)
|
|
||||||
{
|
|
||||||
label = "[DIR] ";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// For ZIPs, show '*' after 4 spaces when favorited (keeps 6-char label area).
|
|
||||||
label = isFavorite ? " * " : new string(' ', 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the console width is smaller than the label, truncate the label
|
// If the console width is smaller than the label, truncate the label
|
||||||
if (width <= label.Length)
|
if (width <= label.Length)
|
||||||
@@ -197,10 +274,38 @@ namespace MarcerGameDvdLauncher
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the left label for an entry based on kind, layer status and favorite state.
|
||||||
|
// Format: [LayerLabel][TypeIndicator] where
|
||||||
|
// LayerLabel = [BOTH] / [ROOT] / [PTCH] (7 chars)
|
||||||
|
// TypeIndicator = [DIR] for dirs, " * " or " " for ZIPs (6 chars)
|
||||||
|
private static string GetLabel(GameEntry e, bool isFavorite)
|
||||||
|
{
|
||||||
|
// Layer label (7 chars)
|
||||||
|
string layer;
|
||||||
|
if (e.InRoot && e.InPatch) layer = "[BOTH] ";
|
||||||
|
else if (e.InPatch) layer = "[PTCH] ";
|
||||||
|
else if (e.InRoot) layer = "[ROOT] ";
|
||||||
|
else layer = " ";
|
||||||
|
|
||||||
|
// Type indicator (6 chars)
|
||||||
|
string type;
|
||||||
|
if (e.Kind == EntryKind.Directory)
|
||||||
|
{
|
||||||
|
type = "[DIR] ";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
type = isFavorite ? " * " : " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer + type; // 13 chars total
|
||||||
|
}
|
||||||
|
|
||||||
private ConsoleColor GetColorForEntry(GameEntry e)
|
private ConsoleColor GetColorForEntry(GameEntry e)
|
||||||
{
|
{
|
||||||
// Virtual entries (like the Favorites pseudo-folder) should be white
|
// Virtual entries (like the Favorites pseudo-folder) should be white
|
||||||
if (e.IsVirtual) return ConsoleColor.White;
|
if (e.IsVirtual) return ConsoleColor.White;
|
||||||
|
|
||||||
if (e.Kind == EntryKind.Directory)
|
if (e.Kind == EntryKind.Directory)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return ConsoleColor.Yellow; // Both layers
|
if (e.InRoot && e.InPatch) return ConsoleColor.Yellow; // Both layers
|
||||||
@@ -209,9 +314,9 @@ namespace MarcerGameDvdLauncher
|
|||||||
}
|
}
|
||||||
else if (e.Kind == EntryKind.Zip)
|
else if (e.Kind == EntryKind.Zip)
|
||||||
{
|
{
|
||||||
if (e.InRoot && e.InPatch) return ConsoleColor.Green;
|
if (e.InRoot && e.InPatch) return ConsoleColor.Green; // Both layers
|
||||||
if (e.InRoot && !e.InPatch) return ConsoleColor.DarkGreen;
|
if (e.InRoot && !e.InPatch) return ConsoleColor.DarkGreen; // Only root
|
||||||
if (e.InPatch && !e.InRoot) return ConsoleColor.Magenta;
|
if (e.InPatch && !e.InRoot) return ConsoleColor.Magenta; // Only patch
|
||||||
}
|
}
|
||||||
return ConsoleColor.DarkGray;
|
return ConsoleColor.DarkGray;
|
||||||
}
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
REM Starts MarcerGameDvdLauncher.exe from the correct folder
|
|
||||||
setlocal
|
|
||||||
set EXE_PATH=%~dp0MarcerGameDvdLauncher\bin\Release\net10.0\MarcerGameDvdLauncher.exe
|
|
||||||
|
|
||||||
if not exist "%EXE_PATH%" (
|
|
||||||
echo [ERROR] Application not built. Please run build.cmd first.
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
pushd "MarcerGameDvdLauncher\bin\Release\net10.0"
|
|
||||||
"MarcerGameDvdLauncher.exe"
|
|
||||||
popd
|
|
||||||
Reference in New Issue
Block a user