Compare commits
12 Commits
v0.1.0
...
e9c939f033
| Author | SHA1 | Date | |
|---|---|---|---|
| e9c939f033 | |||
| 94184c73d0 | |||
| 2fe20ad576 | |||
| 433ccfb0fe | |||
| c96c361973 | |||
| 1cb0097e68 | |||
| b9ed8ffb4e | |||
| be9093655d | |||
| b5de077ac3 | |||
| d220793b2d | |||
| d3c0a3e58e | |||
| 744e12a926 |
64
.gitignore
vendored
64
.gitignore
vendored
@@ -1,10 +1,60 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
pkg/
|
||||
|
||||
# debug information files
|
||||
*.dwo
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.12
|
||||
65
Makefile
65
Makefile
@@ -1,32 +1,53 @@
|
||||
# Compiler settings
|
||||
CC = gcc
|
||||
CFLAGS = -O2 -Wall -Wextra $(shell pkg-config --cflags sdl2 SDL2_image)
|
||||
LIBS = $(shell pkg-config --libs sdl2 SDL2_image)
|
||||
# Makefile for gsplash (community-standard layout)
|
||||
|
||||
# Project Structure
|
||||
TARGET = game-splash
|
||||
SRC = src/gsplash.c
|
||||
|
||||
# Installation Paths (Defaults to user local bin)
|
||||
PREFIX ?= /usr/local
|
||||
bindir ?= $(PREFIX)/bin
|
||||
|
||||
# Default target run when typing just 'make'
|
||||
all: $(TARGET)
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O2 -Wall -Wextra
|
||||
PKG_CONFIG ?= pkg-config
|
||||
|
||||
# Compilation rule
|
||||
$(TARGET): $(SRC)
|
||||
$(CC) $(CFLAGS) $(SRC) -o $(TARGET) $(LIBS)
|
||||
SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 SDL2_image)
|
||||
SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 SDL2_image)
|
||||
FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavformat libavcodec libswscale libavutil)
|
||||
FFMPEG_LIBS := $(shell $(PKG_CONFIG) --libs libavformat libavcodec libswscale libavutil)
|
||||
|
||||
ALL_CFLAGS := $(CFLAGS) $(SDL_CFLAGS) $(FFMPEG_CFLAGS)
|
||||
ALL_LIBS := $(LIBS) $(SDL_LIBS) $(FFMPEG_LIBS) -lm
|
||||
|
||||
BUILD_DIR ?= build
|
||||
TARGET = $(BUILD_DIR)/gsplash
|
||||
DUMMY_TARGET = $(BUILD_DIR)/dummy_game
|
||||
SRC = src/gsplash.c src/video.c
|
||||
DUMMY_SRC = src/dummy_game.c
|
||||
|
||||
.PHONY: all clean install uninstall check
|
||||
|
||||
all: $(TARGET) $(DUMMY_TARGET)
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
$(TARGET): $(BUILD_DIR) $(SRC)
|
||||
$(CC) $(ALL_CFLAGS) $(LDFLAGS) $(SRC) -o $@ $(ALL_LIBS)
|
||||
|
||||
$(DUMMY_TARGET): $(BUILD_DIR) $(DUMMY_SRC)
|
||||
$(CC) $(CFLAGS) $(SDL_CFLAGS) $(LDFLAGS) $(DUMMY_SRC) -o $@ $(SDL_LIBS)
|
||||
|
||||
# Lightweight smoke test (headless via SDL_VIDEODRIVER=dummy)
|
||||
check: $(TARGET) $(DUMMY_TARGET)
|
||||
@echo "Running smoke test (headless)..."
|
||||
SDL_VIDEODRIVER=dummy $(TARGET) nonexistent.png /bin/true || true
|
||||
@echo "Running CLI test suite..."
|
||||
./tests/test_cli.sh
|
||||
@echo "All tests finished successfully"
|
||||
|
||||
# Install binary system-wide
|
||||
install: $(TARGET)
|
||||
install -Dm755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
|
||||
install -d "$(DESTDIR)$(bindir)"
|
||||
install -m 755 $(TARGET) "$(DESTDIR)$(bindir)/gsplash"
|
||||
|
||||
# Remove binary from system
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)
|
||||
rm -f "$(DESTDIR)$(bindir)/gsplash"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
.PHONY: all install uninstall clean
|
||||
rm -f $(TARGET) $(DUMMY_TARGET)
|
||||
|
||||
20
PKGBUILD
20
PKGBUILD
@@ -1,24 +1,22 @@
|
||||
pkgname=gsplash-git
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="A lightweight SDL2 splash screen wrapper for game launchers."
|
||||
pkgdesc="A lightweight native SDL2 splash screen wrapper for game launchers."
|
||||
arch=('x86_64')
|
||||
depends=('sdl2' 'sdl2_image')
|
||||
license=('custom')
|
||||
depends=('sdl2' 'sdl2_image' 'ffmpeg')
|
||||
makedepends=('make' 'gcc' 'pkgconf')
|
||||
source=('src/splash.c' 'Makefile')
|
||||
sha256sums=('SKIP' 'SKIP')
|
||||
|
||||
prepare() {
|
||||
mkdir -p "$srcdir/src"
|
||||
cp "$srcdir/splash.c" "$srcdir/src/"
|
||||
}
|
||||
# Leave this empty so makepkg doesn't look for external downloads or local copies
|
||||
source=()
|
||||
sha256sums=()
|
||||
|
||||
build() {
|
||||
cd "$srcdir"
|
||||
cd "$startdir"
|
||||
make
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir"
|
||||
cd "$startdir"
|
||||
make DESTDIR="$pkgdir" PREFIX="/usr" install
|
||||
}
|
||||
}
|
||||
66
README.md
66
README.md
@@ -5,7 +5,7 @@ A fullscreen splash-screen wrapper for launching a game or app. It displays an i
|
||||
## Features
|
||||
|
||||
1. Fullscreen, borderless splash screen with hidden cursor (SDL2)
|
||||
2. Displays a supplied image via SDL2_image (fallback to black if load fails)
|
||||
2. Displays a supplied image via SDL2_image or video via ffmpeg (fallback to black if load fails)
|
||||
3. Launches the target executable and exits when it finishes
|
||||
4. Hides on focus loss after the game starts or closes on **Esc**
|
||||
|
||||
@@ -16,6 +16,7 @@ A fullscreen splash-screen wrapper for launching a game or app. It displays an i
|
||||
- pkg-config
|
||||
- SDL2
|
||||
- SDL2_image
|
||||
- ffmpeg (libavformat, libavcodec, libswscale, libavutil)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -28,18 +29,73 @@ makepkg -si
|
||||
For other distributions, build and install manually:
|
||||
|
||||
```bash
|
||||
# Build the binary into build/
|
||||
make
|
||||
|
||||
# Install system-wide (defaults to /usr/local)
|
||||
sudo make install
|
||||
|
||||
# Staged install (useful for packaging):
|
||||
DESTDIR=/some/staging/path make install
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Gsplash includes several testing utilities to ensure proper functionality without requiring a heavy game binary.
|
||||
|
||||
### Automated Testing
|
||||
|
||||
Run the automated test suite (which includes a headless smoke test and CLI argument validation) using:
|
||||
|
||||
```bash
|
||||
make check
|
||||
```
|
||||
|
||||
### Interactive Visual Testing
|
||||
|
||||
To physically test the splash screen rendering modes (`stretch`, `center`, `crop`) with a real image, use the interactive test script. It launches `gsplash` with each mode and prompts you to confirm if it displayed correctly:
|
||||
|
||||
```bash
|
||||
./tests/test_interactive.sh path/to/your/image.png
|
||||
```
|
||||
*(Tip: You can place your test images in the `tests/assets/` directory).*
|
||||
|
||||
### Dummy Game Utility
|
||||
|
||||
For manual testing, a `dummy_game` binary is built alongside `gsplash`. It mimics a real game by sleeping to simulate startup time, creating an SDL window to trigger `gsplash`'s focus loss detection and exiting cleanly.
|
||||
|
||||
```bash
|
||||
./build/gsplash path/to/image.png ./build/dummy_game
|
||||
|
||||
# Test with a custom 10 second simulated game load time
|
||||
./build/gsplash path/to/image.png ./build/dummy_game 10
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Build output installs to `/usr/local/bin` by default. Override with `PREFIX`.
|
||||
|
||||
```bash
|
||||
game-splash <image_path> <game_executable> [game_arguments...]
|
||||
gsplash <image_or_video_path> <game_executable> [game_arguments...]
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
game-splash assets/splash.jpg /path/to/game --fullscreen --profile=default
|
||||
gsplash assets/splash.jpg /path/to/game --fullscreen --profile=default
|
||||
|
||||
# Video splash (supported formats depend on ffmpeg build)
|
||||
gsplash assets/splash.mp4 /path/to/game --fullscreen --profile=default
|
||||
```
|
||||
|
||||
Gsplash allows you to configure how the image or video is displayed with 3 modes:
|
||||
|
||||
- `center` (default): letterbox
|
||||
- `crop`: fill screen by cropping
|
||||
- `stretch`: Distort to fill screen
|
||||
|
||||
You can set these by using the `-m` or `--mode` flag:
|
||||
|
||||
```bash
|
||||
build/gsplash [--mode=stretch|center|crop] <background> <executable> [args...]
|
||||
|
||||
build/gsplash -m stretch|center|crop <background> <executable> [args...]
|
||||
```
|
||||
67
src/dummy_game.c
Normal file
67
src/dummy_game.c
Normal file
@@ -0,0 +1,67 @@
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int delay_seconds = 5;
|
||||
if (argc > 1) {
|
||||
int parsed = atoi(argv[1]);
|
||||
if (parsed > 0) {
|
||||
delay_seconds = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
printf("[dummy_game] Simulating engine startup for %d seconds...\n", delay_seconds);
|
||||
for (int i = 0; i < delay_seconds; i++) {
|
||||
sleep(1);
|
||||
printf("[dummy_game] Loading... %d/%d\n", i + 1, delay_seconds);
|
||||
}
|
||||
|
||||
printf("[dummy_game] Creating window to trigger focus-loss on splash screen...\n");
|
||||
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||
fprintf(stderr, "[dummy_game] SDL Init Failed: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
SDL_Window *window = SDL_CreateWindow(
|
||||
"Dummy Game Window",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
800, 600,
|
||||
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE
|
||||
);
|
||||
|
||||
if (!window) {
|
||||
fprintf(stderr, "[dummy_game] Window Creation Failed: %s\n", SDL_GetError());
|
||||
SDL_Quit();
|
||||
return 1;
|
||||
}
|
||||
|
||||
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (renderer) {
|
||||
SDL_SetRenderDrawColor(renderer, 0, 120, 255, 255);
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
|
||||
printf("[dummy_game] Window created. Waiting 5 seconds before exiting...\n");
|
||||
|
||||
SDL_Event event;
|
||||
int running = 1;
|
||||
Uint32 start_time = SDL_GetTicks();
|
||||
|
||||
while (running && (SDL_GetTicks() - start_time < 5000)) {
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
running = 0;
|
||||
}
|
||||
}
|
||||
SDL_Delay(50);
|
||||
}
|
||||
|
||||
printf("[dummy_game] Exiting normally.\n");
|
||||
if (renderer) SDL_DestroyRenderer(renderer);
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_Quit();
|
||||
return 0;
|
||||
}
|
||||
339
src/gsplash.c
339
src/gsplash.c
@@ -7,8 +7,14 @@
|
||||
#include <stdarg.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <ctype.h>
|
||||
|
||||
static void log_info(const char* fmt, ...) {
|
||||
#include "video.h"
|
||||
|
||||
static void log_info(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
fprintf(stderr, "[splash] ");
|
||||
va_start(args, fmt);
|
||||
@@ -18,7 +24,8 @@ static void log_info(const char* fmt, ...) {
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
static void log_error(const char* fmt, ...) {
|
||||
static void log_error(const char *fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
fprintf(stderr, "[splash][error] ");
|
||||
va_start(args, fmt);
|
||||
@@ -28,23 +35,102 @@ static void log_error(const char* fmt, ...) {
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
log_error("Usage: %s <image_path> <game_executable> [args...]", argv[0]);
|
||||
typedef enum RenderMode
|
||||
{
|
||||
RENDER_STRETCH = 0,
|
||||
RENDER_CENTER,
|
||||
RENDER_CROP
|
||||
} RenderMode;
|
||||
|
||||
static RenderMode parse_render_mode(const char *value)
|
||||
{
|
||||
if (strcmp(value, "center") == 0)
|
||||
{
|
||||
return RENDER_CENTER;
|
||||
}
|
||||
if (strcmp(value, "crop") == 0)
|
||||
{
|
||||
return RENDER_CROP;
|
||||
}
|
||||
return RENDER_STRETCH;
|
||||
}
|
||||
|
||||
static void compute_dest_rect(int src_w, int src_h, int out_w, int out_h, RenderMode mode, SDL_Rect *dst)
|
||||
{
|
||||
dst->x = 0;
|
||||
dst->y = 0;
|
||||
dst->w = out_w;
|
||||
dst->h = out_h;
|
||||
|
||||
if (src_w <= 0 || src_h <= 0 || out_w <= 0 || out_h <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == RENDER_CENTER)
|
||||
{
|
||||
float scale = fminf((float)out_w / (float)src_w, (float)out_h / (float)src_h);
|
||||
dst->w = (int)(src_w * scale);
|
||||
dst->h = (int)(src_h * scale);
|
||||
dst->x = (out_w - dst->w) / 2;
|
||||
dst->y = (out_h - dst->h) / 2;
|
||||
}
|
||||
else if (mode == RENDER_CROP)
|
||||
{
|
||||
float scale = fmaxf((float)out_w / (float)src_w, (float)out_h / (float)src_h);
|
||||
dst->w = (int)(src_w * scale);
|
||||
dst->h = (int)(src_h * scale);
|
||||
dst->x = (out_w - dst->w) / 2;
|
||||
dst->y = (out_h - dst->h) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
RenderMode render_mode = RENDER_CENTER;
|
||||
int arg_index = 1;
|
||||
|
||||
if (argc >= 3 && strncmp(argv[1], "--mode=", 7) == 0)
|
||||
{
|
||||
render_mode = parse_render_mode(argv[1] + 7);
|
||||
arg_index += 1;
|
||||
}
|
||||
else if (argc >= 2 && strcmp(argv[1], "-m") == 0)
|
||||
{
|
||||
if (argc < 4)
|
||||
{
|
||||
log_error("Usage: %s [--mode=stretch|center|crop] <image_path> <game_executable> [args...]", argv[0]);
|
||||
log_error(" %s -m stretch|center|crop <image_path> <game_executable> [args...]", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
render_mode = parse_render_mode(argv[2]);
|
||||
arg_index += 2;
|
||||
}
|
||||
|
||||
if (argc - arg_index < 2)
|
||||
{
|
||||
log_error("Usage: %s [--mode=stretch|center|crop] <image_path> <game_executable> [args...]", argv[0]);
|
||||
log_error(" %s -m stretch|center|crop <image_path> <game_executable> [args...]", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
log_info("Starting splash: image='%s', game='%s'", argv[1], argv[2]);
|
||||
const char *image_path = argv[arg_index];
|
||||
const char *game_path = argv[arg_index + 1];
|
||||
|
||||
log_info("Starting splash: image='%s', game='%s', mode=%d", image_path, game_path, render_mode);
|
||||
|
||||
// Initialize SDL2 Video subsystems
|
||||
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) < 0)
|
||||
{
|
||||
log_error("SDL Init Failed: %s", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
|
||||
log_info("SDL initialized");
|
||||
|
||||
// Initialize JPEG and PNG decoders
|
||||
if (!(IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG) & (IMG_INIT_JPG | IMG_INIT_PNG))) {
|
||||
if (!(IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG) & (IMG_INIT_JPG | IMG_INIT_PNG)))
|
||||
{
|
||||
log_error("SDL_image Init Failed: %s", IMG_GetError());
|
||||
SDL_Quit();
|
||||
return 1;
|
||||
@@ -52,14 +138,14 @@ int main(int argc, char* argv[]) {
|
||||
log_info("SDL_image initialized (JPG/PNG)");
|
||||
|
||||
// Create a native borderless fullscreen window
|
||||
SDL_Window* window = SDL_CreateWindow(
|
||||
SDL_Window *window = SDL_CreateWindow(
|
||||
"Game Splash Screen",
|
||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||
0, 0,
|
||||
SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS
|
||||
);
|
||||
SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS);
|
||||
|
||||
if (!window) {
|
||||
if (!window)
|
||||
{
|
||||
log_error("Window Creation Failed: %s", SDL_GetError());
|
||||
IMG_Quit();
|
||||
SDL_Quit();
|
||||
@@ -70,87 +156,250 @@ int main(int argc, char* argv[]) {
|
||||
SDL_ShowCursor(SDL_DISABLE); // Hide the mouse pointer
|
||||
|
||||
// Create a hardware-accelerated renderer
|
||||
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (renderer) {
|
||||
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (renderer)
|
||||
{
|
||||
log_info("Renderer created (accelerated)");
|
||||
}
|
||||
SDL_Texture* texture = IMG_LoadTexture(renderer, argv[1]);
|
||||
SDL_Texture *texture = NULL;
|
||||
VideoPlayer video_player;
|
||||
bool video_active = false;
|
||||
|
||||
if (!texture) {
|
||||
log_error("Failed to load splash image '%s': %s; showing black screen", argv[1], IMG_GetError());
|
||||
if (has_video_extension(image_path))
|
||||
{
|
||||
if (init_video_player(&video_player, renderer, image_path))
|
||||
{
|
||||
video_active = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
log_error("Failed to open video '%s'; showing black screen", image_path);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_Surface *surface = IMG_Load(image_path);
|
||||
if (surface)
|
||||
{
|
||||
SDL_Surface *rgba_surface = SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA32, 0);
|
||||
SDL_FreeSurface(surface);
|
||||
if (rgba_surface)
|
||||
{
|
||||
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
|
||||
SDL_TEXTUREACCESS_STREAMING,
|
||||
rgba_surface->w, rgba_surface->h);
|
||||
if (texture)
|
||||
{
|
||||
SDL_UpdateTexture(texture, NULL, rgba_surface->pixels, rgba_surface->pitch);
|
||||
}
|
||||
else
|
||||
{
|
||||
log_error("Failed to create texture: %s", SDL_GetError());
|
||||
}
|
||||
SDL_FreeSurface(rgba_surface);
|
||||
}
|
||||
}
|
||||
if (!texture && init_video_player(&video_player, renderer, image_path))
|
||||
{
|
||||
video_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!texture && !video_active)
|
||||
{
|
||||
log_error("Failed to load splash image '%s': %s; showing black screen", image_path, IMG_GetError());
|
||||
// Fallback: Clear to solid black if image file is broken
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_RenderPresent(renderer);
|
||||
} else {
|
||||
log_info("Splash image loaded");
|
||||
SDL_RenderClear(renderer);
|
||||
// Automatically scales your image to perfectly match the monitor aspect ratio
|
||||
SDL_RenderCopy(renderer, texture, NULL, NULL);
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_Rect dst_rect = {0, 0, 0, 0};
|
||||
int out_w = 0;
|
||||
int out_h = 0;
|
||||
|
||||
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
|
||||
|
||||
if (video_active)
|
||||
{
|
||||
log_info("Video splash loaded");
|
||||
compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect);
|
||||
decode_next_frame(&video_player);
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect);
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
else if (texture)
|
||||
{
|
||||
int tex_w = 0;
|
||||
int tex_h = 0;
|
||||
|
||||
log_info("Splash image loaded");
|
||||
if (SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h) == 0)
|
||||
{
|
||||
compute_dest_rect(tex_w, tex_h, out_w, out_h, render_mode, &dst_rect);
|
||||
}
|
||||
|
||||
SDL_RenderClear(renderer);
|
||||
if (dst_rect.w > 0 && dst_rect.h > 0)
|
||||
{
|
||||
SDL_RenderCopy(renderer, texture, NULL, &dst_rect);
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_RenderCopy(renderer, texture, NULL, NULL);
|
||||
}
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
// Fork the process to run the game
|
||||
log_info("Launching game executable");
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
if (pid == 0)
|
||||
{
|
||||
// Inside Child Process: Hand over execution directly to the game binary
|
||||
log_info("Execing game: %s", argv[2]);
|
||||
execvp(argv[2], &argv[2]);
|
||||
log_error("Failed to launch target game executable '%s': %s", argv[2], strerror(errno));
|
||||
log_info("Execing game: %s", game_path);
|
||||
execvp(game_path, &argv[arg_index + 1]);
|
||||
log_error("Failed to launch target game executable '%s': %s", game_path, strerror(errno));
|
||||
_exit(1);
|
||||
} else if (pid < 0) {
|
||||
}
|
||||
else if (pid < 0)
|
||||
{
|
||||
log_error("Failed to fork process: %s", strerror(errno));
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
log_info("Game process started (pid=%d)", pid);
|
||||
// Inside Parent Process: Manage splash screen lifecycle
|
||||
int running = 1;
|
||||
SDL_Event event;
|
||||
bool hide_scheduled = false;
|
||||
Uint32 hide_time = 0;
|
||||
|
||||
while (running) {
|
||||
while (running)
|
||||
{
|
||||
// Non-blocking check: Has the game quit?
|
||||
int status;
|
||||
pid_t result = waitpid(pid, &status, WNOHANG);
|
||||
if (result > 0) {
|
||||
if (WIFEXITED(status)) {
|
||||
if (result > 0)
|
||||
{
|
||||
if (WIFEXITED(status))
|
||||
{
|
||||
log_info("Game exited (code=%d)", WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
}
|
||||
else if (WIFSIGNALED(status))
|
||||
{
|
||||
log_info("Game terminated by signal %d", WTERMSIG(status));
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
log_info("Game exited");
|
||||
}
|
||||
break; // Game closed, break the loop and close the script
|
||||
} else if (result < 0) {
|
||||
}
|
||||
else if (result < 0)
|
||||
{
|
||||
log_error("waitpid failed: %s", strerror(errno));
|
||||
break; // Process error safetynet
|
||||
}
|
||||
|
||||
if (video_active && SDL_GetTicks() >= video_player.next_frame_tick)
|
||||
{
|
||||
if (!decode_next_frame(&video_player))
|
||||
{
|
||||
// If decoding fails or loops, try again on next tick
|
||||
}
|
||||
video_player.next_frame_tick += (Uint32)video_player.frame_delay_ms;
|
||||
Uint32 now = SDL_GetTicks();
|
||||
if ((Sint32)(now - video_player.next_frame_tick) > 100)
|
||||
{
|
||||
video_player.next_frame_tick = now;
|
||||
}
|
||||
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_Rect dst_rect = {0, 0, 0, 0};
|
||||
int out_w = 0;
|
||||
int out_h = 0;
|
||||
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
|
||||
compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect);
|
||||
SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect);
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
|
||||
// Check desktop server window events
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
if (event.type == SDL_QUIT)
|
||||
{
|
||||
log_info("Splash dismissed via window close");
|
||||
running = 0;
|
||||
}
|
||||
if (event.type == SDL_WINDOWEVENT) {
|
||||
if (event.type == SDL_WINDOWEVENT)
|
||||
{
|
||||
// THE MOMENT THE GAME WINDOW STEALS FOCUS:
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||
log_info("Splash window hidden (focus lost)");
|
||||
SDL_HideWindow(window); // Instantly make splash transparent/invisible
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST && !hide_scheduled)
|
||||
{
|
||||
log_info("Splash window hidden (focus lost), scheduling hide");
|
||||
hide_scheduled = true;
|
||||
hide_time = SDL_GetTicks() + 250; // 250ms delay to prevent desktop flash
|
||||
}
|
||||
// Re-render static image when compositor requests a redraw
|
||||
if (event.window.event == SDL_WINDOWEVENT_EXPOSED && texture && !video_active)
|
||||
{
|
||||
int out_w = 0, out_h = 0;
|
||||
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
|
||||
int tex_w = 0, tex_h = 0;
|
||||
SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h);
|
||||
SDL_Rect dst_rect = {0, 0, 0, 0};
|
||||
compute_dest_rect(tex_w, tex_h, out_w, out_h, render_mode, &dst_rect);
|
||||
SDL_RenderClear(renderer);
|
||||
if (dst_rect.w > 0 && dst_rect.h > 0)
|
||||
SDL_RenderCopy(renderer, texture, NULL, &dst_rect);
|
||||
else
|
||||
SDL_RenderCopy(renderer, texture, NULL, NULL);
|
||||
SDL_RenderPresent(renderer);
|
||||
}
|
||||
}
|
||||
if (event.type == SDL_KEYDOWN) {
|
||||
if (event.key.keysym.sym == SDLK_ESCAPE) {
|
||||
if (event.type == SDL_KEYDOWN)
|
||||
{
|
||||
if (event.key.keysym.sym == SDLK_ESCAPE)
|
||||
{
|
||||
log_info("Splash dismissed via escape key");
|
||||
running = 0; // Emergency escape key loop override
|
||||
}
|
||||
}
|
||||
}
|
||||
SDL_Delay(33); // ~30 FPS polling loop to ensure near-zero CPU usage
|
||||
|
||||
if (hide_scheduled && SDL_GetTicks() >= hide_time)
|
||||
{
|
||||
log_info("Hiding splash window (delayed)");
|
||||
SDL_HideWindow(window);
|
||||
hide_scheduled = false;
|
||||
}
|
||||
|
||||
if (video_active)
|
||||
{
|
||||
Uint32 now = SDL_GetTicks();
|
||||
if ((Sint32)(video_player.next_frame_tick - now) > 0)
|
||||
{
|
||||
Uint32 delay = video_player.next_frame_tick - now;
|
||||
SDL_Delay(delay > 33 ? 33 : delay);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SDL_Delay(33); // ~30 FPS polling loop to ensure near-zero CPU usage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up memory space
|
||||
if (texture) SDL_DestroyTexture(texture);
|
||||
if (texture)
|
||||
SDL_DestroyTexture(texture);
|
||||
if (video_active)
|
||||
cleanup_video_player(&video_player);
|
||||
SDL_DestroyRenderer(renderer);
|
||||
SDL_DestroyWindow(window);
|
||||
IMG_Quit();
|
||||
|
||||
205
src/video.c
Normal file
205
src/video.c
Normal file
@@ -0,0 +1,205 @@
|
||||
#include "video.h"
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
bool has_video_extension(const char *path)
|
||||
{
|
||||
const char *dot = strrchr(path, '.');
|
||||
if (!dot || dot == path)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char ext[8];
|
||||
size_t i = 0;
|
||||
dot++;
|
||||
while (*dot && i < sizeof(ext) - 1)
|
||||
{
|
||||
ext[i++] = (char)tolower((unsigned char)*dot++);
|
||||
}
|
||||
ext[i] = '\0';
|
||||
|
||||
return strcmp(ext, "mp4") == 0 || strcmp(ext, "mkv") == 0 || strcmp(ext, "webm") == 0 ||
|
||||
strcmp(ext, "avi") == 0 || strcmp(ext, "mov") == 0 || strcmp(ext, "mpg") == 0 ||
|
||||
strcmp(ext, "mpeg") == 0;
|
||||
}
|
||||
|
||||
bool init_video_player(VideoPlayer *player, SDL_Renderer *renderer, const char *path)
|
||||
{
|
||||
memset(player, 0, sizeof(*player));
|
||||
|
||||
if (avformat_open_input(&player->format_ctx, path, NULL, NULL) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avformat_find_stream_info(player->format_ctx, NULL) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
player->stream_index = -1;
|
||||
for (unsigned int i = 0; i < player->format_ctx->nb_streams; ++i)
|
||||
{
|
||||
if (player->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
|
||||
{
|
||||
player->stream_index = (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (player->stream_index < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AVCodecParameters *codecpar = player->format_ctx->streams[player->stream_index]->codecpar;
|
||||
const AVCodec *decoder = avcodec_find_decoder(codecpar->codec_id);
|
||||
if (!decoder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
player->codec_ctx = avcodec_alloc_context3(decoder);
|
||||
if (!player->codec_ctx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_parameters_to_context(player->codec_ctx, codecpar) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (avcodec_open2(player->codec_ctx, decoder, NULL) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
player->width = player->codec_ctx->width;
|
||||
player->height = player->codec_ctx->height;
|
||||
player->frame = av_frame_alloc();
|
||||
player->rgba_frame = av_frame_alloc();
|
||||
if (!player->frame || !player->rgba_frame)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
player->rgba_buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, player->width, player->height, 1);
|
||||
player->rgba_buffer = (uint8_t *)av_malloc(player->rgba_buffer_size);
|
||||
if (!player->rgba_buffer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
av_image_fill_arrays(player->rgba_frame->data, player->rgba_frame->linesize, player->rgba_buffer,
|
||||
AV_PIX_FMT_RGBA, player->width, player->height, 1);
|
||||
|
||||
player->sws_ctx = sws_getContext(player->width, player->height, player->codec_ctx->pix_fmt,
|
||||
player->width, player->height, AV_PIX_FMT_RGBA, SWS_BILINEAR,
|
||||
NULL, NULL, NULL);
|
||||
if (!player->sws_ctx)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
player->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
|
||||
SDL_TEXTUREACCESS_STREAMING, player->width, player->height);
|
||||
if (!player->texture)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AVRational fr = player->format_ctx->streams[player->stream_index]->avg_frame_rate;
|
||||
if (fr.num > 0 && fr.den > 0)
|
||||
{
|
||||
player->frame_delay_ms = (int)(1000.0 * fr.den / fr.num);
|
||||
}
|
||||
else
|
||||
{
|
||||
player->frame_delay_ms = 33;
|
||||
}
|
||||
|
||||
player->next_frame_tick = SDL_GetTicks();
|
||||
return true;
|
||||
}
|
||||
|
||||
void cleanup_video_player(VideoPlayer *player)
|
||||
{
|
||||
if (player->texture)
|
||||
{
|
||||
SDL_DestroyTexture(player->texture);
|
||||
}
|
||||
if (player->sws_ctx)
|
||||
{
|
||||
sws_freeContext(player->sws_ctx);
|
||||
}
|
||||
if (player->rgba_buffer)
|
||||
{
|
||||
av_free(player->rgba_buffer);
|
||||
}
|
||||
if (player->rgba_frame)
|
||||
{
|
||||
av_frame_free(&player->rgba_frame);
|
||||
}
|
||||
if (player->frame)
|
||||
{
|
||||
av_frame_free(&player->frame);
|
||||
}
|
||||
if (player->codec_ctx)
|
||||
{
|
||||
avcodec_free_context(&player->codec_ctx);
|
||||
}
|
||||
if (player->format_ctx)
|
||||
{
|
||||
avformat_close_input(&player->format_ctx);
|
||||
}
|
||||
memset(player, 0, sizeof(*player));
|
||||
}
|
||||
|
||||
bool decode_next_frame(VideoPlayer *player)
|
||||
{
|
||||
AVPacket packet;
|
||||
av_init_packet(&packet);
|
||||
|
||||
while (av_read_frame(player->format_ctx, &packet) >= 0)
|
||||
{
|
||||
if (packet.stream_index != player->stream_index)
|
||||
{
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (avcodec_send_packet(player->codec_ctx, &packet) < 0)
|
||||
{
|
||||
av_packet_unref(&packet);
|
||||
continue;
|
||||
}
|
||||
av_packet_unref(&packet);
|
||||
|
||||
while (true)
|
||||
{
|
||||
int ret = avcodec_receive_frame(player->codec_ctx, player->frame);
|
||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (ret < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sws_scale(player->sws_ctx, (const uint8_t *const *)player->frame->data,
|
||||
player->frame->linesize, 0, player->height,
|
||||
player->rgba_frame->data, player->rgba_frame->linesize);
|
||||
|
||||
SDL_UpdateTexture(player->texture, NULL, player->rgba_frame->data[0],
|
||||
player->rgba_frame->linesize[0]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
av_seek_frame(player->format_ctx, player->stream_index, 0, AVSEEK_FLAG_BACKWARD);
|
||||
avcodec_flush_buffers(player->codec_ctx);
|
||||
return false;
|
||||
}
|
||||
34
src/video.h
Normal file
34
src/video.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#ifndef VIDEO_H
|
||||
#define VIDEO_H
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
|
||||
typedef struct VideoPlayer
|
||||
{
|
||||
AVFormatContext *format_ctx;
|
||||
AVCodecContext *codec_ctx;
|
||||
struct SwsContext *sws_ctx;
|
||||
AVFrame *frame;
|
||||
AVFrame *rgba_frame;
|
||||
uint8_t *rgba_buffer;
|
||||
int rgba_buffer_size;
|
||||
int stream_index;
|
||||
int width;
|
||||
int height;
|
||||
int frame_delay_ms;
|
||||
Uint32 next_frame_tick;
|
||||
SDL_Texture *texture;
|
||||
} VideoPlayer;
|
||||
|
||||
bool has_video_extension(const char *path);
|
||||
bool init_video_player(VideoPlayer *player, SDL_Renderer *renderer, const char *path);
|
||||
void cleanup_video_player(VideoPlayer *player);
|
||||
bool decode_next_frame(VideoPlayer *player);
|
||||
|
||||
#endif // VIDEO_H
|
||||
BIN
tests/assets/sample.jpg
Normal file
BIN
tests/assets/sample.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 652 KiB |
83
tests/test_cli.sh
Executable file
83
tests/test_cli.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A test suite for testing CLI argument permutations of gsplash
|
||||
|
||||
GSPLASH="./build/gsplash"
|
||||
GAME="/bin/true"
|
||||
IMAGE="nonexistent.png"
|
||||
|
||||
if [ ! -f "$GSPLASH" ]; then
|
||||
echo "Please run 'make' first to build gsplash."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SDL_VIDEODRIVER=dummy
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
run_test() {
|
||||
local expected_status=$1
|
||||
shift
|
||||
local expected_log="$1"
|
||||
shift
|
||||
local desc="$1"
|
||||
shift
|
||||
|
||||
printf "Test: %-45s " "$desc"
|
||||
|
||||
# Run the command and capture output
|
||||
local output
|
||||
output=$($GSPLASH "$@" 2>&1)
|
||||
local status=$?
|
||||
|
||||
local passed=true
|
||||
local error_msg=""
|
||||
|
||||
if [ $status -ne $expected_status ]; then
|
||||
passed=false
|
||||
error_msg="Expected status $expected_status, got $status"
|
||||
elif [ -n "$expected_log" ]; then
|
||||
if ! echo "$output" | grep -q "$expected_log"; then
|
||||
passed=false
|
||||
error_msg="Expected log missing: $expected_log"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $passed; then
|
||||
echo "PASS"
|
||||
pass=$((pass+1))
|
||||
else
|
||||
echo "FAIL ($error_msg)"
|
||||
fail=$((fail+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Running CLI Argument Tests ==="
|
||||
|
||||
# Valid combinations (0=STRETCH, 1=CENTER, 2=CROP)
|
||||
run_test 0 "mode=1" "Basic invocation" $IMAGE $GAME
|
||||
run_test 0 "mode=0" "Long mode: stretch" --mode=stretch $IMAGE $GAME
|
||||
run_test 0 "mode=1" "Long mode: center" --mode=center $IMAGE $GAME
|
||||
run_test 0 "mode=2" "Long mode: crop" --mode=crop $IMAGE $GAME
|
||||
run_test 0 "mode=0" "Long mode: fallback to stretch" --mode=invalid $IMAGE $GAME
|
||||
run_test 0 "mode=0" "Short mode: stretch" -m stretch $IMAGE $GAME
|
||||
run_test 0 "mode=1" "Short mode: center" -m center $IMAGE $GAME
|
||||
run_test 0 "mode=2" "Short mode: crop" -m crop $IMAGE $GAME
|
||||
run_test 0 "mode=0" "Short mode: fallback to stretch" -m whatever $IMAGE $GAME
|
||||
run_test 0 "mode=1" "With game arguments" $IMAGE $GAME arg1 arg2
|
||||
run_test 0 "mode=2" "Mode and game arguments" -m crop $IMAGE $GAME arg1 arg2
|
||||
|
||||
# Invalid combinations (Usage errors, so we just check for status 1, no specific log needed)
|
||||
run_test 1 "" "No arguments"
|
||||
run_test 1 "" "Only image" $IMAGE
|
||||
run_test 1 "" "Missing arg for short mode (-m only)" -m $IMAGE
|
||||
run_test 1 "" "Missing game for short mode" -m stretch $IMAGE
|
||||
|
||||
echo "================================================="
|
||||
echo "Tests completed: $pass passed, $fail failed."
|
||||
|
||||
if [ $fail -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
69
tests/test_interactive.sh
Executable file
69
tests/test_interactive.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
GSPLASH="./build/gsplash"
|
||||
DUMMY="./build/dummy_game"
|
||||
|
||||
if [ ! -f "$GSPLASH" ] || [ ! -f "$DUMMY" ]; then
|
||||
echo "Please run 'make' first to build gsplash and dummy_game."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <path_to_test_image_or_video>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE="$1"
|
||||
if [ ! -f "$IMAGE" ]; then
|
||||
echo "Error: File '$IMAGE' not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Interactive gsplash Visual Test ==="
|
||||
echo "This will physically display the splash screen using different modes."
|
||||
echo "You will be asked to confirm if it looked correct after each test."
|
||||
echo ""
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
run_interactive_test() {
|
||||
local mode="$1"
|
||||
|
||||
echo "------------------------------------------------"
|
||||
echo "Testing mode: $mode"
|
||||
echo "Launching in 2 seconds (it will stay open for 3 seconds)..."
|
||||
sleep 2
|
||||
|
||||
# Run gsplash with a 3-second dummy game
|
||||
$GSPLASH --mode="$mode" "$IMAGE" "$DUMMY" 3
|
||||
|
||||
# Prompt the user interactively
|
||||
while true; do
|
||||
read -p "Did it display correctly for mode '$mode'? [y/N] " response
|
||||
case "$response" in
|
||||
[yY][eE][sS]|[yY])
|
||||
echo "=> PASS recorded."
|
||||
pass=$((pass+1))
|
||||
break
|
||||
;;
|
||||
[nN][oO]|[nN]|"")
|
||||
echo "=> FAIL recorded."
|
||||
fail=$((fail+1))
|
||||
break
|
||||
;;
|
||||
*)
|
||||
echo "Please answer y or n."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
run_interactive_test "stretch"
|
||||
run_interactive_test "center"
|
||||
run_interactive_test "crop"
|
||||
|
||||
echo "================================================"
|
||||
echo "Interactive Testing Complete!"
|
||||
echo "Passed: $pass"
|
||||
echo "Failed: $fail"
|
||||
Reference in New Issue
Block a user