├── .dockerignore ├── .github └── workflows │ └── on-release.yml ├── LICENSE ├── README.md ├── deploy ├── Dockerfile.backend.build ├── Dockerfile.backend.dev ├── Dockerfile.launcher.build ├── Dockerfile.launcher.dev ├── Dockerfile.wizard.build ├── Dockerfile.wizard.dev ├── flatimage-alpine.sh └── libraries-wizard.txt ├── doc ├── img │ ├── PCSX2.svg │ ├── RPCS3.svg │ ├── alpine.svg │ ├── arch.svg │ ├── archlinux.svg │ ├── banner.svg │ ├── fedora.svg │ ├── gameimage.png │ ├── gameimage.svg │ ├── getfedora-ar21.svg │ ├── heart.svg │ ├── linux-mint.svg │ ├── linux.svg │ ├── opensuse.svg │ ├── pcsx2.webp │ ├── retroarch.png │ ├── retroarch.svg │ ├── rpcs3.jpg │ ├── steam.svg │ ├── ubuntu-ar21.svg │ ├── ubuntu.svg │ └── wine.svg ├── legacy │ ├── example-gui-1.png │ ├── example-gui-2.png │ ├── example-gui-launcher.png │ ├── example-gui.png │ ├── example-gui.xcf │ ├── gui-installer-1.png │ ├── gui-installer-2.png │ ├── gui-installer-3.png │ ├── gui-launcher-1.png │ ├── gui-launcher-2.png │ ├── not-aidan-tutorial.mp4 │ ├── wall.png │ └── wine.mp4 ├── readme │ ├── showcase-00.png │ ├── showcase-01.png │ ├── showcase-02.png │ └── showcase-03.png ├── toc.sh └── video │ ├── tutorial-demo.mp4 │ ├── tutorial-download.mp4 │ ├── tutorial-linux.low.mp4 │ ├── tutorial-linux.mp4 │ ├── tutorial-wine-multi.low.mp4 │ ├── tutorial-wine-multi.mp4 │ ├── tutorial-wine.low.mp4 │ └── tutorial-wine.mp4 ├── gui ├── Cargo.toml ├── launcher │ ├── Cargo.toml │ └── src │ │ ├── common.rs │ │ ├── db │ │ ├── mod.rs │ │ └── project.rs │ │ ├── frame │ │ ├── cover.rs │ │ ├── fail.rs │ │ ├── menu.rs │ │ ├── menu │ │ │ ├── enabler_executable.rs │ │ │ └── environment.rs │ │ ├── mod.rs │ │ ├── selector_executable.rs │ │ └── selector_game.rs │ │ ├── games.rs │ │ └── main.rs ├── shared │ ├── Cargo.toml │ └── src │ │ ├── db │ │ ├── kv.rs │ │ └── mod.rs │ │ ├── dimm.rs │ │ ├── fltk.rs │ │ ├── fltk │ │ ├── button.rs │ │ ├── dialog.rs │ │ ├── frame.rs │ │ ├── macros.rs │ │ ├── paginator.rs │ │ ├── progress.rs │ │ └── separator.rs │ │ ├── image.rs │ │ ├── lib.rs │ │ ├── std.rs │ │ └── svg.rs └── wizard │ ├── Cargo.toml │ └── src │ ├── common.rs │ ├── db │ ├── fetch.rs │ ├── global.rs │ ├── mod.rs │ └── project.rs │ ├── frame │ ├── common.rs │ ├── creator.rs │ ├── desktop.rs │ ├── finish.rs │ ├── icon.rs │ ├── mod.rs │ ├── platform.rs │ ├── term.rs │ └── welcome.rs │ ├── gameimage │ ├── desktop.rs │ ├── fetch.rs │ ├── gameimage.rs │ ├── init.rs │ ├── install.rs │ ├── mod.rs │ ├── package.rs │ ├── project.rs │ ├── search.rs │ ├── select.rs │ └── test.rs │ ├── lib │ ├── ipc.rs │ └── mod.rs │ ├── main.rs │ └── wizard │ ├── compress.rs │ ├── install.rs │ ├── linux.rs │ ├── mod.rs │ ├── name.rs │ ├── pcsx2.rs │ ├── retroarch.rs │ ├── rpcs3.rs │ ├── test.rs │ └── wine.rs ├── src ├── CMakeLists.txt ├── Dockerfile ├── boot │ └── boot.cpp ├── build.sh ├── cmd │ ├── compress.hpp │ ├── desktop.hpp │ ├── fetch.hpp │ ├── fetch │ │ └── check.hpp │ ├── init.hpp │ ├── install.hpp │ ├── package.hpp │ ├── project.hpp │ ├── search.hpp │ ├── select.hpp │ └── test.hpp ├── common.hpp ├── conanfile.txt ├── enum.hpp ├── lib │ ├── db.hpp │ ├── db │ │ ├── build.hpp │ │ ├── fetch.hpp │ │ └── project.hpp │ ├── hope.hpp │ ├── image.hpp │ ├── ipc.hpp │ ├── log.hpp │ ├── parser.hpp │ ├── sha.hpp │ ├── subprocess.hpp │ ├── tar.hpp │ └── zip.hpp ├── macro.hpp ├── main.cpp └── std │ ├── concepts.hpp │ ├── copy.hpp │ ├── env.hpp │ ├── exception.hpp │ ├── fifo.hpp │ ├── filesystem.hpp │ ├── string.hpp │ └── vector.hpp └── thumbnailer ├── appimage.thumbnailer ├── install.sh └── thumbnailer-appimage /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | !.dockerignore 3 | !.github/workflows/on-release.yml 4 | !.gitlab-ci.yml 5 | !Dockerfile 6 | !LICENSE 7 | !README.md 8 | !deploy 9 | !gui/Cargo.toml 10 | !gui/launcher 11 | !gui/shared 12 | !gui/wizard 13 | !src/std 14 | !src/cmd 15 | !src/boot 16 | !src/lib 17 | !src/CMakeLists.txt 18 | !src/Dockerfile 19 | !src/build.sh 20 | !src/common.hpp 21 | !src/conanfile.txt 22 | !src/enum.hpp 23 | !src/macro.hpp 24 | !src/main.cpp 25 | -------------------------------------------------------------------------------- /.github/workflows/on-release.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: linux-x86_64 4 | 5 | on: 6 | workflow_dispatch: 7 | release: 8 | types: [ created, edited ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Build Package 19 | run: | 20 | # Install tools 21 | sudo apt install -y jq wget tar xz-utils pv git pcregrep 22 | # Custom packages 23 | mkdir -p bin 24 | export PATH="$(pwd)/bin:$PATH" 25 | wget -q --show-progress --progress=dot:binary -O bin/jq \ 26 | https://github.com/jqlang/jq/releases/download/jq-1.7/jq-linux-amd64 27 | chmod +x ./bin/* 28 | # Build packages 29 | ( HOME="$(pwd)" FIM_DEBUG=1 ./deploy/flatimage-alpine.sh ) 30 | # Create dist folder 31 | mkdir -p dist 32 | mv ./build/gameimage dist/gameimage.flatimage 33 | # Create SHA for gameimage.flatimage 34 | ( cd dist && sha256sum gameimage.flatimage > gameimage.flatimage.sha256sum ) 35 | 36 | - name: Set permissions for dist directory 37 | run: | 38 | sudo chown -R "$(id -u)":"$(id -g)" dist/ 39 | sudo chmod -R 766 dist/ 40 | 41 | - name: Upload artifact to release 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: gameimage 45 | path: dist 46 | 47 | - name: Compute Short SHA 48 | id: ssha 49 | run: | 50 | echo "ssha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT 51 | 52 | outputs: 53 | ssha: ${{ steps.ssha.outputs.ssha }} 54 | 55 | release: 56 | runs-on: ubuntu-latest 57 | needs: build 58 | steps: 59 | - uses: actions/download-artifact@v3 60 | with: 61 | name: gameimage 62 | path: dist 63 | - name: Upload to release 64 | uses: fnkr/github-action-ghr@v1 65 | env: 66 | GHR_PATH: dist/ 67 | GHR_REPLACE: true 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | -------------------------------------------------------------------------------- /deploy/Dockerfile.backend.build: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/ruanformigoni/gameimage-backend-dev 2 | 3 | COPY ./src /backend 4 | 5 | WORKDIR /backend 6 | 7 | RUN conan install . --build=missing -g CMakeDeps -g CMakeToolchain 8 | RUN cmake --preset conan-release -DCMAKE_BUILD_TYPE=Release 9 | RUN cmake --build --preset conan-release 10 | 11 | # Move to dist 12 | RUN mkdir /dist 13 | RUN cp ./build/Release/main /dist 14 | RUN cp ./build/Release/boot /dist 15 | WORKDIR /dist 16 | 17 | # Strip 18 | RUN strip -s * 19 | 20 | # Compress 21 | RUN upx -6 --no-lzma * 22 | -------------------------------------------------------------------------------- /deploy/Dockerfile.backend.dev: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | # Enable community repo 4 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/main/ > /etc/apk/repositories 5 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories 6 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/testing/ >> /etc/apk/repositories 7 | 8 | # Install deps 9 | RUN apk update && apk upgrade 10 | RUN apk add --no-cache build-base git libbsd-dev py3-pip pipx git patchelf cmake gcc \ 11 | bash e2fsprogs xz curl zstd gawk debootstrap m4 gcompat nasm wget upx \ 12 | boost-dev boost-static 13 | 14 | # Fonts to copy from inside the container 15 | RUN apk add --no-cache font-noto 16 | 17 | # Install conan 18 | RUN pipx install conan 19 | 20 | # Update PATH 21 | ENV PATH="/root/.local/bin:$PATH" 22 | 23 | # Setup 24 | RUN conan profile detect --force 25 | 26 | # Copy files 27 | COPY ./src /backend 28 | 29 | # Set workdir 30 | WORKDIR /backend 31 | 32 | # Compile 33 | # # This will fail ;(, needs to run, patch, run again 34 | RUN conan install . --build=missing -g CMakeDeps -g CMakeToolchain --output-folder=build || true 35 | # # nasm bug when on musl system "undefined symbol" 36 | # # m4 bug when on musl system "undefined symbol" 37 | RUN cp "$(command -v nasm)" /root/.conan2/p/nasmc*/p/bin/nasm 38 | RUN cp "$(command -v m4)" /root/.conan2/p/m4*/p/bin/m4 39 | # # Should work now 40 | RUN conan install . --build=missing -g CMakeDeps -g CMakeToolchain 41 | # The conan cache should now be built in /root/.conan2, remove project files 42 | WORKDIR / 43 | RUN rm -rf /backend 44 | -------------------------------------------------------------------------------- /deploy/Dockerfile.launcher.build: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/ruanformigoni/gameimage-launcher-dev 2 | 3 | # Copy files 4 | RUN mkdir /gameimage 5 | COPY ./gui /gameimage/ 6 | 7 | # Retrieve target folder 8 | RUN mv /target /gameimage/target 9 | 10 | # Enter src dir 11 | WORKDIR gameimage 12 | 13 | # Compile 14 | RUN cd launcher && cargo build --release 15 | 16 | # Move to dist 17 | RUN mkdir /dist 18 | RUN cp ./target/release/launcher /dist/launcher 19 | -------------------------------------------------------------------------------- /deploy/Dockerfile.launcher.dev: -------------------------------------------------------------------------------- 1 | FROM archlinux:latest 2 | 3 | # Update 4 | RUN pacman -Syy --noconfirm 5 | RUN pacman-key --init 6 | RUN pacman -S --noconfirm archlinux-keyring 7 | RUN pacman -Syu --overwrite "*" --noconfirm 8 | 9 | # Install dependencies 10 | RUN pacman -S --noconfirm --overwrite "*" autoconf automake binutils bison debugedit fakeroot \ 11 | file findutils flex gawk gcc gettext grep groff gzip libtool m4 make patch \ 12 | pkgconf sed sudo texinfo which git cmake gtk3 wayland-protocols wayland dbus 13 | 14 | # Setup rust 15 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly 16 | ENV PATH "/root/.cargo/bin:$PATH" 17 | 18 | # Copy files 19 | RUN mkdir /gameimage 20 | COPY ./gui /gameimage/ 21 | 22 | # Enter src dir 23 | WORKDIR gameimage 24 | 25 | # Compile 26 | RUN cd launcher && cargo build --release 27 | 28 | # Save target folder 29 | WORKDIR / 30 | RUN mv /gameimage/target /target 31 | RUN rm -rf /gameimage 32 | -------------------------------------------------------------------------------- /deploy/Dockerfile.wizard.build: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/ruanformigoni/gameimage-wizard-dev 2 | 3 | # Copy sources 4 | COPY ./gui /gameimage 5 | 6 | # Enter src dir 7 | WORKDIR /gameimage 8 | 9 | # Retrieve pre-compiled dependencies 10 | RUN mv /target /gameimage/target 11 | 12 | # Compile wizard 13 | RUN cd wizard && cargo build --release --target=x86_64-unknown-linux-musl 14 | 15 | # Make dist 16 | RUN mkdir /dist 17 | RUN cp /gameimage/target/x86_64-unknown-linux-musl/release/wizard /dist 18 | -------------------------------------------------------------------------------- /deploy/Dockerfile.wizard.dev: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | # Switch to edge 4 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/main/ > /etc/apk/repositories 5 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories 6 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/testing/ >> /etc/apk/repositories 7 | 8 | # Install packages 9 | RUN apk update 10 | RUN apk add git wget curl patchelf py3-pip pipx file build-base cmake \ 11 | fuse3-dev libxinerama-dev libxcursor-dev libxfixes-dev libxft-dev pango \ 12 | pango-dev libayatana-appindicator libayatana-appindicator-dev gtk+3.0-dev \ 13 | bash fuse fuse3 zlib-static glib-static cairo-static lddtree font-noto \ 14 | openssl-dev wayland wayland-protocols wayland-dev dbus dbus-dev libssl3 15 | 16 | # Symlink compilers 17 | RUN ln -sfT /usr/bin/gcc /usr/bin/musl-gcc 18 | RUN ln -sfT /usr/bin/g++ /usr/bin/musl-g++ 19 | 20 | # Compile as shared libraries 21 | ENV RUSTFLAGS='-C target-feature=-crt-static' 22 | 23 | # Install rust 24 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain nightly 25 | 26 | # Update env 27 | ENV PATH="/root/.cargo/bin:$PATH" 28 | 29 | # Copy sources 30 | COPY ./gui /gameimage 31 | 32 | # Enter src dir 33 | WORKDIR gameimage 34 | 35 | # Compile 36 | RUN cargo build --release --target=x86_64-unknown-linux-musl 37 | 38 | # Move target folder 39 | RUN mv ./target /target 40 | 41 | # Remove gameimage folder 42 | RUN rm -rf /gameimage 43 | -------------------------------------------------------------------------------- /deploy/libraries-wizard.txt: -------------------------------------------------------------------------------- 1 | /usr/lib/libayatana-appindicator3.so.1 2 | /usr/lib/libayatana-appindicator3.so 3 | /usr/lib/libayatana-appindicator3.so.1.0.0 4 | /usr/lib/libayatana-indicator3.so.7.0.0 5 | /usr/lib/libayatana-indicator3.so.7 6 | /usr/lib/libayatana-appindicator3.so 7 | /usr/lib/libayatana-ido3-0.4.so.0 8 | /usr/lib/libayatana-ido3-0.4.so.0.0.0 9 | -------------------------------------------------------------------------------- /doc/img/PCSX2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /doc/img/RPCS3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /doc/img/arch.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /doc/img/archlinux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/img/fedora.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/img/gameimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/img/gameimage.png -------------------------------------------------------------------------------- /doc/img/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /doc/img/pcsx2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/img/pcsx2.webp -------------------------------------------------------------------------------- /doc/img/retroarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/img/retroarch.png -------------------------------------------------------------------------------- /doc/img/retroarch.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 34 | 43 | 46 | 47 | -------------------------------------------------------------------------------- /doc/img/rpcs3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/img/rpcs3.jpg -------------------------------------------------------------------------------- /doc/img/steam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /doc/img/ubuntu-ar21.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/img/ubuntu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /doc/img/wine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 34 | 36 | 43 | 46 | 50 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /doc/legacy/example-gui-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/example-gui-1.png -------------------------------------------------------------------------------- /doc/legacy/example-gui-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/example-gui-2.png -------------------------------------------------------------------------------- /doc/legacy/example-gui-launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/example-gui-launcher.png -------------------------------------------------------------------------------- /doc/legacy/example-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/example-gui.png -------------------------------------------------------------------------------- /doc/legacy/example-gui.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/example-gui.xcf -------------------------------------------------------------------------------- /doc/legacy/gui-installer-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/gui-installer-1.png -------------------------------------------------------------------------------- /doc/legacy/gui-installer-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/gui-installer-2.png -------------------------------------------------------------------------------- /doc/legacy/gui-installer-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/gui-installer-3.png -------------------------------------------------------------------------------- /doc/legacy/gui-launcher-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/gui-launcher-1.png -------------------------------------------------------------------------------- /doc/legacy/gui-launcher-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/gui-launcher-2.png -------------------------------------------------------------------------------- /doc/legacy/not-aidan-tutorial.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/not-aidan-tutorial.mp4 -------------------------------------------------------------------------------- /doc/legacy/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/wall.png -------------------------------------------------------------------------------- /doc/legacy/wine.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/legacy/wine.mp4 -------------------------------------------------------------------------------- /doc/readme/showcase-00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/readme/showcase-00.png -------------------------------------------------------------------------------- /doc/readme/showcase-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/readme/showcase-01.png -------------------------------------------------------------------------------- /doc/readme/showcase-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/readme/showcase-02.png -------------------------------------------------------------------------------- /doc/readme/showcase-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/readme/showcase-03.png -------------------------------------------------------------------------------- /doc/toc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ###################################################################### 4 | # @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 5 | # @file : toc 6 | ###################################################################### 7 | 8 | set -e 9 | 10 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 11 | 12 | cd "${SCRIPT_DIR}" 13 | 14 | readarray -t arr <<<"$(grep -Ei "^#+" ../README.md)" 15 | 16 | for i in "${arr[@]}"; do 17 | lead="${i//${i//\#/}/}" 18 | lead="${lead//#/ }" 19 | text="${i#\#* }" 20 | link="${text// /-}" 21 | link="${link//\(/}" 22 | link="${link//\)/}" 23 | link="$(echo "$link" | tr "[:upper:]" "[:lower:]")" 24 | echo "${lead}- [$text](#${link})" 25 | done >> ../README.md 26 | 27 | # // cmd: !./% 28 | -------------------------------------------------------------------------------- /doc/video/tutorial-demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-demo.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-download.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-download.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-linux.low.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-linux.low.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-linux.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-linux.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-wine-multi.low.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-wine-multi.low.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-wine-multi.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-wine-multi.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-wine.low.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-wine.low.mp4 -------------------------------------------------------------------------------- /doc/video/tutorial-wine.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanformigoni/gameimage/92c19fdf0bf13b4936c37a2c97097c7c2011b219/doc/video/tutorial-wine.mp4 -------------------------------------------------------------------------------- /gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ "shared", "wizard", "launcher", ] 3 | resolver = "2" 4 | 5 | -------------------------------------------------------------------------------- /gui/launcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "launcher" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | shared = { path = "../shared" } 10 | fltk = { version = "1.4", features = ["use-wayland"] } 11 | fltk-theme = "0.6.0" 12 | fltk-evented = "0.5" 13 | display-info = "0.4.4" 14 | anyhow = "1.0" 15 | clown = "1.1.0" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde_yaml = "0.9" 18 | serde_json = "1.0" 19 | regex = "1.10.3" 20 | sysinfo = "0.30.7" 21 | walkdir = "2.5.0" 22 | once_cell = "1.19.0" 23 | clap = { version = "4.5.20", features = ["derive"] } 24 | -------------------------------------------------------------------------------- /gui/launcher/src/common.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | #[allow(dead_code)] 5 | pub enum Msg 6 | { 7 | DrawCover, 8 | DrawSelectorGame, 9 | DrawSelectorExecutable, 10 | DrawEnv, 11 | DrawEnablerExecutable, 12 | DrawMenu, 13 | WindActivate, 14 | WindDeactivate, 15 | Quit, 16 | } // enum 17 | 18 | #[allow(dead_code)] 19 | #[derive(PartialEq)] 20 | pub enum Platform 21 | { 22 | LINUX, 23 | WINE, 24 | RETROARCH, 25 | PCSX2, 26 | RPCS3, 27 | } // Platform 28 | 29 | // impl Platform {{{ 30 | impl Platform 31 | { 32 | pub fn as_str(&self) -> &'static str 33 | { 34 | match self 35 | { 36 | Platform::LINUX => "linux", 37 | Platform::WINE => "wine", 38 | Platform::RETROARCH => "retroarch", 39 | Platform::PCSX2 => "pcsx2", 40 | Platform::RPCS3 => "rpcs3", 41 | } // match 42 | } // as_str 43 | 44 | pub fn from_str(src : &str) -> anyhow::Result 45 | { 46 | match src.to_lowercase().as_str() 47 | { 48 | "linux" => Ok(Platform::LINUX), 49 | "wine" => Ok(Platform::WINE), 50 | "retroarch" => Ok(Platform::RETROARCH), 51 | "pcsx2" => Ok(Platform::PCSX2), 52 | "rpcs3" => Ok(Platform::RPCS3), 53 | &_ => Err(ah!("Could not determine platform with string")), 54 | } // match 55 | } // as_str 56 | } // impl Platform }}} 57 | 58 | #[macro_export] 59 | macro_rules! assign_to_arc_mutex 60 | { 61 | ($arc_mutex:expr, $value:expr) => 62 | { 63 | { 64 | let mut data = $arc_mutex.lock().unwrap(); 65 | *data = $value; 66 | } 67 | }; 68 | } 69 | 70 | #[macro_export] 71 | macro_rules! call_with_args 72 | { 73 | ($func:ident, $( $obj:expr ),* ) => 74 | { 75 | $( 76 | $obj.$func(); 77 | )* 78 | }; 79 | } 80 | 81 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 82 | -------------------------------------------------------------------------------- /gui/launcher/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project; 2 | -------------------------------------------------------------------------------- /gui/launcher/src/db/project.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::io::prelude::*; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | // struct Project {{{ 6 | #[derive(Clone, Serialize, Deserialize)] 7 | pub struct Project 8 | { 9 | pub project : String, 10 | pub platform : String, 11 | pub path_file_icon : Option, 12 | pub path_file_rom : Option, 13 | pub path_file_core : Option, 14 | pub path_file_bios : Option, 15 | #[serde(skip)] 16 | path_file_db : PathBuf, 17 | } // struct Project }}} 18 | 19 | // pub fn read() {{{ 20 | pub fn read(path_file_db : &PathBuf) -> anyhow::Result 21 | { 22 | let mut project : Project = serde_json::from_reader(std::fs::File::open(path_file_db)?)?; 23 | project.path_file_db = path_file_db.into(); 24 | Ok(project) 25 | } // fn: read }}} 26 | 27 | // pub fn write() {{{ 28 | #[allow(dead_code)] 29 | pub fn write(project : Project) -> anyhow::Result<()> 30 | { 31 | write!(std::fs::File::create(&project.path_file_db)?, "{}", serde_json::to_string(&project)?)?; 32 | Ok(()) 33 | } // fn: write }}} 34 | 35 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 36 | -------------------------------------------------------------------------------- /gui/launcher/src/frame/cover.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use fltk::prelude::*; 4 | use fltk::{ 5 | app::Sender, 6 | enums, 7 | frame::Frame, 8 | image::SharedImage, 9 | }; 10 | 11 | use shared::dimm; 12 | use shared::fltk::WidgetExtExtra; 13 | use shared::fltk::SenderExt; 14 | use shared::{hover_blink,column,row,fixed}; 15 | 16 | use crate::common; 17 | use crate::games; 18 | use common::Msg; 19 | 20 | // fn: new {{{ 21 | pub fn new(tx : Sender) 22 | { 23 | let show_btn_game: bool = games::games().map(|e| e.len() > 1).unwrap_or(false); 24 | let show_btn_executable: bool = crate::frame::selector_executable::get_menu_entries().map(|e| e.0.len() > 1).unwrap_or(false); 25 | // Layout 26 | let mut frame_background = Frame::default_fill(); 27 | if let Ok(env_image_launcher) = env::var("GIMG_LAUNCHER_IMG") 28 | && let Ok(shared_image) = SharedImage::load(env_image_launcher) 29 | { 30 | frame_background.set_image_scaled(Some(shared_image.clone())); 31 | frame_background.resize_callback(move |s,_,_,_,_| { s.set_image_scaled(Some(shared_image.clone())); }); 32 | } // if 33 | else 34 | { 35 | println!("Failed to set launcher image"); 36 | } // else 37 | // Buttons 38 | column!(col, 39 | col.add(&Frame::default()); 40 | row!(row, 41 | row.set_margin(dimm::border_half()); 42 | fixed!(row, btn_menu, shared::fltk::button::rect::list(), dimm::width_button_rec()); 43 | if show_btn_game 44 | { 45 | row.add(&Frame::default()); 46 | fixed!(row, btn_game, shared::fltk::button::rect::joystick(), dimm::width_button_rec()); 47 | btn_game.clone().emit(tx, Msg::DrawSelectorGame); 48 | hover_blink!(btn_game); 49 | } // if 50 | if show_btn_executable 51 | { 52 | row.add(&Frame::default()); 53 | fixed!(row, btn_executable, shared::fltk::button::rect::switch(), dimm::width_button_rec()); 54 | btn_executable.clone().emit(tx, Msg::DrawSelectorExecutable); 55 | hover_blink!(btn_executable); 56 | } // if 57 | row.add(&Frame::default()); 58 | fixed!(row, btn_play, shared::fltk::button::rect::play().with_color(enums::Color::Blue), dimm::width_button_rec()); 59 | ); 60 | col.fixed(&row, dimm::height_button_rec() + dimm::border()); 61 | ); 62 | 63 | let mut fb: Vec = vec![0u8; (dimm::width_launcher() * dimm::height_launcher() * 4) as usize]; 64 | // Fill with required color 65 | for (_, pixel) in fb.chunks_exact_mut(4).enumerate() { 66 | pixel.copy_from_slice(&[0, 0, 0, 96]); 67 | } 68 | let image = fltk::image::RgbImage::new(&fb, dimm::width_launcher(), dimm::height_button_rec() + dimm::border(), enums::ColorDepth::Rgba8).unwrap(); 69 | // Bottom background 70 | let mut row = row.clone(); 71 | row.set_align(enums::Align::Inside | enums::Align::Center); 72 | row.set_frame(enums::FrameType::NoBox); 73 | row.set_image(Some(image)); 74 | row.resize_callback(move |s,_,_,w,_| 75 | { 76 | let image = fltk::image::RgbImage::new(&fb, w, dimm::height_button_rec() + dimm::border(), enums::ColorDepth::Rgba8).unwrap(); 77 | s.set_image(Some(image)); 78 | }); 79 | 80 | // Button left aligned 81 | btn_menu.clone().emit(tx, Msg::DrawMenu); 82 | hover_blink!(btn_menu); 83 | hover_blink!(btn_play); 84 | 85 | // Button right aligned 86 | let clone_tx = tx.clone(); 87 | let mut clone_frame_background = frame_background.clone(); 88 | btn_play.clone().set_callback(move |_| 89 | { 90 | // Cover image black and white 91 | if let Ok(env_image_launcher) = env::var("GIMG_LAUNCHER_IMG_GRAYSCALE") 92 | && let Ok(shared_image) = SharedImage::load(env_image_launcher) 93 | { 94 | clone_frame_background.set_image_scaled(Some(shared_image)); 95 | } // if 96 | else 97 | { 98 | println!("Failed to set launcher image"); 99 | } // else 100 | fltk::app::redraw(); 101 | // Deactivate window 102 | clone_tx.send_awake(common::Msg::WindDeactivate); 103 | // Reference to spawned process 104 | std::thread::spawn(move || 105 | { 106 | // Launch game 107 | games::launch(); 108 | // Redraw 109 | clone_tx.send_activate(Msg::DrawCover); 110 | }); 111 | }); 112 | } // fn: new }}} 113 | 114 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 115 | -------------------------------------------------------------------------------- /gui/launcher/src/frame/fail.rs: -------------------------------------------------------------------------------- 1 | use fltk::{ 2 | prelude::*, 3 | frame::Frame, 4 | }; 5 | 6 | use shared::column; 7 | 8 | // fn: new {{{ 9 | pub fn new() 10 | { 11 | column!(col, Frame::default().with_label("No game found inside this image");); 12 | } // fn: new }}} 13 | 14 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 15 | -------------------------------------------------------------------------------- /gui/launcher/src/frame/menu.rs: -------------------------------------------------------------------------------- 1 | use fltk::prelude::*; 2 | use fltk::{ 3 | app::Sender, 4 | enums::{Align,FrameType,Color}, 5 | frame::Frame, 6 | }; 7 | 8 | use clown::clown; 9 | use common::Msg; 10 | use shared::dimm; 11 | use shared::fltk::WidgetExtExtra; 12 | use shared::{fixed,hover_blink,hseparator_fixed,column,row,rescope}; 13 | use fltk::prelude::ButtonExt; 14 | 15 | use crate::common; 16 | 17 | pub mod enabler_executable; 18 | pub mod environment; 19 | 20 | // fn: new {{{ 21 | pub fn new(tx : Sender) 22 | { 23 | // Layout 24 | column!(col, 25 | col.set_margin(dimm::border_half()); 26 | fixed!(col, frame_title, Frame::default(), dimm::height_text()); 27 | hseparator_fixed!(col, col.w() - dimm::border()*2, dimm::border_half()); 28 | column!(col_content, ); 29 | col_content.set_spacing(0); 30 | hseparator_fixed!(col, col.w() - dimm::border()*2, dimm::border_half()); 31 | column!(col_bottom, 32 | row!(row_bottom, 33 | fixed!(row_bottom, btn_back, shared::fltk::button::rect::back(), dimm::width_button_rec()); 34 | ); 35 | col_bottom.fixed(&row_bottom, dimm::height_button_rec()); 36 | ); 37 | col.fixed(&col_bottom, dimm::height_button_rec()); 38 | ); 39 | // Title 40 | let mut frame_title = frame_title.clone(); 41 | frame_title.set_label("Menu"); 42 | // Footer button 43 | let mut btn_back = btn_back.clone(); 44 | btn_back.emit(tx, Msg::DrawCover); 45 | hover_blink!(btn_back); 46 | // Entries 47 | rescope!(col_content, 48 | let f_make_entry = #[clown] move |label : &str| 49 | { 50 | let entry = fltk::button::ToggleButton::default() 51 | .with_size(0, dimm::height_button_wide() + dimm::border_half()) 52 | .with_frame(FrameType::FlatBox) 53 | .with_color(Color::BackGround) 54 | .with_color_selected(Color::BackGround.lighter()) 55 | .with_align(Align::Left | Align::Inside) 56 | .with_label(&format!(" {}", label)); 57 | hover_blink!(entry); 58 | honk!(col_content).clone().fixed(&mut entry.as_base_widget(), entry.h()); 59 | entry 60 | }; 61 | // Environment 62 | f_make_entry("Environment").emit(tx, Msg::DrawEnv); 63 | // Executables 64 | if let Ok(str_platform) = std::env::var("GIMG_PLATFORM") 65 | && let Ok(platform) = common::Platform::from_str(&str_platform) 66 | && platform == common::Platform::WINE 67 | { 68 | f_make_entry("Executable Configuration").emit(tx, Msg::DrawEnablerExecutable); 69 | } 70 | ); 71 | } // fn: new }}} 72 | 73 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 74 | -------------------------------------------------------------------------------- /gui/launcher/src/frame/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cover; 2 | pub mod selector_game; 3 | pub mod fail; 4 | pub mod menu; 5 | pub mod selector_executable; 6 | -------------------------------------------------------------------------------- /gui/launcher/src/frame/selector_game.rs: -------------------------------------------------------------------------------- 1 | use fltk::prelude::*; 2 | use fltk::{ 3 | group, 4 | enums, 5 | app::Sender, 6 | button::Button, 7 | frame::Frame, 8 | }; 9 | 10 | use shared::dimm; 11 | use shared::fltk::WidgetExtExtra; 12 | use shared::fltk::SenderExt; 13 | 14 | use crate::games; 15 | use crate::common; 16 | use common::Msg; 17 | use shared::{fixed,row,column,hpack,scroll,hover_blink,hseparator_fixed,rescope}; 18 | 19 | // fn: new {{{ 20 | pub fn new(tx : Sender) 21 | { 22 | // Layout 23 | column!(col, 24 | col.set_margin(dimm::border_half()); 25 | fixed!(col, frame_title, Frame::default(), dimm::height_text()); 26 | hseparator_fixed!(col, col.w() - dimm::border()*2, dimm::border_half()); 27 | // Content 28 | scroll!(scroll, 29 | hpack!(col_scroll,); 30 | col_scroll.set_spacing(0); 31 | col_scroll.set_size(0,0); 32 | ); 33 | hseparator_fixed!(col, col.w() - dimm::border()*2, dimm::border_half()); 34 | column!(col_bottom, 35 | row!(row_bottom, 36 | row_bottom.add(&Frame::default()); 37 | fixed!(row_bottom, btn_home, &shared::fltk::button::rect::home(), dimm::width_button_rec()); 38 | row_bottom.add(&Frame::default()); 39 | ); 40 | col_bottom.fixed(&row_bottom, dimm::height_button_rec()); 41 | ); 42 | col.fixed(&col_bottom, dimm::height_button_rec()); 43 | ); 44 | 45 | // Title 46 | let mut frame_title = frame_title.clone(); 47 | frame_title.set_label("Switch Game"); 48 | // Scroll resize callback 49 | scroll.resize_callback({let mut c = col_scroll.clone(); move |_,_,_,w,_| 50 | { 51 | c.resize(c.x(),c.y(),w-dimm::border_half()*3,c.h()); 52 | }}); 53 | scroll.set_type(group::ScrollType::VerticalAlways); 54 | // Configure buttons 55 | let mut btn_home = btn_home.clone(); 56 | btn_home.set_color(enums::Color::Blue); 57 | btn_home.emit(tx, Msg::DrawCover); 58 | hover_blink!(btn_home); 59 | 60 | // Create entries 61 | rescope!(col_scroll, 62 | for game in games::games().unwrap_or_default() 63 | { 64 | // Set entry name 65 | let osstr_name_file = game.path_root.file_name().unwrap_or_default(); 66 | let str_name_file = osstr_name_file.to_str().unwrap_or_default(); 67 | let entry = Button::default() 68 | .with_size(0, dimm::height_button_wide() + dimm::border_half()) 69 | .with_label(&str_name_file) 70 | .with_frame(enums::FrameType::FlatBox) 71 | .with_color(enums::Color::BackGround) 72 | .with_color_selected(enums::Color::BackGround.lighter()) 73 | .with_align(enums::Align::Left | enums::Align::Inside) 74 | .with_callback(move |_| { games::select(&game); tx.send_awake(Msg::DrawCover); }); 75 | hover_blink!(entry); 76 | col_scroll.add(&entry); 77 | } // for 78 | ); 79 | 80 | } // fn: new }}} 81 | 82 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 83 | -------------------------------------------------------------------------------- /gui/launcher/src/games.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::fs::DirEntry; 3 | use std::path::PathBuf; 4 | 5 | use shared::std::PathBufExt; 6 | 7 | use anyhow::anyhow as ah; 8 | 9 | use crate::common; 10 | use crate::db; 11 | 12 | pub struct Game 13 | { 14 | pub platform : common::Platform, 15 | pub path_root : PathBuf, 16 | pub path_icon : PathBuf, 17 | pub path_icon_grayscale : PathBuf, 18 | pub path_boot : PathBuf, 19 | } // Game 20 | 21 | // pub fn launch() {{{ 22 | pub fn launch() 23 | { 24 | let _ = std::process::Command::new("sh") 25 | .args(["-c", "$GIMG_LAUNCHER_BOOT"]) 26 | .stdout(std::process::Stdio::inherit()) 27 | .stderr(std::process::Stdio::inherit()) 28 | .output(); 29 | } // fn: launch }}} 30 | 31 | // pub fn select() {{{ 32 | pub fn select(game: &Game) 33 | { 34 | std::env::set_var("GIMG_PLATFORM", game.platform.as_str()); 35 | std::env::set_var("GIMG_LAUNCHER_BOOT", game.path_boot.to_str().unwrap_or("")); 36 | std::env::set_var("GIMG_LAUNCHER_ROOT", game.path_root.to_str().unwrap_or("")); 37 | std::env::set_var("GIMG_LAUNCHER_IMG", game.path_icon.to_str().unwrap_or("")); 38 | std::env::set_var("GIMG_LAUNCHER_IMG_GRAYSCALE", game.path_icon_grayscale.to_str().unwrap_or("")); 39 | } // fn: select }}} 40 | 41 | // pub fn select_by_index() {{{ 42 | pub fn select_by_index(index: usize) -> anyhow::Result<()> 43 | { 44 | select(games()?.get(index).ok_or(ah!("Index out of bounds"))?); 45 | Ok(()) 46 | } // fn: select_by_index }}} 47 | 48 | // fn game() {{{ 49 | fn game(path_root : PathBuf) -> anyhow::Result 50 | { 51 | let db_project = db::project::read(&path_root.join("gameimage.json"))?; 52 | let path_icon = path_root.join("icon/icon.png"); 53 | let path_icon_grayscale = path_root.join("icon").join("icon.grayscale.png"); 54 | let path_boot = path_root.join("boot"); 55 | let platform = common::Platform::from_str(&db_project.platform)?; 56 | if path_icon.exists() && path_boot.exists() 57 | { 58 | return Ok(Game{ platform, path_boot, path_root, path_icon, path_icon_grayscale }) 59 | } // if 60 | Err(ah!("Could not include project from '{}'", path_root.string())) 61 | } // fn game() }}} 62 | 63 | // pub fn games() {{{ 64 | pub fn games() -> anyhow::Result> 65 | { 66 | let vec_entries : Vec = fs::read_dir("/opt/gameimage-games")? 67 | .filter_map(|e| { e.ok() }) 68 | .filter(|e|{ e.path().is_dir() }) 69 | .collect(); 70 | 71 | let mut vec_data : Vec = vec![]; 72 | 73 | for entry in vec_entries 74 | { 75 | match game(entry.path()) 76 | { 77 | Ok(game) => vec_data.push(game), 78 | Err(e) => eprintln!("{}", e), 79 | } 80 | } // for 81 | 82 | // Sort 83 | vec_data.sort_by(|a, b| return a.path_root.string().partial_cmp(&b.path_root.string()).unwrap()); 84 | 85 | Ok(vec_data) 86 | } // games() }}} 87 | 88 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 89 | -------------------------------------------------------------------------------- /gui/launcher/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains, proc_macro_hygiene, stmt_expr_attributes)] 2 | 3 | // Gui 4 | use fltk::{ 5 | app, 6 | prelude::*, 7 | window::Window, 8 | enums::FrameType, 9 | }; 10 | 11 | use shared::svg; 12 | 13 | mod games; 14 | mod frame; 15 | mod common; 16 | mod db; 17 | 18 | use common::Msg; 19 | 20 | use shared::dimm; 21 | use shared::std::PathBufExt; 22 | use clap::Parser; 23 | 24 | // struct: Gui {{{ 25 | #[derive(Debug)] 26 | struct Gui 27 | { 28 | app: fltk::app::App, 29 | wind: Window, 30 | rx : fltk::app::Receiver, 31 | tx : fltk::app::Sender, 32 | } // struct: Gui }}} 33 | 34 | // impl: Gui {{{ 35 | impl Gui 36 | { 37 | 38 | // fn: new {{{ 39 | pub fn new() -> Self 40 | { 41 | let app = app::App::default().with_scheme(app::Scheme::Gtk); 42 | app::set_frame_type(FrameType::BorderBox); 43 | app::set_font_size(dimm::height_text()); 44 | let mut wind = Window::default() 45 | .with_label("GameImage") 46 | .with_size(dimm::width_launcher(), dimm::height_launcher()) 47 | .center_screen(); 48 | wind.make_resizable(false); 49 | 50 | shared::fltk::theme(); 51 | 52 | // Window icon 53 | if let Some(image) = fltk::image::SvgImage::from_data(svg::ICON_GAMEIMAGE).ok() 54 | { 55 | wind.set_icon(Some(image)); 56 | } // if 57 | else 58 | { 59 | println!("Failed to load icon image"); 60 | } // else 61 | 62 | let (tx, rx) = fltk::app::channel(); 63 | 64 | Gui 65 | { 66 | app, 67 | wind, 68 | rx, 69 | tx, 70 | } 71 | } // fn: new }}} 72 | 73 | // fn redraw() {{{ 74 | fn redraw(&mut self, msg: Msg) 75 | { 76 | self.wind.clear(); 77 | self.wind.begin(); 78 | 79 | match Some(msg) 80 | { 81 | Some(Msg::DrawCover) => frame::cover::new(self.tx), 82 | Some(Msg::DrawSelectorGame) => frame::selector_game::new(self.tx), 83 | Some(Msg::DrawSelectorExecutable) => frame::selector_executable::new(self.tx), 84 | Some(Msg::DrawEnablerExecutable) => frame::menu::enabler_executable::new(self.tx), 85 | Some(Msg::DrawEnv) => frame::menu::environment::new(self.tx), 86 | Some(Msg::DrawMenu) => frame::menu::new(self.tx), 87 | _ => (), 88 | } 89 | self.wind.end(); 90 | } // fn: redraw }}} 91 | 92 | // init() {{{ 93 | fn init(&mut self) 94 | { 95 | let vec_games = match games::games() 96 | { 97 | Ok(vec_games) => vec_games, 98 | Err(_) => { frame::fail::new(); vec![] } 99 | }; // match 100 | 101 | // Fetch game entries 102 | if vec_games.is_empty() 103 | { 104 | frame::fail::new(); 105 | } // if 106 | else 107 | { 108 | // Create initial cover frame 109 | self.tx.send(common::Msg::DrawCover); 110 | // Select the first game as the current 111 | games::select(vec_games.first().unwrap()); 112 | } // else 113 | 114 | // Show window 115 | self.wind.end(); 116 | self.wind.show(); 117 | 118 | while self.app.wait() 119 | { 120 | match self.rx.recv() 121 | { 122 | Some(common::Msg::WindActivate) => 123 | { 124 | shared::fltk::set_active(self.wind.clone(), true); 125 | app::awake(); 126 | } 127 | Some(common::Msg::WindDeactivate) => 128 | { 129 | shared::fltk::set_active(self.wind.clone(), false); 130 | app::awake(); 131 | } 132 | Some(Msg::Quit) => 133 | { 134 | app::quit(); 135 | app::flush(); 136 | } 137 | Some(value) => self.redraw(value), 138 | None => (), 139 | } // match 140 | } // while 141 | } // init() }}} 142 | 143 | } // impl: Gui }}} 144 | 145 | // struct Cli {{{ 146 | #[derive(Parser)] 147 | #[command(version, about, long_about = None)] 148 | struct Cli 149 | { 150 | #[arg(long, value_name = "INDEX")] 151 | select_index: Option, 152 | #[arg(long)] 153 | select_list: bool, 154 | } // struct Cli }}} 155 | 156 | // fn: main {{{ 157 | fn main() -> anyhow::Result<()> 158 | { 159 | let args = Cli::parse(); 160 | 161 | // Launch game directly 162 | if let Some(index) = args.select_index 163 | { 164 | match games::select_by_index(index as usize) 165 | { 166 | Ok(()) => games::launch(), 167 | Err(e) => { eprintln!("Could not select index '{}': '{}'", index, e); } 168 | } // match 169 | return Ok(()); 170 | } // if 171 | else if args.select_list 172 | { 173 | for (index, game) in games::games()?.into_iter().enumerate() 174 | { 175 | println!("{}: {}", index, game.path_root.file_name_string()); 176 | } // for 177 | return Ok(()); 178 | } // else if 179 | 180 | // Start GUI 181 | Gui::new().init(); 182 | 183 | Ok(()) 184 | } // }}} 185 | 186 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 187 | -------------------------------------------------------------------------------- /gui/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | fltk = "^1.4" 9 | fltk-theme = "0.6.0" 10 | fltk-evented = "0.5" 11 | display-info = "0.4.4" 12 | image = "0.24.8" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | -------------------------------------------------------------------------------- /gui/shared/src/db/kv.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::fs::File; 3 | use std::path::PathBuf; 4 | use std::collections::HashMap; 5 | 6 | pub type Kv = HashMap; 7 | 8 | // fn open() {{{ 9 | fn open(db: &PathBuf) -> anyhow::Result 10 | { 11 | Ok(serde_json::from_reader(File::open(db)?)?) 12 | } // fn open() }}} 13 | 14 | // pub fn read() {{{ 15 | pub fn read(db : &PathBuf) -> anyhow::Result 16 | { 17 | open(db) 18 | } // fn: read }}} 19 | 20 | // pub fn write() {{{ 21 | pub fn write(db : &PathBuf, key: &String, val: &String) -> anyhow::Result<()> 22 | { 23 | // Open existing or create an empty database 24 | let mut kv : Kv = open(db).unwrap_or(Kv::default()); 25 | 26 | // Append 27 | kv.insert(key.clone(), val.clone()); 28 | 29 | // Write to file 30 | write!(File::create(&db)?, "{}", serde_json::to_string(&kv)?)?; 31 | 32 | Ok(()) 33 | } // fn: write }}} 34 | 35 | // pub fn erase() {{{ 36 | pub fn erase(db : &PathBuf, key: String) -> anyhow::Result<()> 37 | { 38 | // Read current 39 | let mut kv = read(db)?; 40 | 41 | // Erase key 42 | kv.remove(&key); 43 | 44 | // Write to file 45 | write!(File::create(db)?, "{}", serde_json::to_string(&kv)?)?; 46 | 47 | Ok(()) 48 | } // erase() }}} 49 | 50 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 51 | -------------------------------------------------------------------------------- /gui/shared/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod kv; 2 | -------------------------------------------------------------------------------- /gui/shared/src/dimm.rs: -------------------------------------------------------------------------------- 1 | const HEIGHT_WIZARD : i32 = 500; 2 | const WIDTH_WIZARD : i32 = 500; 3 | const HEIGHT_LAUNCHER : i32 = 450; 4 | const WIDTH_LAUNCHER : i32 = 300; 5 | const BORDER : i32 = 10; 6 | const BORDER_HALF : i32 = BORDER / 2; 7 | 8 | const HEIGHT_BUTTON_WIDE : i32 = 30; 9 | const WIDTH_BUTTON_WIDE : i32 = HEIGHT_BUTTON_WIDE*2; 10 | 11 | const HEIGHT_BUTTON_REC : i32 = HEIGHT_BUTTON_WIDE; 12 | const WIDTH_BUTTON_REC : i32 = WIDTH_BUTTON_WIDE/2; 13 | 14 | const HEIGHT_TEXT : i32 = 14; 15 | 16 | const HEIGHT_STATUS : i32 = (HEIGHT_WIZARD as f64 * 0.05) as i32; 17 | 18 | const HEIGHT_SEP : i32 = 1; 19 | 20 | const HEIGHT_HEADER : i32 = HEIGHT_BUTTON_WIDE + BORDER*2 + HEIGHT_SEP; 21 | const HEIGHT_FOOTER : i32 = HEIGHT_BUTTON_WIDE + BORDER*2 + HEIGHT_STATUS; 22 | 23 | const HEIGHT_BAR : i32 = HEIGHT_BUTTON_WIDE + BORDER; 24 | 25 | const POSY_FOOTER : i32 = HEIGHT_WIZARD - HEIGHT_FOOTER; 26 | 27 | macro_rules! function_scale 28 | { 29 | ($func_name:ident, $baseline:expr) => 30 | { 31 | pub fn $func_name() -> i32 32 | { 33 | return $baseline; 34 | } 35 | } 36 | } 37 | 38 | function_scale!(height_wizard, HEIGHT_WIZARD); 39 | function_scale!(width_wizard, WIDTH_WIZARD); 40 | function_scale!(height_launcher, HEIGHT_LAUNCHER); 41 | function_scale!(width_launcher, WIDTH_LAUNCHER); 42 | function_scale!(border, BORDER); 43 | function_scale!(border_half, BORDER_HALF); 44 | 45 | function_scale!(height_button_wide, HEIGHT_BUTTON_WIDE); 46 | function_scale!(width_button_wide, WIDTH_BUTTON_WIDE); 47 | 48 | function_scale!(height_button_rec, HEIGHT_BUTTON_REC); 49 | function_scale!(width_button_rec, WIDTH_BUTTON_REC); 50 | 51 | function_scale!(height_text_header, HEIGHT_TEXT*2); 52 | function_scale!(height_text, HEIGHT_TEXT); 53 | 54 | function_scale!(height_status, HEIGHT_STATUS); 55 | 56 | function_scale!(height_sep, HEIGHT_SEP); 57 | 58 | function_scale!(height_header, HEIGHT_HEADER); 59 | function_scale!(height_footer, HEIGHT_FOOTER); 60 | 61 | function_scale!(posy_footer, POSY_FOOTER); 62 | 63 | function_scale!(bar, HEIGHT_BAR); 64 | 65 | pub fn width_checkbutton() -> i32 { 18 } 66 | 67 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 68 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/dialog.rs: -------------------------------------------------------------------------------- 1 | use fltk:: 2 | { 3 | frame::Frame, 4 | window::Window, 5 | prelude::*, 6 | enums::{Align,Color}, 7 | }; 8 | 9 | use crate::dimm; 10 | use crate::svg; 11 | use crate::hover_blink; 12 | 13 | #[derive(Clone)] 14 | pub struct KeyValue 15 | { 16 | pub wind : Window, 17 | pub input_key : fltk::input::Input, 18 | pub input_value : fltk::input::Input, 19 | pub btn_ok : fltk::button::Button, 20 | } 21 | 22 | pub fn key_value() -> KeyValue 23 | { 24 | let mut wind = Window::default().with_size( 25 | dimm::width_button_wide() * 4 + dimm::border() * 3 26 | , dimm::height_button_wide() * 3 + dimm::border() * 4 27 | ); 28 | // Window should be de-attached from other windows 29 | if let Some(mut parent) = wind.parent() 30 | { 31 | parent.remove(&wind); 32 | } // if 33 | // Window icon 34 | if let Some(image) = fltk::image::SvgImage::from_data(svg::ICON_GAMEIMAGE).ok() 35 | { 36 | wind.set_icon(Some(image.clone())); 37 | } // if 38 | wind.begin(); 39 | let input_key = fltk::input::Input::default() 40 | .with_pos(wind.w() - dimm::width_button_wide()*3 - dimm::border(), dimm::border()) 41 | .with_size(dimm::width_button_wide()*3, dimm::height_button_wide()) 42 | .with_align(Align::Left); 43 | let _label_key = Frame::default() 44 | .with_size(dimm::width_button_wide(), dimm::height_button_wide()) 45 | .left_of(&input_key, dimm::border()) 46 | .with_align(Align::Inside | Align::Left) 47 | .with_label("Key"); 48 | let input_value = fltk::input::Input::default() 49 | .below_of(&input_key, dimm::border()) 50 | .with_size(input_key.w(), input_key.h()) 51 | .with_align(input_key.align()); 52 | let label_value = Frame::default() 53 | .with_size(dimm::width_button_wide(), dimm::height_button_wide()) 54 | .left_of(&input_value, dimm::border()) 55 | .with_align(Align::Inside | Align::Left) 56 | .with_label("Value"); 57 | let mut btn_ok = crate::fltk::button::wide::default() 58 | .with_size(dimm::width_button_wide(), dimm::height_button_wide()) 59 | .below_of(&label_value, dimm::border()) 60 | .with_label("OK"); 61 | hover_blink!(btn_ok); 62 | btn_ok.set_pos(wind.w() / 2 - btn_ok.w() / 2, btn_ok.y()); 63 | btn_ok.set_color(Color::Green); 64 | wind.end(); 65 | 66 | KeyValue{ wind, input_key, input_value, btn_ok } 67 | } 68 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 69 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/frame.rs: -------------------------------------------------------------------------------- 1 | use fltk:: 2 | { 3 | draw, 4 | enums::Color, 5 | prelude::*, 6 | frame::Frame, 7 | enums::FrameType 8 | }; 9 | 10 | use crate::dimm; 11 | use crate::fltk::WidgetExtExtra; 12 | 13 | pub fn bordered() -> Frame 14 | { 15 | let mut frame = Frame::default() 16 | .with_frame(FrameType::FlatBox) 17 | .with_color(Color::White); 18 | 19 | frame.draw(move |f| 20 | { 21 | let (x,y,w,h) = (f.x(),f.y(),f.w(),f.h()); 22 | draw::draw_rect_fill(x, y, w, h, f.color()); 23 | draw::draw_rect_fill(x + dimm::height_sep() 24 | , y + dimm::height_sep() 25 | , w - dimm::height_sep()*2 26 | , h - dimm::height_sep()*2 27 | , Color::BackGround 28 | ); 29 | // Draw Image 30 | if let Some(mut image) = f.image() 31 | { 32 | let img_w = image.width(); 33 | let img_h = image.height(); 34 | let img_x = x + (w - img_w) / 2; // Center horizontally 35 | let img_y = y + (h - img_h) / 2; // Center vertically 36 | image.draw(img_x, img_y, img_w, img_h); 37 | } 38 | // Draw label 39 | draw::set_draw_color(Color::Foreground); 40 | draw::draw_text2(&f.label(), f.x(), f.y(), f.w(), f.h(), f.align()); 41 | }); 42 | 43 | frame 44 | } 45 | 46 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 47 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! fit_to_children_height 3 | { 4 | ($widget:ident) => 5 | { 6 | { 7 | let parent = $widget.parent().unwrap(); 8 | let spacing = $widget.spacing(); 9 | let height = $widget.bounds() 10 | .iter() 11 | .map(|e| e.3).sum::() + (($widget.children()-1) * spacing); 12 | $widget.resize($widget.x() 13 | , $widget.y() 14 | , $widget.w() 15 | , height.min(parent.h()).min($widget.height()) 16 | ); 17 | height 18 | } 19 | }; 20 | } 21 | 22 | #[macro_export] 23 | macro_rules! rescope 24 | { 25 | ($row_name:ident, $($body:tt)*) => 26 | { 27 | $row_name.begin(); 28 | $($body)* 29 | $row_name.end(); 30 | }; 31 | } 32 | 33 | #[macro_export] 34 | macro_rules! group 35 | { 36 | ($col_name:ident, $($body:tt)*) => 37 | { 38 | let mut $col_name = fltk::group::Group::default_fill(); 39 | $($body)* 40 | $col_name.end(); 41 | }; 42 | } 43 | 44 | #[macro_export] 45 | macro_rules! tabs 46 | { 47 | ($col_name:ident, $($body:tt)*) => 48 | { 49 | let mut $col_name = fltk::group::Tabs::default_fill(); 50 | $($body)* 51 | $col_name.end(); 52 | }; 53 | } 54 | 55 | #[macro_export] 56 | macro_rules! hpack 57 | { 58 | ($col_name:ident, $($body:tt)*) => 59 | { 60 | let mut $col_name = fltk::group::Pack::default_fill(); 61 | $col_name.set_type(fltk::group::PackType::Vertical); 62 | $($body)* 63 | $col_name.end(); 64 | }; 65 | } 66 | 67 | #[macro_export] 68 | macro_rules! column 69 | { 70 | ($col_name:ident, $($body:tt)*) => 71 | { 72 | let mut $col_name = fltk::group::Flex::default_fill().column(); 73 | $($body)* 74 | $col_name.end(); 75 | }; 76 | } 77 | 78 | #[macro_export] 79 | macro_rules! row 80 | { 81 | ($row_name:ident, $($body:tt)*) => 82 | { 83 | let mut $row_name = fltk::group::Flex::default_fill().row(); 84 | $($body)* 85 | $row_name.end(); 86 | }; 87 | } 88 | 89 | #[macro_export] 90 | macro_rules! scroll 91 | { 92 | ($scroll_name:ident, $($body:tt)*) => 93 | { 94 | let mut $scroll_name = fltk::group::Scroll::default_fill(); 95 | $scroll_name.set_scrollbar_size(dimm::border()); 96 | $($body)* 97 | $scroll_name.end(); 98 | }; 99 | } 100 | 101 | #[macro_export] 102 | macro_rules! hseparator_fixed 103 | { 104 | ($col_name:ident, $width:expr, $height: expr) => 105 | {{ 106 | column!(col_inner, 107 | col_inner.add(&Frame::default()); 108 | let sep = shared::fltk::separator::horizontal($width); 109 | col_inner.fixed(&sep, sep.h()); 110 | col_inner.add(&Frame::default()); 111 | ); 112 | $col_name.fixed(&col_inner, $height); 113 | }} 114 | } 115 | 116 | #[macro_export] 117 | macro_rules! hseparator 118 | { 119 | ($col_name:ident, $width:expr) => 120 | {{ 121 | column!(col_inner, 122 | col_inner.add(&Frame::default()); 123 | let sep = shared::fltk::separator::horizontal($width); 124 | col_inner.fixed(&sep, sep.h()); 125 | col_inner.add(&Frame::default()); 126 | ); 127 | $col_name.add(&col_inner); 128 | }} 129 | } 130 | 131 | #[macro_export] 132 | macro_rules! fixed 133 | { 134 | ($col_name:ident, $name:ident, $widget:expr, $size:expr) => 135 | { 136 | let $name = $widget.clone(); 137 | $col_name.fixed(&$name, $size); 138 | } 139 | } 140 | 141 | #[macro_export] 142 | macro_rules! add 143 | { 144 | ($col_name:ident, $name:ident, $widget:expr) => 145 | { 146 | let $name = $widget.clone(); 147 | $col_name.add(&$name); 148 | } 149 | } 150 | 151 | #[macro_export] 152 | macro_rules! col_center 153 | { 154 | ($widget:expr) => 155 | {{ 156 | let mut col = fltk::group::Flex::default_fill().column(); 157 | col.add(&fltk::frame::Frame::default()); 158 | let widget = $widget.clone(); 159 | col.fixed(&widget, widget.w()); 160 | col.add(&fltk::frame::Frame::default()); 161 | col.end(); 162 | (col,widget) 163 | }} 164 | } 165 | 166 | #[macro_export] 167 | macro_rules! row_center 168 | { 169 | ($widget:expr) => 170 | {{ 171 | let mut row = fltk::group::Flex::default_fill().row(); 172 | row.add(&fltk::frame::Frame::default()); 173 | let widget = $widget.clone(); 174 | row.fixed(&widget, widget.w()); 175 | row.add(&fltk::frame::Frame::default()); 176 | row.end(); 177 | (row,widget) 178 | }} 179 | } 180 | 181 | #[macro_export] 182 | macro_rules! hover_blink 183 | { 184 | ($button:ident) => 185 | { 186 | $button.clone().handle(|b,ev| match ev { 187 | fltk::enums::Event::Enter => { b.set_value(true); true}, 188 | fltk::enums::Event::Leave => { b.set_value(false); true}, 189 | _ => false, 190 | }); 191 | }; 192 | } 193 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 194 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/paginator.rs: -------------------------------------------------------------------------------- 1 | use fltk::{ 2 | prelude::*, 3 | frame, 4 | output, 5 | }; 6 | 7 | use crate::{dimm,column,row,hover_blink}; 8 | 9 | // pub fn paginator() {{{ 10 | pub fn paginator(mut get_page: F, mut set_page: G, mut count_pages: H) -> fltk::group::Flex 11 | where F: Clone + FnMut() -> usize, 12 | G: 'static + Clone + FnMut(usize), 13 | H: 'static + Clone + FnMut() -> usize 14 | { 15 | column!(col, 16 | row!(row, 17 | let mut btn_prev = crate::fltk::button::rect::arrow_backward(); 18 | row.fixed(&btn_prev, dimm::width_button_rec()); 19 | row.add(&frame::Frame::default()); 20 | row!(row_pages, 21 | let mut input_page : fltk_evented::Listener<_> = fltk::input::Input::default().into(); 22 | row_pages.fixed(&input_page.as_base_widget(), dimm::width_button_rec()); 23 | row_pages.fixed(&frame::Frame::default().with_label("/"), dimm::border()); 24 | let mut output_pages = output::Output::default(); 25 | row_pages.fixed(&output_pages, dimm::width_button_rec()); 26 | let _ = output_pages.insert(&count_pages().to_string()); 27 | ); 28 | row.fixed(&row_pages, dimm::width_button_rec()*2 + dimm::border()); 29 | row.add(&frame::Frame::default()); 30 | let mut btn_next = crate::fltk::button::rect::arrow_forward(); 31 | row.fixed(&btn_next, dimm::width_button_rec()); 32 | ); 33 | col.fixed(&row, dimm::height_button_wide()); 34 | ); 35 | // Adjust size 36 | col.resize(col.x(), col.y(), col.w(), dimm::height_button_wide()); 37 | // Check for bounds 38 | if get_page() > count_pages() { set_page(0); } // if 39 | // Configure hover 40 | hover_blink!(btn_prev); 41 | hover_blink!(btn_next); 42 | // Configure callback 43 | input_page.set_value(&get_page().to_string()); 44 | input_page.on_keydown({ 45 | let mut set_page = set_page.clone(); 46 | move |e| 47 | { 48 | // Filter non-digit values 49 | e.set_value(&e.value().chars().filter(|c| "1234567890".contains(*c)).collect::()); 50 | // Make sure it does not stay empty 51 | if e.value().is_empty() { e.set_value("0"); } // if 52 | // Remove leading zeroes 53 | while e.value().starts_with("0") && e.value().len() > 1 { e.set_value(&e.value()[1..]); } 54 | // Set page 55 | if fltk::app::event_key() == fltk::enums::Key::Enter 56 | { 57 | set_page(e.value().parse::().unwrap_or_default()); 58 | } // if 59 | } 60 | }); 61 | btn_prev.set_callback({ 62 | let input_page = input_page.clone(); 63 | let mut set_page = set_page.clone(); 64 | move |_| 65 | { 66 | let value = input_page.value().parse::().unwrap_or_default(); 67 | set_page(if value == 0 { 0 } else { value-1 }); 68 | } 69 | }); 70 | btn_next.set_callback({ 71 | let input_page = input_page.clone(); 72 | let mut set_page = set_page.clone(); 73 | move |_| 74 | { 75 | let value = input_page.value().parse::().unwrap_or_default(); 76 | set_page((value + 1).min(count_pages())); 77 | } 78 | }); 79 | col 80 | } // pub fn paginator() }}} 81 | 82 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 83 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/progress.rs: -------------------------------------------------------------------------------- 1 | use fltk:: 2 | { 3 | draw, 4 | enums::Color, 5 | misc::Progress, 6 | prelude::*, 7 | }; 8 | 9 | pub fn progress() -> Progress 10 | { 11 | let mut frame = Progress::default(); 12 | 13 | frame.draw(move |f| 14 | { 15 | let (x,y,w,h) = (f.x(),f.y(),f.w(),f.h()); 16 | let cs = f.selection_color(); 17 | let cf = f.color(); 18 | let value = f.value().clamp(f.minimum(), f.maximum()) as f32 / 100.0; 19 | let filled_width = (w as f32 * value).round() as i32; 20 | 21 | draw::draw_rect_fill(x, y, w, h, cf); 22 | draw::draw_rect_fill(x, y, filled_width, h, cs); 23 | draw::draw_rect_with_color(x, y, w, h, Color::BackGround.lighter()); 24 | draw::set_draw_color(Color::ForeGround); 25 | draw::draw_text2(&f.label() , x , y , w , h , f.align()); 26 | }); 27 | 28 | frame 29 | } 30 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 31 | -------------------------------------------------------------------------------- /gui/shared/src/fltk/separator.rs: -------------------------------------------------------------------------------- 1 | use fltk:: 2 | { 3 | draw, 4 | enums::Color, 5 | prelude::*, 6 | frame::Frame, 7 | enums::FrameType 8 | }; 9 | 10 | use crate::dimm; 11 | use crate::fltk::WidgetExtExtra; 12 | 13 | pub fn vertical(height: i32) -> Frame 14 | { 15 | let mut frame = Frame::default() 16 | .with_size(dimm::height_sep(), height) 17 | .with_frame(FrameType::FlatBox) 18 | .with_color(Color::White); 19 | 20 | frame.draw(move |f| 21 | { 22 | let (x,y,w,h) = (f.x(),f.y(),f.w(),f.h()); 23 | draw::draw_rect_fill(x, y, w, h, f.color()); 24 | }); 25 | 26 | frame 27 | } 28 | 29 | pub fn horizontal(width: i32) -> Frame 30 | { 31 | let mut frame = Frame::default() 32 | .with_size(width, dimm::height_sep()) 33 | .with_frame(FrameType::FlatBox) 34 | .with_color(Color::White); 35 | 36 | frame.draw(move |f| 37 | { 38 | let (x,y,w,h) = (f.x(),f.y(),f.w(),f.h()); 39 | draw::draw_rect_fill(x, y, w, h, f.color()); 40 | }); 41 | 42 | frame 43 | } 44 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 45 | 46 | -------------------------------------------------------------------------------- /gui/shared/src/image.rs: -------------------------------------------------------------------------------- 1 | // pub fn resize() {{{ 2 | pub fn resize(path_out : std::path::PathBuf, path_in : std::path::PathBuf, width : u32, height : u32) -> anyhow::Result<()> 3 | { 4 | let mut img = image::io::Reader::open(path_in)?.decode()?; 5 | img = img.resize(width, height, image::imageops::FilterType::CatmullRom); 6 | Ok(img.save(path_out)?) 7 | } // }}} 8 | 9 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 10 | -------------------------------------------------------------------------------- /gui/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(let_chains,proc_macro_hygiene, stmt_expr_attributes, once_cell_get_mut, concat_idents)] 2 | pub mod fltk; 3 | pub mod dimm; 4 | pub mod svg; 5 | pub mod std; 6 | pub mod image; 7 | pub mod db; 8 | 9 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 10 | -------------------------------------------------------------------------------- /gui/shared/src/std.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | 3 | 4 | // pub trait OsStrExt {{{ 5 | pub trait OsStrExt 6 | { 7 | fn string(&self) -> String; 8 | } 9 | 10 | impl OsStrExt for OsStr 11 | { 12 | fn string(&self) -> String 13 | { 14 | self.to_string_lossy().into_owned() 15 | } // fn: string 16 | } 17 | // }}} 18 | 19 | // pub trait OsStringExt {{{ 20 | pub trait OsStringExt 21 | { 22 | fn append(&mut self, val: &str) -> &mut Self; 23 | fn string(&self) -> String; 24 | } 25 | 26 | impl OsStringExt for OsString 27 | { 28 | fn append(&mut self, val: &str) -> &mut Self 29 | { 30 | self.push(val); 31 | self 32 | } 33 | fn string(&self) -> String 34 | { 35 | self.to_string_lossy().into_owned() 36 | } // fn: string 37 | } 38 | // }}} 39 | 40 | // pub trait PathBufExt {{{ 41 | #[allow(warnings)] 42 | pub trait PathBufExt 43 | { 44 | fn string(&self) -> String; 45 | fn append_extension(&self, val: &str) -> Self; 46 | fn prepend(&self, upper: &std::path::PathBuf) -> Self; 47 | fn file_name_string(&self) -> String; 48 | } 49 | 50 | impl PathBufExt for std::path::PathBuf 51 | { 52 | fn string(&self) -> String 53 | { 54 | self.clone().to_string_lossy().into_owned() 55 | } // fn: string 56 | 57 | fn append_extension(&self, val: &str) -> Self 58 | { 59 | std::path::PathBuf::from(self.clone().into_os_string().append(val).string()) 60 | } // fn: extend_extension 61 | 62 | fn prepend(&self, upper: &std::path::PathBuf) -> Self 63 | { 64 | upper.join(self) 65 | } // fn: prepend 66 | 67 | fn file_name_string(&self) -> String 68 | { 69 | self.file_name().unwrap_or_default().to_string_lossy().to_string() 70 | } // fn: file_name_string 71 | } 72 | // }}} 73 | 74 | // pub trait VecExt {{{ 75 | pub struct VecString 76 | { 77 | str_owned: Vec, 78 | } // VecString 79 | 80 | impl VecString 81 | { 82 | // Method to access the &str references of the owned strings 83 | pub fn as_str_slice(&self) -> Vec<&str> 84 | { 85 | self.str_owned.iter().map(|s| s.as_str()).collect() 86 | } // as_str_slice() 87 | } // impl VecString 88 | 89 | pub trait VecExt 90 | { 91 | fn append_strings(&self, other: Vec) -> VecString; 92 | } // trait VecExt 93 | 94 | impl VecExt for Vec<&str> 95 | { 96 | fn append_strings(&self, other: Vec) -> VecString 97 | { 98 | // Map self to String 99 | let mut str_owned: Vec = self.iter().map(|s| s.to_string()).collect(); 100 | 101 | // Extend self with other 102 | str_owned.extend(other); 103 | 104 | // Create VecString 105 | VecString { str_owned } 106 | } 107 | } // impl VecExt 108 | // }}} 109 | 110 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 111 | -------------------------------------------------------------------------------- /gui/wizard/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wizard" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | shared = { path = "../shared" } 10 | fltk = { version = "1.4", features = ["use-wayland"] } 11 | fltk-theme = "0.6.0" 12 | fltk-grid = "0.4" 13 | fltk-evented = "0.5" 14 | serde_yaml = "0.9" 15 | which = "7.0.0" 16 | closure = "0.3.0" 17 | clown = "1.1.0" 18 | walkdir = "2" 19 | url = "2.5.0" 20 | anyhow = "1.0" 21 | sha256 = "1.5.0" 22 | rust_search = "2.1.0" 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0" 25 | image_search = { version = "0.4.3", features = ["blocking"] } 26 | tokio = { version = "1", features = ["full"] } 27 | once_cell = "1.19.0" 28 | sysinfo = "0.30.7" 29 | regex = "1.10.3" 30 | libc = "0.2.153" 31 | errno = "0.3.8" 32 | lazy_static = "1.5.0" 33 | -------------------------------------------------------------------------------- /gui/wizard/src/db/fetch.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::path::PathBuf; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Clone, Serialize, Deserialize)] 8 | pub struct EntryEmulator 9 | { 10 | pub layer: String, 11 | } // Entry }}} 12 | 13 | #[derive(Clone, Serialize, Deserialize)] 14 | pub struct EntryWine 15 | { 16 | pub layer: HashMap, 17 | } // Entry }}} 18 | 19 | // struct Entry {{{ 20 | #[derive(Clone, Serialize, Deserialize)] 21 | pub struct Entry 22 | { 23 | pub version: String, 24 | pub linux: EntryEmulator, 25 | pub rpcs3: EntryEmulator, 26 | pub pcsx2: EntryEmulator, 27 | pub retroarch: EntryEmulator, 28 | pub wine: EntryWine, 29 | } // Entry }}} 30 | 31 | // read() {{{ 32 | pub fn read() -> anyhow::Result 33 | { 34 | // GIMG_DIR should contain the path to the build dir 35 | let path_file_db : PathBuf = env::var("GIMG_DIR")?.into(); 36 | 37 | // Try to open the gameimage.json file in it 38 | let file = File::open(path_file_db.join("fetch.json"))?; 39 | 40 | // Parse 41 | let entry : Entry = serde_json::from_reader(file)?; 42 | 43 | // Return with absolute paths 44 | Ok(entry) 45 | } // fn: read }}} 46 | 47 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 48 | -------------------------------------------------------------------------------- /gui/wizard/src/db/global.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | use std::collections::HashMap; 3 | use std::env; 4 | use std::fs::File; 5 | use std::path::PathBuf; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | // struct Entry {{{ 9 | #[derive(Clone, Serialize, Deserialize)] 10 | pub struct Entry 11 | { 12 | pub project: String, // path to default project 13 | pub path_dir_build: PathBuf, // path to build dir 14 | pub path_dir_cache: PathBuf, // path to build dir 15 | pub path_file_image: PathBuf, // path to main flatimage 16 | pub path_file_output: PathBuf, // path to output file 17 | pub dist_wine: String, // Current wine distribution 18 | pub projects: HashMap, 19 | } // Entry }}} 20 | 21 | // struct EntryDetails {{{ 22 | #[derive(Clone, Serialize, Deserialize)] 23 | pub struct EntryDetails { 24 | pub path_dir_project: PathBuf, 25 | pub path_dir_project_root: PathBuf, 26 | pub platform: String, 27 | } // EntryDetails }}} 28 | 29 | impl Entry 30 | { 31 | 32 | // get_project_dir() {{{ 33 | pub fn get_project_dir(&self, name_project : &str) -> anyhow::Result 34 | { 35 | Ok(self.projects.get(name_project).ok_or(ah!("Key '{}' not found in projects list", name_project))?.path_dir_project.clone()) 36 | } // get_project_dir() }}} 37 | 38 | } 39 | 40 | // get_current_project() {{{ 41 | pub fn get_current_project() -> anyhow::Result 42 | { 43 | let db_global = read()?; 44 | let name_project = db_global.project; 45 | Ok(db_global.projects.get(&name_project).ok_or(ah!("Project not found in projects list"))?.clone()) 46 | } // get_current_project() }}} 47 | 48 | // read() {{{ 49 | pub fn read() -> anyhow::Result 50 | { 51 | // GIMG_DIR should contain the path to the build dir 52 | let path_file_db : PathBuf = env::var("GIMG_DIR")?.into(); 53 | // Try to open the gameimage.json file in it 54 | let file = File::open(path_file_db.join("gameimage.json"))?; 55 | // Parse 56 | let entry : Entry = serde_json::from_reader(file)?; 57 | // Return with absolute paths 58 | Ok(entry) 59 | } // fn: read }}} 60 | 61 | // write() {{{ 62 | pub fn write(entry: &Entry) -> anyhow::Result<()> 63 | { 64 | // GIMG_DIR should contain the path to the build dir 65 | let path_file_db : PathBuf = env::var("GIMG_DIR")?.into(); 66 | // Try to open the gameimage.json file in it 67 | let file = File::create(path_file_db.join("gameimage.json"))?; 68 | // Parse 69 | serde_json::to_writer_pretty(file, entry)?; 70 | Ok(()) 71 | } // fn: write }}} 72 | 73 | // update() {{{ 74 | pub fn update(f: F) -> anyhow::Result<()> 75 | where F: FnOnce(Entry) -> Entry 76 | { 77 | // Read 78 | let entry = read()?; 79 | // Update 80 | let entry = f(entry); 81 | // Write 82 | write(&entry)?; 83 | Ok(()) 84 | } // fn: update }}} 85 | 86 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 87 | -------------------------------------------------------------------------------- /gui/wizard/src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project; 2 | pub mod global; 3 | pub mod fetch; 4 | -------------------------------------------------------------------------------- /gui/wizard/src/db/project.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | use std::fs::File; 3 | use std::path::PathBuf; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::db::global; 7 | use crate::common; 8 | use shared::std::PathBufExt; 9 | use crate::log; 10 | 11 | #[allow(dead_code)] 12 | pub enum EntryName 13 | { 14 | PathFileIcon, 15 | PathFileRom, 16 | PathFileCore, 17 | PathFileBios, 18 | } 19 | 20 | // struct Entry {{{ 21 | #[derive(Clone, Serialize, Deserialize)] 22 | pub struct Entry 23 | { 24 | project : String, 25 | platform : String, 26 | path_file_icon : Option, 27 | path_file_rom : Option, 28 | path_file_core : Option, 29 | path_file_bios : Option, 30 | } // Entry 31 | 32 | impl Entry 33 | { 34 | 35 | pub fn get_project(&self) -> String 36 | { 37 | self.project.clone() 38 | } // project 39 | 40 | pub fn get_platform(&self) -> String 41 | { 42 | self.platform.clone() 43 | } // project 44 | 45 | pub fn get_dir_self(&self) -> anyhow::Result 46 | { 47 | // Get the build dir 48 | let db = global::read()?; 49 | 50 | // Get project name 51 | let name_project = self.get_project(); 52 | 53 | // Return project dir 54 | Ok(db.get_project_dir(&name_project)?) 55 | } 56 | 57 | pub fn get_path_absolute(&self, entry: EntryName) -> anyhow::Result 58 | { 59 | // Get project dir == build_dir / project_name 60 | let project_dir_self = self.get_dir_self()?; 61 | 62 | let f_to_absolute = |entry : &Option| -> Option 63 | { 64 | match entry 65 | { 66 | Some(pathbuf) => Some(project_dir_self.join(pathbuf)), 67 | None => None, 68 | } 69 | }; 70 | 71 | let ok_path_file_absolute = match entry 72 | { 73 | EntryName::PathFileIcon => f_to_absolute(&self.path_file_icon), 74 | EntryName::PathFileRom => f_to_absolute(&self.path_file_rom), 75 | EntryName::PathFileCore => f_to_absolute(&self.path_file_core), 76 | EntryName::PathFileBios => f_to_absolute(&self.path_file_bios), 77 | }; // match 78 | 79 | Ok(ok_path_file_absolute.ok_or(ah!("Could not read absolute path"))?) 80 | } // dir_absolute 81 | 82 | pub fn get_path_relative(&self, entry: EntryName) -> anyhow::Result 83 | { 84 | let some_path_project_relative = match entry 85 | { 86 | EntryName::PathFileIcon => self.path_file_icon.clone(), 87 | EntryName::PathFileRom => self.path_file_rom.clone(), 88 | EntryName::PathFileCore => self.path_file_core.clone(), 89 | EntryName::PathFileBios => self.path_file_bios.clone(), 90 | }; // match 91 | 92 | Ok(some_path_project_relative.ok_or(ah!("Could not read relative path"))?) 93 | } // get_dir_relative 94 | 95 | } 96 | // struct Entry }}} 97 | 98 | pub type Entries = Vec; 99 | 100 | // list() {{{ 101 | 102 | // List all projects 103 | pub fn list() -> anyhow::Result 104 | { 105 | let mut entries : Entries = Vec::new(); 106 | let db_global = global::read()?; 107 | 108 | for project in db_global.projects.clone() 109 | { 110 | let (_, data) = project.clone(); 111 | 112 | // Expected json file 113 | let path_file_json = data.path_dir_project.join("gameimage.json"); 114 | 115 | // Open project file 116 | let file = match File::open(&path_file_json) 117 | { 118 | Ok(file) => file, 119 | Err(e) => { log!("Could not open file '{}' with error '{}'", path_file_json.string(), e); continue; } 120 | }; 121 | 122 | // Get project entry 123 | let entry : Entry = match serde_json::from_reader(file) 124 | { 125 | Ok(entry) => entry, 126 | Err(_) => continue, 127 | }; 128 | 129 | // Include in vec 130 | entries.push(entry); 131 | } // for 132 | 133 | Ok(entries) 134 | } // fn: list }}} 135 | 136 | // pub fn current() {{{ 137 | 138 | // Reads the current project entry 139 | pub fn current() -> anyhow::Result 140 | { 141 | // Get global info 142 | let global = global::read()?; 143 | 144 | // Get the current project 145 | let path_file_project = global.get_project_dir(&global.project)?.join("gameimage.json"); 146 | 147 | // Read file 148 | let file = File::open(&path_file_project)?; 149 | 150 | // Read entry 151 | Ok(serde_json::from_reader(file)?) 152 | } // current() }}} 153 | 154 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 155 | -------------------------------------------------------------------------------- /gui/wizard/src/frame/common.rs: -------------------------------------------------------------------------------- 1 | // Gui 2 | use fltk::prelude::*; 3 | use fltk::{ 4 | output::Output, 5 | frame::Frame, 6 | enums::{Align,Color}, 7 | }; 8 | 9 | use shared::fltk::WidgetExtExtra; 10 | use shared::{tabs,hover_blink,hseparator_fixed,column,row,add,fixed}; 11 | 12 | use crate::dimm; 13 | use crate::frame; 14 | 15 | // pub fn layout() {{{ 16 | pub fn layout() 17 | { 18 | column!(col, 19 | col.set_margin(dimm::border_half()); 20 | col.set_spacing(dimm::border_half()); 21 | row!(row_header, 22 | fixed!(row_header, btn_term, shared::fltk::button::rect::terminal().with_color(Color::Blue), dimm::width_button_rec()); 23 | add!(row_header, frame_title, Frame::default().with_id("header_title").with_align(Align::Inside | Align::Center)); 24 | fixed!(row_header, btn_resize, shared::fltk::button::rect::resize_down() 25 | .with_id("btn_resize") 26 | .with_color(Color::Blue), dimm::width_button_rec() 27 | ); 28 | ); 29 | col.fixed(&row_header, dimm::height_button_rec()); 30 | hseparator_fixed!(col, dimm::width_wizard() - dimm::border()*2, dimm::border_half()); 31 | tabs!(tab_content, 32 | column!(col_content_footer, 33 | col_content_footer.set_frame(fltk::enums::FrameType::FlatBox); 34 | col_content_footer.set_color(Color::BackGround); 35 | column!(group_content, group_content.set_id("content");); 36 | col_content_footer.add(&group_content); 37 | hseparator_fixed!(col_content_footer, dimm::width_wizard() - dimm::border()*2, dimm::border_half()); 38 | row!(footer, 39 | footer.set_id("footer"); 40 | fixed!(footer, btn_prev, shared::fltk::button::wide::default() 41 | .with_id("footer_prev") 42 | .with_label("Prev"), dimm::width_button_wide()); 43 | add!(footer, expand, Frame::default()); 44 | fixed!(footer, btn_next, shared::fltk::button::wide::default() 45 | .with_id("footer_next") 46 | .with_label("Next") 47 | .with_color(Color::Blue), dimm::width_button_wide()); 48 | ); 49 | col_content_footer.fixed(&footer, dimm::height_button_wide()); 50 | col_content_footer.fixed(&Output::default().with_id("footer_status"), 20); 51 | ); 52 | column!(col_content_term, 53 | col_content_term.set_frame(fltk::enums::FrameType::FlatBox); 54 | col_content_term.set_color(Color::BackGround); 55 | let term = frame::term::Term::default(); 56 | term.term.clone().set_id("term_log"); 57 | ); 58 | ); 59 | col.add(&tab_content); 60 | ); 61 | 62 | // Configure buttons 63 | hover_blink!(btn_term); 64 | hover_blink!(btn_resize); 65 | hover_blink!(btn_prev); 66 | hover_blink!(btn_next); 67 | 68 | // Title font size 69 | frame_title.clone().set_label_size((dimm::height_text() as f32 * 1.5) as i32); 70 | 71 | // Switch between tabs 72 | btn_term.clone().set_callback({ 73 | let col_content_term = col_content_term.clone(); 74 | let col_content_footer = col_content_footer.clone(); 75 | move |_| 76 | { 77 | if tab_content.value().unwrap().is_same(&col_content_footer.as_group().unwrap()) 78 | { 79 | let _ = tab_content.set_value(&col_content_term.as_group().unwrap()); 80 | } 81 | else 82 | { 83 | let _ = tab_content.set_value(&col_content_footer.as_group().unwrap()); 84 | } // else 85 | } 86 | }); 87 | 88 | } // }}} 89 | 90 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 91 | -------------------------------------------------------------------------------- /gui/wizard/src/frame/finish.rs: -------------------------------------------------------------------------------- 1 | // Gui 2 | use fltk::prelude::*; 3 | use fltk::{ 4 | app::Sender, 5 | output, 6 | text, 7 | }; 8 | 9 | use anyhow::anyhow as ah; 10 | 11 | use shared::fltk::WidgetExtExtra; 12 | 13 | use crate::db; 14 | use crate::dimm; 15 | use crate::log_status; 16 | use crate::common; 17 | use shared::std::OsStrExt; 18 | use shared::std::PathBufExt; 19 | 20 | // fn: finish_file_location() {{{ 21 | fn finish_file_location(output: &mut fltk::output::Output) -> anyhow::Result 22 | { 23 | let db_global = db::global::read()?; 24 | let _ = output.insert(&db_global.path_file_output.string()); 25 | Ok(format!(".{}.config", &db_global.path_file_output.file_name().ok_or(ah!("Could not get file stem"))?.string())) 26 | } // fn: finish_file_location() }}} 27 | 28 | // pub fn finish() {{{ 29 | pub fn finish(tx: Sender, title: &str) 30 | { 31 | // Enter the build directory 32 | if let Err(e) = common::dir_build() 33 | { 34 | log_status!("Err: {}", e.to_string()); 35 | } // if 36 | 37 | let mut ui = crate::GUI.lock().unwrap().ui.clone()(title); 38 | 39 | // Deactivate prev button 40 | ui.btn_prev.deactivate(); 41 | 42 | // Set next button to start over 43 | ui.btn_next.emit(tx, common::Msg::DrawWelcome); 44 | ui.btn_next.set_label("Finish"); 45 | 46 | // Show where the package was saved into 47 | let mut col = fltk::group::Flex::default() 48 | .column() 49 | .with_size_of(&ui.group) 50 | .with_pos_of(&ui.group); 51 | // Label 52 | col.fixed(&fltk::frame::Frame::default() 53 | .with_align(fltk::enums::Align::Inside | fltk::enums::Align::Left) 54 | .with_label("Your package was saved in this location"), dimm::height_text()); 55 | // Saved file path 56 | let mut output_saved_location = output::Output::default().with_focus(false); 57 | col.fixed(&output_saved_location, dimm::height_button_wide()); 58 | // Retrieve output image file location 59 | let str_package_basename = finish_file_location(&mut output_saved_location).unwrap_or_default(); 60 | let mut output_info = text::TextDisplay::default() 61 | .with_color(fltk::enums::Color::BackGround) 62 | .with_frame(fltk::enums::FrameType::NoBox); 63 | output_info.wrap_mode(text::WrapMode::AtColumn, 0); 64 | output_info.set_buffer(text::TextBuffer::default()); 65 | output_info.insert("You can now move the package to your games folder,"); 66 | output_info.insert(" other Linux computer or an external hard drive."); 67 | output_info.insert(" To start using your application, simply click to launch."); 68 | output_info.insert("\n\n"); 69 | output_info.insert("Regardless of where you store your package,"); 70 | output_info.insert(" launching it for the first time will generate"); 71 | output_info.insert(&format!(" a directory called '{}'", str_package_basename)); 72 | output_info.insert(", this directory contains application data such as save games."); 73 | output_info.insert("\n\n"); 74 | output_info.insert("If you encounter any issues or have suggestions for new features,"); 75 | output_info.insert(" I encourage you to create an issue on GitHub or GitLab."); 76 | output_info.insert(" Your feedback is invaluable to help project improve."); 77 | col.add(&output_info); 78 | col.end(); 79 | } // }}} 80 | 81 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 82 | -------------------------------------------------------------------------------- /gui/wizard/src/frame/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod term; 2 | pub mod common; 3 | pub mod welcome; 4 | pub mod platform; 5 | pub mod creator; 6 | pub mod desktop; 7 | pub mod finish; 8 | pub mod icon; 9 | -------------------------------------------------------------------------------- /gui/wizard/src/frame/welcome.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | // Gui 5 | use fltk::prelude::*; 6 | use fltk::{ 7 | app::Sender, 8 | input::FileInput, 9 | frame::Frame, 10 | dialog::dir_chooser, 11 | enums::Align, 12 | }; 13 | 14 | use shared::fltk::SenderExt; 15 | use anyhow::anyhow as ah; 16 | 17 | use crate::db; 18 | use crate::gameimage; 19 | use crate::dimm; 20 | use crate::common; 21 | use crate::log_status; 22 | use shared::svg; 23 | use shared::std::PathBufExt; 24 | use shared::{column,row,add,fixed}; 25 | 26 | // check_version() {{{ 27 | fn check_version() -> anyhow::Result<()> 28 | { 29 | let db_fetch = match db::fetch::read() 30 | { 31 | Ok(db) => db, 32 | Err(e) => return Err(ah!("error: could not read fetch.json, backend failed? No internet? '{}", e)), 33 | }; // match 34 | 35 | let version = db_fetch.version; 36 | if ! version.starts_with("1.6") 37 | { 38 | return Err(ah!("error: you should update to version {}", version)); 39 | } // if 40 | 41 | Ok(()) 42 | } // check_version() }}} 43 | 44 | // pub fn welcome() {{{ 45 | pub fn welcome(tx: Sender, title: &str) 46 | { 47 | let ui = crate::GUI.lock().unwrap().ui.clone()(title); 48 | 49 | column!(col, 50 | add!(col, spacer, Frame::default()); 51 | row!(row, 52 | add!(row, spacer, Frame::default()); 53 | fixed!(row, frame_image, Frame::default(), dimm::height_button_wide()*4); 54 | add!(row, spacer, Frame::default()); 55 | ); 56 | col.fixed(&row, dimm::height_button_wide()*4); 57 | add!(col, spacer, Frame::default()); 58 | fixed!(col, _label, Frame::default() 59 | .with_align(Align::Left | Align::Inside) 60 | .with_label("Select The Directory for GameImage's Temporary Files") 61 | , dimm::height_text()); 62 | fixed!(col, input_dir, FileInput::default(), dimm::height_button_wide() + dimm::border_half()); 63 | ); 64 | 65 | // Image 66 | let mut frame_image = frame_image.clone(); 67 | frame_image.set_align(Align::Inside | Align::Bottom); 68 | frame_image.set_image_scaled(fltk::image::SvgImage::from_data(svg::ICON_GAMEIMAGE).ok()); 69 | 70 | // Input 71 | let mut input_dir = input_dir.clone(); 72 | input_dir.set_pos(dimm::border(), input_dir.y()); 73 | input_dir.set_readonly(true); 74 | input_dir.set_value(&env::var("GIMG_DIR").unwrap_or_default()); 75 | input_dir.set_callback(move |e| 76 | { 77 | let mut path_selected = match dir_chooser("Select the build directory", "", false) 78 | { 79 | Some(value) => PathBuf::from(value), 80 | None => { log_status!("No file selected"); return; }, 81 | }; 82 | // Set build dir as chosen dir + /build 83 | path_selected = path_selected.join("build"); 84 | // Update chosen dir in selection bar 85 | e.set_value(&path_selected.string()); 86 | // Set env var to build dir 87 | env::set_var("GIMG_DIR", &path_selected.string()); 88 | }); 89 | 90 | // Set callback for next 91 | let clone_tx = tx.clone(); 92 | ui.btn_next.clone().set_callback(move |_| 93 | { 94 | let path_dir_build = match env::var("GIMG_DIR") 95 | { 96 | Ok(value) => PathBuf::from(value), 97 | Err(e) => { log_status!("Invalid temporary files directory: {}", e); return; } 98 | }; // if 99 | // Create build directory 100 | match std::fs::create_dir_all(&path_dir_build) 101 | { 102 | Ok(()) => (), 103 | Err(e) => log_status!("Could not create build directory: {}", e), 104 | } 105 | // Init project build directory 106 | match gameimage::init::build(path_dir_build) 107 | { 108 | Ok(()) => (), 109 | Err(e) => log_status!("Error to initialize build directory: {}", e) 110 | }; // match 111 | // Fetch fetch list 112 | match gameimage::fetch::sources() 113 | { 114 | Ok(code) => log_status!("Fetch exited with code {}", code), 115 | Err(e) => log_status!("Error to initialize build directory: {}", e) 116 | }; // match 117 | // Check if version matches 118 | if let Err(e) = check_version() 119 | { 120 | log_status!("{}", e); 121 | fltk::dialog::message_default(&format!("{}", e)); 122 | clone_tx.send_awake(common::Msg::WindActivate); 123 | return; 124 | } // if 125 | // Draw creator frame 126 | clone_tx.send_awake(common::Msg::DrawCreator); 127 | }); 128 | } // fn: welcome }}} 129 | 130 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 131 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/desktop.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | use serde_json::json; 4 | 5 | use shared::std::PathBufExt; 6 | 7 | use crate::gameimage::gameimage; 8 | 9 | // pub fn desktop() {{{ 10 | pub fn desktop(name: &str, items: &str) -> anyhow::Result<()> 11 | { 12 | let mut json_args = json!({}); 13 | json_args["op"] = "desktop".into(); 14 | json_args["desktop"]["op"] = "setup".into(); 15 | json_args["desktop"]["name"] = name.into(); 16 | json_args["desktop"]["items"] = items.into(); 17 | // Wait for message & check return value 18 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 19 | { 20 | 0 => Ok(()), 21 | ret => Err(ah!("Could not include {} into the image: {}", name, ret)), 22 | } // match 23 | } // fn: desktop }}} 24 | 25 | // pub fn icon() {{{ 26 | pub fn icon(path : &std::path::PathBuf) -> anyhow::Result<()> 27 | { 28 | let mut json_args = json!({}); 29 | json_args["op"] = "desktop".into(); 30 | json_args["desktop"]["op"] = "icon".into(); 31 | json_args["desktop"]["path_file_icon"] = path.string().into(); 32 | // Wait for message & check return value 33 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 34 | { 35 | 0 => Ok(()), 36 | ret => Err(ah!("Could not setup desktop icon {}: {}", path.string(), ret)), 37 | } // match 38 | } // fn: icon }}} 39 | 40 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 41 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/fetch.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{mpsc,Arc,Mutex}; 2 | 3 | use serde_json::json; 4 | 5 | use anyhow::anyhow as ah; 6 | 7 | use crate::log; 8 | use crate::common; 9 | use crate::gameimage; 10 | 11 | // fetch() {{{ 12 | pub fn fetch(platform: common::Platform, f: F) -> anyhow::Result 13 | where F: FnMut(mpsc::Receiver) + Send + 'static 14 | { 15 | let mut json_args = json!({}); 16 | json_args["op"] = "fetch".into(); 17 | json_args["fetch"]["op"] = "fetch".into(); 18 | json_args["fetch"]["platform"] = platform.as_str().into(); 19 | match gameimage::gameimage::gameimage_sync_ipc(vec![&json_args.to_string()], f) 20 | { 21 | 0 => { log!("Fetch on backend finished successfully"); Ok(0) }, 22 | rc => { return Err(ah!("Failed to execute fetch on backend with {}", rc)); }, 23 | } // match 24 | } // fetch() }}} 25 | 26 | // installed() {{{ 27 | pub fn installed() -> anyhow::Result> 28 | { 29 | let mut json_args = json!({}); 30 | json_args["op"] = "fetch".into(); 31 | json_args["fetch"]["op"] = "installed".into(); 32 | let arc_platforms : Arc>> = Arc::new(Mutex::new(vec![])); 33 | let clone_arc_platforms = arc_platforms.clone(); 34 | gameimage::gameimage::gameimage_sync_ipc(vec![&json_args.to_string()], move |rx| 35 | { 36 | while let Ok(msg) = rx.recv() 37 | { 38 | match clone_arc_platforms.lock() 39 | { 40 | Ok(mut guard) => match common::Platform::from_str(&msg) 41 | { 42 | Some(platform) => guard.push(platform), 43 | None => log!("Invalid platform: {}", msg), 44 | } 45 | Err(e) => log!("Could not lock installed vec: {}", e), 46 | }; 47 | } // while 48 | }); 49 | 50 | match arc_platforms.clone().lock() 51 | { 52 | Ok(platforms) => Ok(platforms.clone()), 53 | Err(e) => return Err(ah!("Could not lock platforms: {}", e)), 54 | } 55 | } // installed() }}} 56 | 57 | // sources() {{{ 58 | pub fn sources() -> anyhow::Result 59 | { 60 | let mut json_args = json!({}); 61 | json_args["op"] = "fetch".into(); 62 | json_args["fetch"]["op"] = "sources".into(); 63 | match gameimage::gameimage::gameimage_sync(vec![&json_args.to_string()]) 64 | { 65 | 0 => { log!("Fetch on backend finished successfully"); Ok(0)}, 66 | rc => { log!("Failed to execute fetch on backend with {}", rc); Ok(rc)}, 67 | } // match 68 | } // sources() }}} 69 | 70 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 71 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/gameimage.rs: -------------------------------------------------------------------------------- 1 | use std:: 2 | { 3 | env, 4 | sync::{Arc,Mutex,mpsc}, 5 | }; 6 | 7 | use crate::lib; 8 | use crate::common; 9 | use crate::log_err; 10 | use crate::log; 11 | 12 | 13 | 14 | // fn binary() {{{ 15 | pub fn binary() -> anyhow::Result 16 | { 17 | Ok(which::which("gameimage-cli")?) 18 | } // }}} 19 | 20 | // pub fn dir_build() {{{ 21 | pub fn dir_build() -> anyhow::Result<()> 22 | { 23 | Ok(env::set_current_dir(std::path::PathBuf::from(env::var("GIMG_DIR")?))?) 24 | } // fn: dir_build }}} 25 | 26 | // pub fn gameimage_async() {{{ 27 | pub fn gameimage_async(args : Vec<&str>) -> anyhow::Result<(mpsc::Receiver, mpsc::Receiver)> 28 | { 29 | dir_build()?; 30 | 31 | let path_binary_gameimage = binary()?; 32 | 33 | let mut handle = std::process::Command::new(&path_binary_gameimage) 34 | .stdout(std::process::Stdio::piped()) 35 | .stderr(std::process::Stdio::piped()) 36 | .args(&args) 37 | .spawn()?; 38 | 39 | log!("Dispatch command: {:?} : {:?}", path_binary_gameimage, args); 40 | 41 | // Create arc reader for stdout 42 | let arc_stdout = Arc::new(Mutex::new(handle.stdout.take())); 43 | let arc_stderr = Arc::new(Mutex::new(handle.stderr.take())); 44 | let arc_handle = Arc::new(Mutex::new(handle)); 45 | 46 | // Create t/r 47 | let stdout = arc_stdout.lock().unwrap().take(); 48 | let stderr = arc_stderr.lock().unwrap().take(); 49 | let (tx_code, rx_code) = mpsc::channel(); 50 | std::thread::spawn(move || 51 | { 52 | let (tx_log, rx_log) = mpsc::channel(); 53 | let f_callback = |tx : mpsc::Sender, msg| { log_err!(tx.send(msg)); }; 54 | let handle_stdout = std::thread::spawn(common::log_fd(stdout.unwrap(), tx_log.clone(), f_callback)); 55 | let handle_stderr = std::thread::spawn(common::log_fd(stderr.unwrap(), tx_log, f_callback)); 56 | while let Ok(msg) = rx_log.recv() 57 | { 58 | for line in msg.split('\n') 59 | { 60 | log!("{}", line); 61 | } 62 | } // while 63 | 64 | log_err!(handle_stdout.join()); 65 | log_err!(handle_stderr.join()); 66 | 67 | // Close IPC 68 | lib::ipc::Ipc::close(); 69 | 70 | // Send exit code 71 | if let Ok(mut guard) = arc_handle.lock() 72 | && let Ok(status) = guard.wait() 73 | && let Some(code) = status.code() 74 | { 75 | log_err!(tx_code.send(code)); 76 | } 77 | else 78 | { 79 | log_err!(tx_code.send(1)); 80 | } // else 81 | }); 82 | 83 | let (tx_ipc, rx_ipc) = mpsc::channel(); 84 | // Write from ipc to channel 85 | std::thread::spawn(move || 86 | { 87 | // Open ipc 88 | let ipc = match lib::ipc::Ipc::new() 89 | { 90 | Ok(ipc) => ipc, 91 | Err(e) => { log!("Could not create ipc instance: {}", e); return; }, 92 | }; // match 93 | // Write received message to transmitter 94 | while let Ok(msg) = ipc.recv() 95 | { 96 | if let Err(e) = tx_ipc.send(msg) 97 | { 98 | log!("Could not send ipc retrieved message: {}", e); 99 | } // if 100 | } // while 101 | }); 102 | 103 | Ok((rx_ipc, rx_code)) 104 | } // fn: gameimage_async }}} 105 | 106 | // pub fn gameimage_sync() {{{ 107 | pub fn gameimage_sync(args : Vec<&str>) -> i32 108 | { 109 | let (_, rx_code) = match gameimage_async(args) 110 | { 111 | Ok((rx_ipc, rx_code)) => (rx_ipc, rx_code), 112 | Err(e) => { log!("Could not start backend: {}", e); return 1; }, 113 | }; // if 114 | 115 | rx_code.recv().unwrap_or(1) 116 | } // fn: gameimage_sync }}} 117 | 118 | // pub fn gameimage_sync_ipc() {{{ 119 | pub fn gameimage_sync_ipc(args : Vec<&str>, mut f: F) -> i32 120 | where F: FnMut(mpsc::Receiver) + Send + 'static 121 | { 122 | let (rx_ipc, rx_code) = match gameimage_async(args) 123 | { 124 | Ok((rx_ipc, rx_code)) => (rx_ipc, rx_code), 125 | Err(e) => { log!("Could not start backend: {}", e); return 1; }, 126 | }; // if 127 | 128 | // Receive messages 129 | f(rx_ipc); 130 | 131 | // Recover exit code 132 | rx_code.recv().unwrap_or(1) 133 | } // fn: gameimage_sync_ipc }}} 134 | 135 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 136 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/init.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::anyhow as ah; 4 | 5 | use serde_json::json; 6 | 7 | use shared::std::PathBufExt; 8 | 9 | use crate::gameimage::gameimage; 10 | 11 | // pub fn build() {{{ 12 | pub fn build(path_dir_build : PathBuf) -> anyhow::Result<()> 13 | { 14 | let mut json_args = json!({}); 15 | json_args["op"] = "init".into(); 16 | json_args["init"]["op"] = "build".into(); 17 | json_args["init"]["path_dir_build"] = path_dir_build.string().into(); 18 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 19 | { 20 | 0 => Ok(()), 21 | ret => Err(ah!("Could not init gameimage build root: {}", ret)), 22 | } // match 23 | } // fn: build }}} 24 | 25 | // pub fn project() {{{ 26 | pub fn project(name : String, platform : String) -> anyhow::Result<()> 27 | { 28 | let mut json_args = json!({}); 29 | json_args["op"] = "init".into(); 30 | json_args["init"]["op"] = "project".into(); 31 | json_args["init"]["name"] = name.into(); 32 | json_args["init"]["platform"] = platform.into(); 33 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 34 | { 35 | 0 => Ok(()), 36 | ret => Err(ah!("Could not init gameimage project: {}", ret)), 37 | } // match 38 | } // fn: project }}} 39 | 40 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 41 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/install.rs: -------------------------------------------------------------------------------- 1 | use shared::std::PathBufExt; 2 | 3 | use serde_json::json; 4 | 5 | use anyhow::anyhow as ah; 6 | 7 | use crate::gameimage::gameimage; 8 | 9 | // pub fn icon() {{{ 10 | pub fn icon(path : &std::path::PathBuf) -> anyhow::Result<()> 11 | { 12 | let mut json_args = json!({}); 13 | json_args["op"] = "install".into(); 14 | json_args["install"]["op"] = "install".into(); 15 | json_args["install"]["sub_op"] = "icon".into(); 16 | json_args["install"]["args"] = vec![path.string()].into(); 17 | // Wait for message & check return value 18 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 19 | { 20 | 0 => Ok(()), 21 | ret => Err(ah!("Could not install icon '{}' into the image: {}", path.string(), ret)), 22 | } // match 23 | } // fn: icon }}} 24 | 25 | // pub fn install() {{{ 26 | pub fn install(str_type : &str, vec_path_files : Vec) -> anyhow::Result<()> 27 | { 28 | let mut json_args = json!({}); 29 | json_args["op"] = "install".into(); 30 | json_args["install"]["op"] = "install".into(); 31 | json_args["install"]["sub_op"] = str_type.into(); 32 | json_args["install"]["args"] = vec_path_files.into(); 33 | // Wait for message & check return value 34 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 35 | { 36 | 0 => Ok(()), 37 | ret => Err(ah!("Could not install files: {}", ret)), 38 | } // match 39 | } // fn: install }}} 40 | 41 | // pub fn remote() {{{ 42 | pub fn remote(str_type : &str, vec_path_files : Vec) -> anyhow::Result<()> 43 | { 44 | let mut json_args = json!({}); 45 | json_args["op"] = "install".into(); 46 | json_args["install"]["op"] = "remote".into(); 47 | json_args["install"]["sub_op"] = str_type.into(); 48 | json_args["install"]["args"] = vec_path_files.into(); 49 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 50 | { 51 | 0 => Ok(()), 52 | ret => Err(ah!("Could not install remote files: {}", ret)), 53 | } // match 54 | } // fn: remote }}} 55 | 56 | // pub fn remove() {{{ 57 | pub fn remove(str_type : &str, vec_path_files : Vec) -> anyhow::Result<()> 58 | { 59 | let mut json_args = json!({}); 60 | json_args["op"] = "install".into(); 61 | json_args["install"]["op"] = "remove".into(); 62 | json_args["install"]["sub_op"] = str_type.into(); 63 | json_args["install"]["args"] = vec_path_files.into(); 64 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 65 | { 66 | 0 => Ok(()), 67 | ret => Err(ah!("Could not remove files: {}", ret)), 68 | } // match 69 | } // fn: remove }}} 70 | 71 | // pub fn gui() {{{ 72 | pub fn gui() -> anyhow::Result<()> 73 | { 74 | let mut json_args = json!({}); 75 | json_args["op"] = "install".into(); 76 | json_args["install"]["op"] = "install".into(); 77 | json_args["install"]["sub_op"] = "gui".into(); 78 | json_args["install"]["args"] = Vec::::new().into(); 79 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 80 | { 81 | 0 => Ok(()), 82 | ret => Err(ah!("Could not install files: {}", ret)), 83 | } // match 84 | } // fn: install }}} 85 | 86 | // pub fn winetricks() {{{ 87 | pub fn winetricks(vec_path_files : Vec) -> anyhow::Result<()> 88 | { 89 | let mut json_args = json!({}); 90 | json_args["op"] = "install".into(); 91 | json_args["install"]["op"] = "install".into(); 92 | json_args["install"]["sub_op"] = "winetricks".into(); 93 | json_args["install"]["args"] = vec_path_files.into(); 94 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 95 | { 96 | 0 => Ok(()), 97 | ret => Err(ah!("Could not install files: {}", ret)), 98 | } // match 99 | } // fn: winetricks }}} 100 | 101 | // pub fn wine() {{{ 102 | pub fn wine(vec_path_files : Vec) -> anyhow::Result<()> 103 | { 104 | let mut json_args = json!({}); 105 | json_args["op"] = "install".into(); 106 | json_args["install"]["op"] = "install".into(); 107 | json_args["install"]["sub_op"] = "wine".into(); 108 | json_args["install"]["args"] = vec_path_files.into(); 109 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 110 | { 111 | 0 => Ok(()), 112 | ret => Err(ah!("Could not install files: {}", ret)), 113 | } // match 114 | } // fn: wine }}} 115 | 116 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 117 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gameimage; 2 | pub mod fetch; 3 | pub mod search; 4 | pub mod package; 5 | pub mod desktop; 6 | pub mod install; 7 | pub mod select; 8 | pub mod init; 9 | pub mod test; 10 | pub mod project; 11 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/package.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | use serde_json::json; 4 | 5 | use crate::gameimage::gameimage; 6 | 7 | // pub fn package() {{{ 8 | pub fn package(name: &str, projects : Vec) -> anyhow::Result<()> 9 | { 10 | let mut json_args = json!({}); 11 | json_args["op"] = "package".into(); 12 | json_args["package"]["name"] = name.into(); 13 | json_args["package"]["projects"] = projects.clone().into(); 14 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 15 | { 16 | 0 => Ok(()), 17 | ret => Err(ah!("Could not include projects '{}' into the image: {}", projects.join(":"), ret)), 18 | } // match 19 | } // fn: package }}} 20 | 21 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 22 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/project.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | use serde_json::json; 4 | 5 | use crate::gameimage::gameimage; 6 | 7 | // pub fn set() {{{ 8 | #[allow(dead_code)] pub fn set(str_name: &str) -> anyhow::Result<()> 9 | { 10 | let mut json_args = json!({}); 11 | json_args["op"] = "project".into(); 12 | json_args["project"]["op"] = "set".into(); 13 | json_args["project"]["name"] = str_name.into(); 14 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 15 | { 16 | 0 => Ok(()), 17 | ret => Err(ah!("Project command failed with return code: {}", ret)), 18 | } // match 19 | } // fn: select }}} 20 | 21 | // pub fn del() {{{ 22 | pub fn del(str_name: &str) -> anyhow::Result<()> 23 | { 24 | let mut json_args = json!({}); 25 | json_args["op"] = "project".into(); 26 | json_args["project"]["op"] = "del".into(); 27 | json_args["project"]["name"] = str_name.into(); 28 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 29 | { 30 | 0 => Ok(()), 31 | ret => Err(ah!("Project command failed with return code: {}", ret)), 32 | } // match 33 | } // fn: select }}} 34 | 35 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 36 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/search.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | use serde_json::json; 4 | 5 | use crate::log; 6 | use crate::common; 7 | use crate::gameimage; 8 | 9 | // search() {{{ 10 | fn search(str_type : &str, use_remote : bool) -> anyhow::Result> 11 | { 12 | let mut json_args = json!({}); 13 | json_args["op"] = "search".into(); 14 | json_args["search"]["op"] = if use_remote { "remote".into() } else { "local".into() }; 15 | json_args["search"]["query"] = str_type.into(); 16 | // Start backend 17 | let (rx_msg, rx_code) = match gameimage::gameimage::gameimage_async(vec![&json_args.to_string()]) 18 | { 19 | Ok((rx_msg, rx_code)) => (rx_msg, rx_code), 20 | Err(e) => return Err(ah!("Could not start gameimage backend: {}", e)), 21 | }; 22 | log!("Started backend"); 23 | // Retrieve messages 24 | let mut vec : Vec = vec![]; 25 | while let Ok(msg) = rx_msg.recv() 26 | { 27 | vec.push(msg.into()); 28 | } // while 29 | log!("Finished reading messages"); 30 | match rx_code.recv() 31 | { 32 | Ok(code) => match code 33 | { 34 | 0 => log!("Backend exited successfully"), 35 | val => log!("Backend exited with code '{}'", val), 36 | }, 37 | Err(e) => return Err(ah!("Failed to retrieve code from backend: {}", e)), 38 | } // match 39 | Ok(vec) 40 | } // search() }}} 41 | 42 | // search_local() {{{ 43 | pub fn search_local(str_type : &str) -> anyhow::Result> 44 | { 45 | Ok(search(str_type, false)?) 46 | } // search_local() }}} 47 | 48 | // search_remote() {{{ 49 | pub fn search_remote(str_type : &str) -> anyhow::Result> 50 | { 51 | Ok(search(str_type, true)?) 52 | } // search_remote() }}} 53 | 54 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 55 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/select.rs: -------------------------------------------------------------------------------- 1 | use shared::std::PathBufExt; 2 | 3 | use serde_json::json; 4 | 5 | use anyhow::anyhow as ah; 6 | 7 | use crate::gameimage::gameimage; 8 | 9 | // pub fn select() {{{ 10 | pub fn select(str_label : &str, path : &std::path::PathBuf) -> anyhow::Result<()> 11 | { 12 | let mut json_args = json!({}); 13 | json_args["op"] = "select".into(); 14 | json_args["select"]["op"] = str_label.into(); 15 | json_args["select"]["path_file_target"] = path.string().into(); 16 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 17 | { 18 | 0 => Ok(()), 19 | ret => Err(ah!("Could not select '{}' '{}' into the image: {}", &str_label, path.string(), ret)), 20 | } // match 21 | } // fn: select }}} 22 | 23 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 24 | -------------------------------------------------------------------------------- /gui/wizard/src/gameimage/test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow as ah; 2 | 3 | use serde_json::json; 4 | 5 | use crate::log; 6 | use crate::common; 7 | use crate::gameimage::gameimage; 8 | 9 | // pub fn test() {{{ 10 | pub fn test() -> anyhow::Result<()> 11 | { 12 | let mut json_args = json!({}); 13 | json_args["op"] = "test".into(); 14 | match gameimage::gameimage_sync(vec![&json_args.to_string()]) 15 | { 16 | 0 => { log!("test returned successfully"); return Ok(()) }, 17 | ret => return Err(ah!("test returned with error: {}", ret)), 18 | } // match 19 | } // fn: test }}} 20 | -------------------------------------------------------------------------------- /gui/wizard/src/lib/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use anyhow::anyhow as ah; 4 | 5 | use shared::std::PathBufExt; 6 | 7 | use crate::log; 8 | use crate::common; 9 | use crate::gameimage; 10 | 11 | // struct MsgBuf {{{ 12 | #[repr(C)] 13 | struct MsgBuf 14 | { 15 | mtype: libc::c_long, 16 | mtext: [u8; 1024], 17 | } // struct MsgBuf }}} 18 | 19 | // pub struct Ipc {{{ 20 | pub struct Ipc 21 | { 22 | msgid : i32, 23 | } // struct Ipc }}} 24 | 25 | impl Ipc 26 | { 27 | 28 | // pub fn new() {{{ 29 | pub fn new() -> anyhow::Result 30 | { 31 | let msgid = match Ipc::get_msgid(libc::IPC_CREAT) 32 | { 33 | Ok(msgid) => msgid, 34 | Err(e) => return Err(ah!("Failed to create message queue: {}", e)), 35 | }; 36 | 37 | Ok(Ipc { msgid }) 38 | } // }}} 39 | 40 | // pub fn recv() {{{ 41 | pub fn recv(&self) -> anyhow::Result 42 | { 43 | let mut buf: MsgBuf = unsafe { std::mem::zeroed() }; 44 | 45 | let ret = unsafe 46 | { 47 | libc::msgrcv(self.msgid 48 | , &mut buf as *mut MsgBuf as *mut libc::c_void 49 | , buf.mtext.len() as libc::size_t 50 | , 0 51 | , libc::MSG_NOERROR) 52 | }; 53 | 54 | if ret == -1 55 | { 56 | return Err(ah!("Could not recover message: {}", std::io::Error::last_os_error())); 57 | } // if 58 | 59 | let ret = ret as usize; 60 | let bytes = &buf.mtext[..ret]; 61 | 62 | let str_slice = match std::str::from_utf8(bytes) { 63 | Ok(s) => s, 64 | Err(e) => return Err(ah!("Received message is not valid UTF-8: {}", e)), 65 | }; 66 | 67 | Ok(str_slice.to_owned()) 68 | } // }}} 69 | 70 | // fn get_msgid() {{{ 71 | fn get_msgid(flags: i32) -> anyhow::Result 72 | { 73 | let path_file_backend = match gameimage::gameimage::binary() 74 | { 75 | Ok(path_file_backend) => path_file_backend, 76 | Err(e) => return Err(ah!("Could not get path to backend binary for ipc: {}", e)), 77 | }; 78 | 79 | // Wait for backend to create fifo 80 | let cstr_path = match CString::new(path_file_backend.string().clone()) 81 | { 82 | Ok(cstr) => cstr, 83 | Err(e) => return Err(ah!("Could not create C string: {}", e)), 84 | }; // match 85 | 86 | let key = match unsafe { libc::ftok(cstr_path.as_ptr(), 65) } 87 | { 88 | -1 => return Err(ah!("Failed to get key to check message queue: {}", errno::errno())), 89 | key => key, 90 | }; 91 | log!("Frontend key is: {}", key); 92 | 93 | match unsafe { libc::msgget(key, 0o666 | flags) } 94 | { 95 | -1 => return Err(ah!("Message queue does not yet exist, no need to close: {}", errno::errno())), 96 | msgid => Ok(msgid), 97 | } 98 | } // fn get_msgid() }}} 99 | 100 | // pub fn close() {{{ 101 | pub fn close() 102 | { 103 | let msgid = match Ipc::get_msgid(0) 104 | { 105 | Ok(msgid) => msgid, 106 | Err(e) => { log!("Could not retrieve msgid: {}", e); return; } 107 | }; 108 | 109 | match unsafe { libc::msgctl(msgid, libc::IPC_RMID, std::ptr::null_mut()) } 110 | { 111 | -1 => log!("Could not close existing message queue"), 112 | _ => log!("Closed existing message queue"), 113 | } // match 114 | } // close }}} 115 | 116 | } // impl Ipc 117 | 118 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 119 | -------------------------------------------------------------------------------- /gui/wizard/src/lib/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ipc; 2 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/compress.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | // Gui 4 | use fltk:: 5 | { 6 | prelude::*, 7 | app::Sender, 8 | menu, 9 | }; 10 | 11 | use shared::fltk::SenderExt; 12 | use shared::std::PathBufExt; 13 | 14 | use crate::dimm; 15 | use crate::gameimage; 16 | use crate::frame; 17 | use crate::common; 18 | use crate::log; 19 | use crate::log_alert; 20 | use crate::log_err; 21 | use shared::{column,row,fixed}; 22 | 23 | // fn compress_next() {{{ 24 | pub fn compress_next(tx: Sender, term: frame::term::Term) 25 | { 26 | tx.send_awake(common::Msg::WindDeactivate); 27 | let backend = match gameimage::gameimage::binary() 28 | { 29 | Ok(backend) => backend, 30 | Err(e) => { log_alert!("Error to execute backend: {}", e); return; } 31 | }; 32 | let mut term = term.clone(); 33 | std::thread::spawn(move || 34 | { 35 | let handle = term.dispatch(vec![&backend.string(), r#"{ "op": "compress" }"#], |_| {}); 36 | match handle 37 | { 38 | Ok(handle) => log_err!(handle.lock().unwrap().wait().map(|_|{})), 39 | Err(e) => log!("{}", e), 40 | }; 41 | tx.send_activate(common::Msg::DrawCreator); 42 | }); 43 | 44 | } // fn compress_next() }}} 45 | 46 | // pub fn compress() {{{ 47 | pub fn compress(tx: Sender 48 | , title: &str 49 | , msg_prev: common::Msg 50 | , _msg_curr: common::Msg 51 | , _msg_next: common::Msg) 52 | { 53 | let ui = crate::GUI.lock().unwrap().ui.clone()(title); 54 | // Layout 55 | column!(col, 56 | let term = frame::term::Term::default(); 57 | col.add(&term.group); 58 | row!(row, 59 | fixed!(row, btn_level, menu::MenuButton::default(), dimm::width_button_wide()); 60 | row.add(&fltk::frame::Frame::default() 61 | .with_align(fltk::enums::Align::Inside | fltk::enums::Align::Center) 62 | .with_label("Select the compression level before clicking on start") 63 | ); 64 | ); 65 | col.fixed(&row, dimm::height_button_wide()); 66 | ); 67 | 68 | // Configure buttons 69 | ui.btn_prev.clone().emit(tx.clone(), msg_prev); 70 | let mut btn_next = ui.btn_next.clone(); 71 | btn_next.set_label("Start"); 72 | btn_next.set_callback(move |_| { compress_next(tx, term.clone()); }); 73 | 74 | // Open space for compress level button 75 | let mut btn_level = btn_level.clone(); 76 | btn_level.set_callback(|e| 77 | { 78 | let str_level = e.choice().unwrap_or(String::from("7")); 79 | log!("Set compression level to {}", str_level); 80 | env::set_var("FIM_COMPRESSION_LEVEL", &str_level); 81 | e.set_value(e.value()); 82 | e.set_label(&str_level); 83 | }); 84 | for i in 0..11 85 | { 86 | btn_level.add_choice(&i.to_string()); 87 | } // for 88 | env::set_var("FIM_COMPRESSION_LEVEL", "7"); 89 | btn_level.set_value(8); 90 | btn_level.set_label("7"); 91 | } // fn compress() }}} 92 | 93 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 94 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/install.rs: -------------------------------------------------------------------------------- 1 | // Gui 2 | use fltk::prelude::*; 3 | use fltk::{ 4 | app::Sender, 5 | browser::MultiBrowser, 6 | dialog, 7 | enums::{FrameType,Color}, 8 | }; 9 | 10 | use shared::fltk::WidgetExtExtra; 11 | use shared::fltk::SenderExt; 12 | use shared::{hover_blink,column,row,add,fixed}; 13 | 14 | use crate::dimm; 15 | use crate::common; 16 | use shared::std::PathBufExt; 17 | use crate::log_status; 18 | use crate::gameimage; 19 | 20 | // pub fn install() {{{ 21 | pub fn install(tx: Sender 22 | , title: &str 23 | , label: &str 24 | , msg_prev: common::Msg 25 | , msg_curr: common::Msg 26 | , msg_next: common::Msg) -> crate::Ui 27 | { 28 | let ui = crate::GUI.lock().unwrap().ui.clone()(title); 29 | 30 | // Set previous frame 31 | ui.btn_prev.clone().emit(tx.clone(), msg_prev); 32 | ui.btn_next.clone().emit(tx.clone(), msg_next); 33 | // Layout 34 | column!(col, 35 | row!(row, 36 | add!(row, list, MultiBrowser::default()); 37 | column!(col_buttons, 38 | fixed!(col_buttons, btn_add, shared::fltk::button::rect::add(), dimm::height_button_rec()); 39 | fixed!(col_buttons, btn_del, shared::fltk::button::rect::del(), dimm::height_button_rec()); 40 | col_buttons.add(&fltk::frame::Frame::default()); 41 | ); 42 | row.fixed(&col_buttons, dimm::width_button_rec()); 43 | ); 44 | ); 45 | // Buttons 46 | hover_blink!(btn_add); 47 | hover_blink!(btn_del); 48 | // List of the currently installed items 49 | let mut list = list.clone(); 50 | list.set_frame(FrameType::BorderBox); 51 | list.set_text_size(dimm::height_text()); 52 | // Insert items in list of currently installed items 53 | match gameimage::search::search_local(label) 54 | { 55 | Ok(vec_items) => for item in vec_items { list.add(&item.string()); }, 56 | Err(e) => log_status!("Could not get items to insert: {}", e), 57 | }; // match 58 | // Add new item 59 | let clone_tx = tx.clone(); 60 | let clone_label : String = label.to_string(); 61 | let _ = btn_add.clone() 62 | .with_color(Color::Green) 63 | .with_callback(move |_| 64 | { 65 | // Pick files to install 66 | let mut chooser = dialog::FileChooser::new("." 67 | , "*" 68 | , dialog::FileChooserType::Multi 69 | , "Pick one or multiple files"); 70 | // Start dialog 71 | chooser.show(); 72 | // Wait for choice(s) 73 | while chooser.shown() { fltk::app::wait(); } // while 74 | // Check if choice is valid 75 | if chooser.value(1).is_none() 76 | { 77 | log_status!("No file selected"); 78 | return; 79 | } // if 80 | // Install files 81 | clone_tx.send_awake(common::Msg::WindDeactivate); 82 | let count = chooser.count()+1; 83 | let clone_label = clone_label.clone(); 84 | let mut vec_entries : Vec = vec![]; 85 | (1..count).into_iter().for_each(|idx| { vec_entries.push(chooser.value(idx).unwrap()); }); 86 | std::thread::spawn(move || 87 | { 88 | match gameimage::install::install(&clone_label, vec_entries.clone()) 89 | { 90 | Ok(_) => log_status!("Installed selected files"), 91 | Err(e) => log_status!("Failed to install files: {}", e), 92 | }; // match 93 | clone_tx.send_activate(msg_curr); 94 | }); 95 | }); 96 | // Erase package 97 | let mut btn_del = btn_del.clone() 98 | .with_color(Color::Red); 99 | let mut clone_output_status = ui.status.clone(); 100 | let clone_label = label.to_string(); 101 | let clone_frame_list = list.clone(); 102 | let clone_tx = tx.clone(); 103 | btn_del.set_callback(move |_| 104 | { 105 | clone_tx.send_awake(common::Msg::WindDeactivate); 106 | let vec_indices = clone_frame_list.selected_items(); 107 | if vec_indices.len() == 0 108 | { 109 | clone_output_status.set_value("No item selected for deletion"); 110 | clone_tx.send_awake(common::Msg::WindActivate); 111 | return; 112 | } // if 113 | // Remove 114 | let clone_tx = clone_tx.clone(); 115 | let clone_label = clone_label.clone(); 116 | let clone_frame_list = clone_frame_list.clone(); 117 | std::thread::spawn(move || 118 | { 119 | // Get items 120 | let vec_items : Vec = vec_indices.into_iter().map(|e|{ clone_frame_list.text(e).unwrap() }).collect(); 121 | // Run backend 122 | match gameimage::install::remove(&clone_label, vec_items.clone()) 123 | { 124 | Ok(_) => log_status!("Successfully removed files"), 125 | Err(e) => log_status!("Failed to remove files: {}", e), 126 | }; // match 127 | // Redraw GUI 128 | clone_tx.send_activate(msg_curr); 129 | }); // std::thread 130 | }); // set_callback 131 | ui 132 | } 133 | // }}} 134 | 135 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 136 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/mod.rs: -------------------------------------------------------------------------------- 1 | mod name; 2 | mod install; 3 | mod compress; 4 | mod test; 5 | pub mod linux; 6 | pub mod wine; 7 | pub mod retroarch; 8 | pub mod pcsx2; 9 | pub mod rpcs3; 10 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/name.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | // Gui 4 | use fltk::prelude::*; 5 | use fltk::{ 6 | app::Sender, 7 | input::Input, 8 | frame::Frame, 9 | enums::{Align,FrameType}, 10 | }; 11 | 12 | use anyhow::anyhow as ah; 13 | 14 | use shared::fltk::SenderExt; 15 | use shared::svg; 16 | use shared::{column,row,fixed}; 17 | 18 | use crate::dimm; 19 | use crate::frame; 20 | use crate::common; 21 | use crate::log_status; 22 | use crate::gameimage; 23 | 24 | // fn name_next() {{{ 25 | fn name_next() -> anyhow::Result<()> 26 | { 27 | // Check for name 28 | let name = match env::var("GIMG_NAME") 29 | { 30 | Ok(name) => name, 31 | Err(e) => return Err(ah!("Could not fetch GIMG_NAME: {}", e)), 32 | }; 33 | // Check for platform 34 | let platform = match frame::platform::PLATFORM.lock() 35 | { 36 | Ok(guard) => match guard.clone() 37 | { 38 | Some(platform) => platform, 39 | None => return Err(ah!("No platform selected")), 40 | }, 41 | Err(e) => return Err(ah!("Could not lock platform: {}", e)), 42 | }; 43 | // Init project 44 | match gameimage::init::project(name, platform.as_str().to_string()) 45 | { 46 | Ok(_) => (), 47 | Err(e) => return Err(ah!("Could not init project: {}", e)), 48 | } // match 49 | 50 | Ok(()) 51 | } // fn name_next() }}} 52 | 53 | // pub fn name() {{{ 54 | pub fn name(tx: Sender 55 | , title: &str 56 | , msg_prev: common::Msg 57 | , msg_next: common::Msg) 58 | { 59 | let ui = crate::GUI.lock().unwrap().ui.clone()(title); 60 | // Layout 61 | column!(col, 62 | col.add(&Frame::default()); 63 | row!(row_icon, 64 | row_icon.add(&Frame::default()); 65 | fixed!(row_icon, frame_icon, Frame::default(), 225); 66 | row_icon.add(&Frame::default()); 67 | ); 68 | col.fixed(&row_icon, 150); 69 | col.add(&Frame::default()); 70 | fixed!(col, input_name, Input::default(), dimm::height_button_wide()); 71 | ); 72 | // Configure icon box 73 | let mut frame_icon = frame_icon.clone(); 74 | frame_icon.set_frame(FrameType::NoBox); 75 | frame_icon.set_image(Some(fltk::image::SvgImage::from_data(svg::icon_joystick(10.0).as_str()).unwrap())); 76 | // Game name 77 | let mut input_name = input_name.clone() 78 | .with_align(Align::Top | Align::Left); 79 | input_name.set_pos(ui.group.x(), input_name.y() - input_name.h()); 80 | let _ = input_name.take_focus(); 81 | // Sanitize game name 82 | let f_sanitize = |input : String| -> String 83 | { 84 | input 85 | .chars() 86 | .filter_map(|c| 87 | { 88 | if c.is_alphanumeric() { Some(c) } 89 | else if c == '-' { Some(c) } 90 | else if c == '_' { Some(c) } 91 | else if c == ':' { Some('-') } 92 | else if c == ' ' { Some('-') } 93 | else { None } 94 | }) 95 | .collect() 96 | }; 97 | // Check if GIMG_NAME exists 98 | let env_name = f_sanitize(env::var("GIMG_NAME").unwrap_or_default()); 99 | env::set_var("GIMG_NAME", &env_name); 100 | input_name.set_value(&env_name); 101 | // Set input_name callback 102 | input_name.handle(move |input,ev| 103 | { 104 | if ev == fltk::enums::Event::KeyUp 105 | { 106 | env::set_var("GIMG_NAME", f_sanitize(input.value())); 107 | return true; 108 | } // if 109 | return false; 110 | }); 111 | // Callback to previous 112 | ui.btn_prev.clone().emit(tx, msg_prev); 113 | // Callback to Next 114 | let clone_tx = tx.clone(); 115 | let clone_msg_next = msg_next.clone(); 116 | ui.btn_next.clone().set_callback(move |_| 117 | { 118 | clone_tx.send_awake(common::Msg::WindDeactivate); 119 | std::thread::spawn(move || 120 | { 121 | match name_next() 122 | { 123 | Ok(()) => tx.send_activate(clone_msg_next), 124 | Err(e) => { clone_tx.send_awake(common::Msg::WindActivate); log_status!("{}", e); } 125 | } 126 | }); 127 | }); 128 | } // }}} 129 | 130 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 131 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/pcsx2.rs: -------------------------------------------------------------------------------- 1 | use fltk::app::Sender; 2 | 3 | use crate::common; 4 | use crate::frame; 5 | use crate::wizard; 6 | 7 | // pub fn name() {{{ 8 | pub fn name(tx: Sender, title: &str) 9 | { 10 | wizard::name::name(tx.clone() 11 | , title 12 | , common::Msg::DrawPlatform 13 | , common::Msg::DrawPcsx2Icon); 14 | } // }}} 15 | 16 | // pub fn icon() {{{ 17 | pub fn icon(tx: Sender, title: &str) 18 | { 19 | frame::icon::project(tx.clone() 20 | , title 21 | , common::Msg::DrawPcsx2Name 22 | , common::Msg::DrawPcsx2Icon 23 | , common::Msg::DrawPcsx2Rom 24 | ); 25 | } // }}} 26 | 27 | // pub fn rom() {{{ 28 | pub fn rom(tx: Sender, title: &str) 29 | { 30 | wizard::install::install(tx.clone() 31 | , title 32 | , "rom" 33 | , common::Msg::DrawPcsx2Icon 34 | , common::Msg::DrawPcsx2Rom 35 | , common::Msg::DrawPcsx2Bios); 36 | } // }}} 37 | 38 | // pub fn bios() {{{ 39 | pub fn bios(tx: Sender, title: &str) 40 | { 41 | wizard::install::install(tx.clone() 42 | , title 43 | , "bios" 44 | , common::Msg::DrawPcsx2Rom 45 | , common::Msg::DrawPcsx2Bios 46 | , common::Msg::DrawPcsx2Test); 47 | } // }}} 48 | 49 | // pub fn test() {{{ 50 | pub fn test(tx: Sender, title: &str) 51 | { 52 | wizard::test::test(tx.clone() 53 | , title 54 | , common::Msg::DrawPcsx2Bios 55 | , common::Msg::DrawPcsx2Test 56 | , common::Msg::DrawPcsx2Compress); 57 | } // }}} 58 | 59 | // pub fn compress() {{{ 60 | pub fn compress(tx: Sender, title: &str) 61 | { 62 | wizard::compress::compress(tx.clone() 63 | , title 64 | , common::Msg::DrawPcsx2Test 65 | , common::Msg::DrawPcsx2Compress 66 | , common::Msg::DrawCreator); 67 | } // }}} 68 | 69 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 70 | -------------------------------------------------------------------------------- /gui/wizard/src/wizard/test.rs: -------------------------------------------------------------------------------- 1 | // Gui 2 | use fltk::prelude::*; 3 | use fltk::{ 4 | app::Sender, 5 | enums::Color, 6 | }; 7 | 8 | use serde_json::json; 9 | 10 | use shared::fltk::SenderExt; 11 | use shared::fltk::WidgetExtExtra; 12 | use shared::std::PathBufExt; 13 | use shared::{column,row,fixed,hover_blink}; 14 | 15 | use crate::gameimage; 16 | use crate::dimm; 17 | use crate::frame; 18 | use crate::common; 19 | use crate::log_alert; 20 | 21 | // pub fn test() {{{ 22 | pub fn test(tx: Sender 23 | , title: &str 24 | , msg_prev: common::Msg 25 | , _msg_curr: common::Msg 26 | , msg_next: common::Msg) 27 | { 28 | let ui = crate::GUI.lock().unwrap().ui.clone()(title); 29 | // Layout 30 | column!(col, 31 | let term = frame::term::Term::default(); 32 | row!(row, 33 | row.add(&fltk::frame::Frame::default()); 34 | fixed!(row, btn_test, shared::fltk::button::wide::default(), dimm::width_button_wide()); 35 | row.add(&fltk::frame::Frame::default()); 36 | ); 37 | col.fixed(&row, dimm::height_button_wide()); 38 | ); 39 | // Cofigure buttons 40 | hover_blink!(btn_test); 41 | ui.btn_prev.clone().emit(tx.clone(), msg_prev); 42 | ui.btn_next.clone().emit(tx.clone(), msg_next); 43 | 44 | // Add a 'test' button 45 | let clone_tx = tx.clone(); 46 | let mut term = term.clone(); 47 | let mut btn_test = btn_test.clone() 48 | .with_label("Test") 49 | .with_color(Color::Green); 50 | btn_test.set_callback(move |_| 51 | { 52 | clone_tx.send_awake(common::Msg::WindDeactivate); 53 | let backend = match gameimage::gameimage::binary() 54 | { 55 | Ok(backend) => backend, 56 | Err(e) => { log_alert!("Error to execute backend: {}", e); return; } 57 | }; 58 | let mut json_args = json!({}); 59 | json_args["op"] = "test".into(); 60 | let _ = term.dispatch(vec![&backend.string(), &json_args.to_string()], move |_| 61 | { 62 | clone_tx.send_awake(common::Msg::WindActivate); 63 | }); 64 | }); 65 | } // fn test() }}} 66 | 67 | // vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et : 68 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | # @file : CMakeLists 4 | ###################################################################### 5 | 6 | cmake_minimum_required(VERSION 3.5) 7 | 8 | project(GameImage DESCRIPTION "GameImage - FlatImage Game Packer" LANGUAGES CXX) 9 | 10 | # Compiler 11 | message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}") 12 | add_definitions("--std=c++23 -Wall -Wextra") 13 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") 14 | message(STATUS "CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}") 15 | message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") 16 | message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") 17 | 18 | # Tools 19 | ## Git 20 | find_program(GIT_COMMAND git) 21 | if(NOT GIT_COMMAND) 22 | message(FATAL_ERROR "git command not found") 23 | endif() 24 | 25 | # External libraries 26 | find_package(fmt REQUIRED) 27 | find_package(cpr REQUIRED) 28 | find_package(argparse REQUIRED) 29 | find_package(nlohmann_json REQUIRED) 30 | find_package(matchit REQUIRED) 31 | find_package(magic_enum REQUIRED) 32 | find_package(Boost REQUIRED) 33 | find_package(easyloggingpp REQUIRED) 34 | find_package(libjpeg-turbo REQUIRED) 35 | find_package(PNG REQUIRED) 36 | find_package(ZLIB REQUIRED) 37 | find_package(cppcoro REQUIRED) 38 | find_package(libzippp REQUIRED) 39 | find_package(cryptopp REQUIRED) 40 | find_package(LibArchive REQUIRED) 41 | find_package(Boost COMPONENTS filesystem REQUIRED) 42 | INCLUDE_DIRECTORIES(${Boost_INCLUDE_DIR}) 43 | 44 | # Main executable 45 | add_executable(main main.cpp) 46 | target_link_libraries(main PRIVATE 47 | fmt::fmt 48 | argparse::argparse 49 | cpr::cpr 50 | nlohmann_json::nlohmann_json 51 | matchit::matchit 52 | magic_enum::magic_enum 53 | easyloggingpp::easyloggingpp 54 | libjpeg-turbo::libjpeg-turbo 55 | PNG::PNG 56 | /usr/lib/libboost_filesystem.a 57 | ZLIB::ZLIB 58 | cppcoro::cppcoro 59 | libzippp::libzippp 60 | cryptopp::cryptopp 61 | LibArchive::LibArchive 62 | ) 63 | target_include_directories(main PRIVATE ${fmt_INCLUDE_DIRS}) 64 | 65 | # Boot executables for each platform 66 | add_executable(boot boot/boot.cpp) 67 | target_link_libraries(boot PRIVATE 68 | fmt::fmt 69 | argparse::argparse 70 | cpr::cpr 71 | nlohmann_json::nlohmann_json 72 | matchit::matchit 73 | magic_enum::magic_enum 74 | easyloggingpp::easyloggingpp 75 | libjpeg-turbo::libjpeg-turbo 76 | PNG::PNG 77 | /usr/lib/libboost_filesystem.a 78 | ZLIB::ZLIB 79 | cppcoro::cppcoro 80 | ) 81 | target_include_directories(boot PRIVATE ${fmt_INCLUDE_DIRS}) 82 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | # Enable community repo 4 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/main/ > /etc/apk/repositories 5 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/community/ >> /etc/apk/repositories 6 | RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/testing/ >> /etc/apk/repositories 7 | 8 | # Install deps 9 | RUN apk update && apk upgrade 10 | RUN apk add --no-cache build-base git libbsd-dev py3-pip pipx git patchelf cmake gcc \ 11 | bash e2fsprogs xz curl zstd gawk debootstrap m4 gcompat nasm 12 | 13 | # Install conan 14 | RUN pipx install conan 15 | 16 | # Update PATH 17 | ENV PATH="/root/.local/bin:$PATH" 18 | 19 | # Setup 20 | RUN conan profile detect --force 21 | 22 | # Copy files 23 | RUN mkdir /gameimage 24 | COPY . /gameimage/ 25 | 26 | # Set workdir 27 | WORKDIR /gameimage 28 | 29 | # Compile 30 | RUN conan install . --build=missing -g CMakeDeps -g CMakeToolchain --output-folder=build || true 31 | # nasm bug when on musl system "undefined symbol" 32 | RUN cp "$(command -v nasm)" /root/.conan2/p/nasmc*/p/bin/nasm 33 | RUN conan install . --build=missing -g CMakeDeps -g CMakeToolchain --output-folder=build || true 34 | RUN cmake --preset conan-release -DCMAKE_BUILD_TYPE=Release 35 | RUN cmake --build --preset conan-release 36 | 37 | # Build image 38 | # RUN cp dist/main bin/elf 39 | # RUN ./src/scripts/_build.sh debootstrap focal 40 | # RUN ./src/scripts/_build.sh archbootstrap 41 | # RUN ./src/scripts/_build.sh alpinebootstrap 42 | -------------------------------------------------------------------------------- /src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | # export HOME="$SCRIPT_DIR" 6 | 7 | conan install . --build=missing -g CMakeDeps -g CMakeToolchain -s build_type=Debug 8 | cmake --preset conan-debug -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 9 | cmake --build --preset conan-debug 10 | -------------------------------------------------------------------------------- /src/cmd/compress.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : compress 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "../enum.hpp" 9 | 10 | #include "../std/filesystem.hpp" 11 | 12 | #include "../lib/db/build.hpp" 13 | #include "../lib/db/project.hpp" 14 | #include "../lib/subprocess.hpp" 15 | 16 | namespace ns_compress 17 | { 18 | 19 | namespace fs = std::filesystem; 20 | 21 | // validate() {{{ 22 | inline void validate(ns_db::ns_build::Metadata& db_metadata, ns_db::ns_project::Project& db_project) 23 | { 24 | // Icon 25 | "Icon is not installed"_try([&] 26 | { 27 | fs::path path_file_icon = db_metadata.path_dir_project / db_project.path_file_icon; 28 | ns_fs::ns_path::file_exists(path_file_icon); 29 | ns_log::write('i', "Found icon '", path_file_icon, "'"); 30 | }); 31 | 32 | 33 | auto f_validate_file_or_directory = [&](ns_enum::Op const& op) 34 | { 35 | fs::path path_file = db_metadata.path_dir_project / db_project.find_file(op); 36 | try 37 | { 38 | ns_fs::ns_path::file_exists(path_file); 39 | } // try 40 | catch(std::exception const& e) 41 | { 42 | ns_fs::ns_path::dir_exists(path_file); 43 | } // catch 44 | }; 45 | 46 | auto f_validate_files = [&](ns_enum::Op const& op) 47 | { 48 | std::vector paths_file = db_project.find_files(op); 49 | 50 | for(auto&& path_file : paths_file) 51 | { 52 | "Missing file {} in json for '{}'"_try([&] 53 | { 54 | try 55 | { 56 | ns_fs::ns_path::file_exists(db_metadata.path_dir_project / path_file); 57 | } // try 58 | catch(std::exception const& e) 59 | { 60 | ns_fs::ns_path::dir_exists(db_metadata.path_dir_project / path_file); 61 | } // catch 62 | }, ns_enum::to_string(op), path_file); 63 | } // for 64 | }; 65 | 66 | switch(db_metadata.platform) 67 | { 68 | case ns_enum::Platform::LINUX: 69 | case ns_enum::Platform::WINE: 70 | { 71 | f_validate_file_or_directory(ns_enum::Op::ROM); 72 | } 73 | break; 74 | case ns_enum::Platform::RETROARCH: 75 | { 76 | // default rom 77 | f_validate_file_or_directory(ns_enum::Op::ROM); 78 | // default core 79 | f_validate_file_or_directory(ns_enum::Op::CORE); 80 | // all roms 81 | f_validate_files(ns_enum::Op::ROM); 82 | // all cores 83 | f_validate_files(ns_enum::Op::CORE); 84 | } // case 85 | break; 86 | case ns_enum::Platform::PCSX2: 87 | { 88 | // default rom 89 | f_validate_file_or_directory(ns_enum::Op::ROM); 90 | // default bios 91 | f_validate_file_or_directory(ns_enum::Op::BIOS); 92 | // all roms 93 | f_validate_files(ns_enum::Op::ROM); 94 | // all bios 95 | f_validate_files(ns_enum::Op::BIOS); 96 | } // case 97 | break; 98 | case ns_enum::Platform::RPCS3: 99 | { 100 | // default rom 101 | f_validate_file_or_directory(ns_enum::Op::ROM); 102 | } // case 103 | break; 104 | } // switch 105 | 106 | } // validate() }}} 107 | 108 | // compress() {{{ 109 | inline decltype(auto) compress() 110 | { 111 | // Open databases 112 | auto db_build = ns_db::ns_build::read(); 113 | ethrow_if(not db_build, "Could not open build database"); 114 | auto db_metadata = db_build->find(db_build->project); 115 | auto db_project = ns_db::ns_project::read(); 116 | ethrow_if(not db_project, "Could not open project database"); 117 | 118 | // Validate package by platform 119 | validate(db_metadata, *db_project); 120 | 121 | // Output file 122 | fs::path path_file_layer{db_metadata.path_dir_project_root.string() + ".layer"}; 123 | 124 | // Erase if exists 125 | lec(fs::remove, path_file_layer); 126 | 127 | // Log 128 | ns_log::write('i', "project: ", db_metadata.name); 129 | ns_log::write('i', "image: ", db_build->path_file_image); 130 | ns_log::write('i', "layer: ", path_file_layer); 131 | 132 | // Execute portal 133 | auto f_portal = [](Args&&... args) 134 | { 135 | std::ignore = ns_subprocess::Subprocess("/fim/static/fim_portal") 136 | .with_piped_outputs() 137 | .with_args(std::forward(args)...) 138 | .spawn() 139 | .wait(); 140 | }; 141 | 142 | // Compress 143 | f_portal(db_build->path_file_image , "fim-layer" , "create" , db_metadata.path_dir_project_root , path_file_layer); 144 | 145 | ns_log::write('i', "Wrote file to '", path_file_layer, "'"); 146 | } // compress() }}} 147 | 148 | } // namespace ns_compress 149 | 150 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 151 | -------------------------------------------------------------------------------- /src/cmd/desktop.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : desktop 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "../lib/image.hpp" 9 | #include "../lib/db/build.hpp" 10 | #include "../lib/subprocess.hpp" 11 | #include "../std/vector.hpp" 12 | 13 | namespace ns_desktop 14 | { 15 | 16 | enum class Op 17 | { 18 | ICON, 19 | SETUP, 20 | }; 21 | 22 | namespace 23 | { 24 | 25 | namespace fs = std::filesystem; 26 | 27 | } 28 | 29 | enum class IntegrationItems 30 | { 31 | MIMETYPE, 32 | ENTRY, 33 | ICON 34 | }; 35 | 36 | 37 | // icon() {{{ 38 | inline void icon(fs::path const& path_file_icon) 39 | { 40 | // Open databases 41 | auto db_build = ns_db::ns_build::read(); 42 | ethrow_if(not db_build, "Could not open build database"); 43 | auto db_metadata = db_build->find(db_build->project); 44 | 45 | // Create icon path 46 | fs::path path_file_icon_dst = db_build->path_dir_build / "desktop.png"; 47 | 48 | // Resize icon to specified icon path 49 | ns_image::resize(path_file_icon, path_file_icon_dst, 300, 450); 50 | } // icon() }}} 51 | 52 | // desktop() {{{ 53 | inline void desktop(std::string str_name, std::vector vec_items) 54 | { 55 | // Open databases 56 | auto db_build = ns_db::ns_build::read(); 57 | ethrow_if(not db_build, "Could not open build database"); 58 | auto db_metadata = db_build->find(db_build->project); 59 | 60 | // Check if output file exists 61 | db_build->path_file_output = ns_fs::ns_path::file_exists(db_build->path_file_output)._ret; 62 | 63 | // Path to project 64 | fs::path path_dir_build = db_build->path_dir_build; 65 | fs::path path_file_desktop = path_dir_build / "desktop.json"; 66 | fs::path path_file_icon = ns_fs::ns_path::file_exists(path_dir_build / "desktop.png")._ret; 67 | 68 | // Create application data 69 | std::ignore = ns_db::from_file(path_file_desktop 70 | , [&](auto&& db) 71 | { 72 | db("name") = str_name; 73 | db("icon") = path_file_icon; 74 | db("categories") = std::vector{"Game"}; 75 | }, ns_db::Mode::CREATE); 76 | 77 | // Apply application data 78 | std::ignore = ns_subprocess::Subprocess("/fim/static/fim_portal") 79 | .with_piped_outputs() 80 | .with_args(db_build->path_file_output, "fim-desktop", "setup", path_file_desktop) 81 | .spawn() 82 | .wait(); 83 | 84 | // Enable desktop integration 85 | std::ignore = ns_subprocess::Subprocess("/fim/static/fim_portal") 86 | .with_piped_outputs() 87 | .with_args(db_build->path_file_output 88 | , "fim-desktop" 89 | , "enable" 90 | , ns_string::from_container(vec_items , ',', [](auto&& e){ return ns_enum::to_string(e); })) 91 | .spawn() 92 | .wait(); 93 | } // desktop() }}} 94 | 95 | } // namespace ns_test 96 | 97 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 98 | -------------------------------------------------------------------------------- /src/cmd/fetch/check.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : check 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include "../../lib/log.hpp" 11 | #include "../../lib/sha.hpp" 12 | 13 | namespace ns_fetch::ns_check 14 | { 15 | 16 | namespace 17 | { 18 | 19 | namespace fs = std::filesystem; 20 | 21 | // check_file_from_sha() {{{ 22 | inline bool check_file_from_sha(fs::path const& path_file_src 23 | , fs::path const& path_file_sha 24 | , ns_sha::SHA_TYPE const& sha_type) 25 | { 26 | return ns_sha::check_sha(path_file_src, path_file_sha, sha_type); 27 | } // }}} 28 | 29 | // check_file_from_url_impl() {{{ 30 | inline bool check_file_from_url_impl(fs::path const& path_file_src, cpr::Url const& url) 31 | { 32 | uintmax_t size_reference = fs::file_size(path_file_src); 33 | uintmax_t size_calculated = 0; 34 | 35 | ns_log::write('i', "SIZE: Reference is ", size_reference); 36 | 37 | // Get size of file to download 38 | cpr::Response response_head = cpr::Head(url); 39 | if ( response_head.status_code != 200 ) 40 | { 41 | ns_log::write('e', "Could not fetch remote size to compare local size with"); 42 | return false; 43 | } // if 44 | 45 | auto it = response_head.header.find("Content-Length"); 46 | if (it == response_head.header.end()) 47 | { 48 | ns_log::write('e', "Could not find field 'Content-Length' in response"); 49 | return false; 50 | } // if 51 | 52 | size_calculated = std::stoi(it->second); 53 | ns_log::write('i', "SIZE: Calculated is ", size_calculated); 54 | 55 | if ( size_reference != size_calculated ) 56 | { 57 | ns_log::write('e', "Size reference differs from size_calculated"); 58 | return false; 59 | } // if 60 | 61 | return true; 62 | } // check_file_from_url_impl() }}} 63 | 64 | // check_file_from_url() {{{ 65 | inline bool check_file_from_url(fs::path const& path_file_src, cpr::Url const& url) 66 | { 67 | auto expected_check = ns_exception::to_expected([&]{ return check_file_from_url_impl(path_file_src, url); }); 68 | ereturn_if(not expected_check, expected_check.error(), false); 69 | return *expected_check; 70 | } // check_file_from_url() }}} 71 | 72 | } // namespace 73 | 74 | // check_file() {{{ 75 | inline bool check_file(fs::path const& path_file_src 76 | , fs::path const& path_file_sha 77 | , ns_sha::SHA_TYPE const& sha_type 78 | , cpr::Url const& url) 79 | { 80 | return check_file_from_sha(path_file_src, path_file_sha, sha_type) or check_file_from_url(path_file_src, url); 81 | } // check_file() }}} 82 | 83 | } // namespace ns_fetch::ns_check 84 | 85 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 86 | -------------------------------------------------------------------------------- /src/cmd/init.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : init 4 | // @created : Friday Jan 19, 2024 19:13:00 -03 5 | /// 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | #include "project.hpp" 12 | #include "../macro.hpp" 13 | #include "../enum.hpp" 14 | 15 | #include "../std/filesystem.hpp" 16 | #include "../std/env.hpp" 17 | 18 | #include "../lib/log.hpp" 19 | #include "../lib/subprocess.hpp" 20 | #include "../lib/db/build.hpp" 21 | #include "../lib/db/project.hpp" 22 | 23 | // 24 | // Initializes a new directory configuration for gameimage 25 | // 26 | namespace ns_init 27 | { 28 | 29 | namespace fs = std::filesystem; 30 | 31 | // build() {{{ 32 | inline void build(fs::path path_dir_build) 33 | { 34 | // Log 35 | ns_log::write('i', "path_dir_build: ", path_dir_build); 36 | // Initialize projects data 37 | elogerror(ns_db::ns_build::init(path_dir_build)); 38 | } // function: build }}} 39 | 40 | // project() {{{ 41 | inline void project(std::string const& str_name, ns_enum::Platform const& platform) 42 | { 43 | // Read build database 44 | auto db_build = ns_db::ns_build::read(); 45 | ethrow_if(not db_build, "Could not read build database"); 46 | // Create project dir and return an absolute path to it 47 | fs::path path_dir_project_root = ns_fs::ns_path::dir_create(db_build->path_dir_build / str_name)._ret; 48 | // The actual project files are nested in /opt/gameimage-games, because that's the final path 49 | // inside the container 50 | fs::path path_dir_project = ns_fs::ns_path::dir_create(db_build->path_dir_build / str_name / "opt" / "gameimage-games" / str_name)._ret; 51 | // Log 52 | ns_log::write('i', "platform :", ns_enum::to_string_lower(platform)); 53 | ns_log::write('i', "image :", db_build->path_file_image); 54 | ns_log::write('i', "path_dir_project_root :", path_dir_project_root); 55 | ns_log::write('i', "path_dir_project :", path_dir_project); 56 | // Create novel metadata for project 57 | db_build->projects.push_back(ns_db::ns_build::Metadata 58 | { 59 | .name = str_name 60 | , .path_dir_project = path_dir_project 61 | , .path_dir_project_root = path_dir_project_root 62 | , .platform = platform 63 | }); 64 | // Write changes to database 65 | ns_db::ns_build::write(*db_build); 66 | // Set project as default 67 | ns_project::set(str_name); 68 | // Copy boot file for platform 69 | auto path_file_boot = ns_subprocess::search_path("gameimage-boot"); 70 | ethrow_if(not path_file_boot, "Could not find gameimage-boot in PATH"); 71 | lec(fs::copy_file 72 | , *path_file_boot 73 | , path_dir_project / "boot" 74 | , fs::copy_options::overwrite_existing 75 | ); 76 | ns_log::write('i', "Copy ", *path_file_boot, " -> ", path_dir_project / "boot"); 77 | // Create project database 78 | elogerror(ns_db::ns_project::init(path_dir_project, platform)); 79 | } // function: project }}} 80 | 81 | } // namespace ns_init 82 | 83 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 84 | -------------------------------------------------------------------------------- /src/cmd/project.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | ///@author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | ///@file : project 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #include "../lib/db/build.hpp" 11 | 12 | namespace ns_project 13 | { 14 | 15 | namespace 16 | { 17 | 18 | namespace fs = std::filesystem; 19 | 20 | } // namespace 21 | 22 | // set() {{{ 23 | [[nodiscard]] inline std::expected set(std::string const& name) noexcept 24 | { 25 | // Open db 26 | auto db_build = ns_db::ns_build::read(); 27 | qreturn_if(not db_build, std::unexpected("Could not open build database")); 28 | // Check if 'name' is present in db 29 | auto const& opt_metadata = ns_exception::to_expected([&]{ return db_build->find(name); }); 30 | qreturn_if(not opt_metadata, std::unexpected("Could not find project '{}'"_fmt(name))); 31 | // Set as current 32 | db_build->project = opt_metadata->name; 33 | // Update database 34 | ns_db::ns_build::write(*db_build); 35 | ns_log::write('i', "Set default project to: ", name); 36 | return {}; 37 | } // set() }}} 38 | 39 | // del() {{{ 40 | [[nodiscard]] inline std::expected del(std::string_view str_name) noexcept 41 | { 42 | // Open build db 43 | auto db_build = ns_db::ns_build::read(); 44 | qreturn_if(not db_build, std::unexpected("Could not open build database")); 45 | // Find project 46 | auto it = std::ranges::find_if(db_build->projects 47 | , [&](auto&& e){ return e.name == str_name; } 48 | ); 49 | qreturn_if(it == std::ranges::end(db_build->projects) 50 | , std::unexpected("Project '{}' not found to delete"_fmt(str_name)) 51 | ); 52 | // Erase project files from disk 53 | lec(fs::remove_all, it->path_dir_project_root); 54 | lec(fs::remove, it->path_dir_project_root.string() + ".layer"); 55 | // Erase project from database 56 | db_build->projects.erase(it); 57 | // If is current project, blank it out 58 | if ( db_build->project == str_name ) { db_build->project = ""; } // if 59 | ns_db::ns_build::write(*db_build); 60 | return {}; 61 | } // del() }}} 62 | 63 | } // namespace ns_project 64 | 65 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 66 | -------------------------------------------------------------------------------- /src/cmd/select.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : select 4 | // @created : Monday Feb 05, 2024 15:02:17 -03 5 | /// 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | #include 12 | 13 | #include "../common.hpp" 14 | #include "../enum.hpp" 15 | 16 | #include "../std/filesystem.hpp" 17 | 18 | #include "../lib/db/build.hpp" 19 | #include "../lib/db/project.hpp" 20 | 21 | namespace ns_select 22 | { 23 | 24 | namespace fs = std::filesystem; 25 | 26 | using Op = ns_enum::Op; 27 | 28 | // by_op() {{{ 29 | inline void by_op(ns_enum::Platform enum_platform 30 | , Op op 31 | , fs::path path_dir_project 32 | , fs::path path_file_target) 33 | { 34 | switch(enum_platform) 35 | { 36 | case ns_enum::Platform::LINUX: 37 | { 38 | "Only the rom option is available for linux"_throw_if([&]{ return op != Op::ROM; }); 39 | } // case 40 | break; 41 | case ns_enum::Platform::WINE: 42 | { 43 | "Only the rom option is available for wine"_throw_if([&]{ return op != Op::ROM; }); 44 | } // case 45 | break; 46 | case ns_enum::Platform::RETROARCH: 47 | { 48 | "Only rom, core and bios options are available for retroarch"_throw_if([&] 49 | { 50 | return op != Op::ROM && op != Op::CORE && op != Op::BIOS; 51 | }); 52 | } // case 53 | break; 54 | case ns_enum::Platform::PCSX2: 55 | { 56 | "Only rom and bios options are available for pcsx2"_throw_if([&] 57 | { 58 | return op != Op::ROM && op != Op::BIOS; 59 | }); 60 | } // case 61 | break; 62 | case ns_enum::Platform::RPCS3: 63 | { 64 | "Only rom and bios options are available for rpcs3"_throw_if([&] 65 | { 66 | return op != Op::ROM && op != Op::BIOS; 67 | }); 68 | } // case 69 | break; 70 | } // switch 71 | 72 | // Check if is regular file or directory 73 | try 74 | { 75 | ns_fs::ns_path::file_exists(path_dir_project / path_file_target); 76 | } // try 77 | catch(std::exception const& e) 78 | { 79 | ns_fs::ns_path::dir_exists(path_dir_project / path_file_target); 80 | } // catch 81 | 82 | auto db_project = ns_db::ns_project::read(); 83 | ethrow_if(not db_project, "Could not open project database '{}'"_fmt(db_project.error())); 84 | switch (op) 85 | { 86 | case ns_enum::Op::ROM: db_project->path_file_rom = path_file_target; break; 87 | case ns_enum::Op::BIOS: db_project->path_file_bios = path_file_target; break; 88 | case ns_enum::Op::CORE: db_project->path_file_core = path_file_target; break; 89 | default: throw std::runtime_error("Cannot set default for '{}'"_fmt(ns_enum::to_string(op))); 90 | } // switch 91 | ns_db::ns_project::write(*db_project); 92 | } // select() }}} 93 | 94 | // select() {{{ 95 | inline void select(Op const& op, fs::path const& path_file_target) 96 | { 97 | // Open db 98 | auto db_build = ns_db::ns_build::read(); 99 | ethrow_if(not db_build, "Could not open build database"); 100 | auto db_metadata = db_build->find(db_build->project); 101 | // Select the default file by platform 102 | by_op(db_metadata.platform, op, db_metadata.path_dir_project, path_file_target); 103 | } // }}} 104 | 105 | } // namespace ns_select 106 | 107 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 108 | -------------------------------------------------------------------------------- /src/cmd/test.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : test 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "../lib/db/build.hpp" 9 | #include "../lib/subprocess.hpp" 10 | 11 | namespace ns_test 12 | { 13 | 14 | // test() {{{ 15 | inline decltype(auto) test() 16 | { 17 | // Open db 18 | auto db_build = ns_db::ns_build::read(); 19 | ethrow_if(not db_build, "Could not open build database"); 20 | auto db_metadata = db_build->find(db_build->project); 21 | 22 | // Start application 23 | std::ignore = ns_subprocess::Subprocess("/fim/static/fim_portal") 24 | .with_piped_outputs() 25 | .with_args(db_build->path_file_image, "fim-exec", db_metadata.path_dir_project / "boot") 26 | .spawn() 27 | .wait(); 28 | } // test() }}} 29 | 30 | } // namespace ns_test 31 | 32 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 33 | -------------------------------------------------------------------------------- /src/common.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : common 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "std/concepts.hpp" 15 | #include "std/string.hpp" 16 | 17 | #include "lib/log.hpp" 18 | 19 | inline const char * GIMG_PATH_JSON_FETCH = "/tmp/gameimage/json"; 20 | 21 | namespace std 22 | { 23 | 24 | template 25 | using error = optional; 26 | 27 | namespace ranges 28 | { 29 | 30 | template 31 | void sort_unique(R& r) 32 | { 33 | sort(r); 34 | auto [it_beg,it_end] = std::ranges::unique(r); 35 | r.erase(it_beg, it_end); 36 | } 37 | 38 | } 39 | 40 | } // namespace std 41 | 42 | // macros {{{ 43 | #define throw_if(cond, msg) \ 44 | if (cond) { throw std::runtime_error(msg); } 45 | 46 | #define return_if(cond, ...) \ 47 | if (cond) { return __VA_ARGS__; } 48 | 49 | #define return_if_else(cond, val1, val2) \ 50 | if (cond) { return val1; } else { return val2; } 51 | 52 | #define break_if(cond) \ 53 | if ( (cond) ) { break; } 54 | 55 | #define continue_if(cond) \ 56 | if ( (cond) ) { continue; } 57 | 58 | #define assign_if(cond, var, val) \ 59 | if ( cond ) { var = val; } 60 | 61 | #define assign_or_return(val, cond, ret) \ 62 | val; if ( not cond ) { return ret; } 63 | // }}} 64 | 65 | // class Exception {{{ 66 | class Exception : public std::exception 67 | { 68 | private: 69 | std::string m_msg; 70 | 71 | public: 72 | Exception(std::string const& msg) 73 | : m_msg(msg) 74 | { 75 | } // Exception 76 | const char * what() const noexcept override 77 | { 78 | return m_msg.c_str(); 79 | } // what() 80 | }; // class: Exception }}} 81 | 82 | // User defined literals {{{ 83 | 84 | // Format strings with user-defined literals 85 | inline decltype(auto) operator ""_fmt(const char* str, size_t) 86 | { 87 | return [str](Args&&... args) 88 | { 89 | return fmt::format(fmt::runtime(str), ns_string::to_string(std::forward(args))... ) ; 90 | }; 91 | } // 92 | 93 | // Format strings with user-defined literals 94 | inline decltype(auto) operator ""_throw(const char* str, size_t) 95 | { 96 | return [str](Args&&... args) 97 | { 98 | throw Exception(fmt::format(fmt::runtime(str), ns_string::to_string(std::forward(args))...)); 99 | }; 100 | } 101 | 102 | // Format strings with user-defined literals, throws if condition is false 103 | inline decltype(auto) operator ""_catch(const char* str, size_t) 104 | { 105 | return [str](F&& f, Args&&... args) 106 | { 107 | try 108 | { 109 | f(); 110 | } // try 111 | catch(std::exception const& e) 112 | { 113 | ns_log::write('e', e.what()); 114 | ns_log::write('e', fmt::format(fmt::runtime(str), ns_string::to_string(std::forward(args))...)); 115 | } // catch 116 | }; 117 | } 118 | 119 | // Format strings with user-defined literals, throws if condition is false 120 | inline decltype(auto) operator ""_throw_if(const char* str, size_t) 121 | { 122 | return [str](F&& f, Args&&... args) 123 | { 124 | if ( f() ) 125 | { 126 | throw Exception(fmt::format(fmt::runtime(str), ns_string::to_string(std::forward(args))...)); 127 | } 128 | }; 129 | } 130 | 131 | // Format strings with user-defined literals, throws if condition is false 132 | inline decltype(auto) operator ""_try(const char* str, size_t) 133 | { 134 | return [str](F&& f, Args&&... args) 135 | { 136 | try 137 | { 138 | f(); 139 | } // try 140 | catch(std::exception const& e) 141 | { 142 | ns_log::write('e', e.what()); 143 | throw Exception(fmt::format(fmt::runtime(str), ns_string::to_string(std::forward(args))...)); 144 | } // catch 145 | }; 146 | } 147 | 148 | // }}} 149 | 150 | // ns_common {{{ 151 | 152 | namespace ns_common 153 | { 154 | 155 | // check_and() {{{ 156 | template 157 | constexpr bool check_and(T&& t, Args&&... flags) 158 | { 159 | return static_cast(t) & ( static_cast(flags) & ... ); 160 | } // check_and() }}} 161 | 162 | // catch_to_optional() {{{ 163 | template 164 | auto catch_to_optional(F&& f, Args&&... args) -> std::optional 165 | { 166 | try 167 | { 168 | auto val = make_optional(f(std::forward(args)...)); 169 | ns_log::write('i', "Optional: ", *val); 170 | return val; 171 | } // try 172 | catch(std::exception const& e) 173 | { 174 | ns_log::write('i', "Optional (caught): ", e.what()); 175 | return std::nullopt; 176 | } // catch 177 | } // catch_to_optional() }}} 178 | 179 | } // namespace ns_common}}} 180 | 181 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 182 | -------------------------------------------------------------------------------- /src/conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | fmt/10.1.1 3 | argparse/3.0 4 | cpr/1.10.5 5 | nlohmann_json/3.11.3 6 | matchit/1.0.1 7 | magic_enum/0.9.5 8 | easyloggingpp/9.97.1 9 | libjpeg-turbo/3.0.1 10 | libpng/1.6.40 11 | zlib/1.3 12 | andreasbuhr-cppcoro/cci.20230629 13 | libzippp/7.1-1.10.1 14 | cryptopp/8.9.0 15 | libarchive/3.7.2 16 | 17 | [generators] 18 | CMakeDeps 19 | CMakeToolchain 20 | 21 | [layout] 22 | cmake_layout 23 | -------------------------------------------------------------------------------- /src/enum.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : enum 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | 11 | #include "common.hpp" 12 | 13 | #include "std/string.hpp" 14 | #include "std/concepts.hpp" 15 | 16 | namespace magic = magic_enum; 17 | 18 | // namespace ns_enum 19 | namespace ns_enum 20 | { 21 | 22 | // Concepts 23 | template 24 | concept Enum = std::is_enum_v>; 25 | 26 | // enum class IpcQuery {{{ 27 | enum class IpcQuery 28 | { 29 | FILES, 30 | URLS, 31 | INSTALLED, 32 | NONE, 33 | }; // }}} 34 | 35 | // enum class ImageFormat {{{ 36 | enum class ImageFormat 37 | { 38 | PNG, 39 | JPG, 40 | JPEG, 41 | }; // }}} 42 | 43 | // enum class Platform {{{ 44 | enum class Platform 45 | { 46 | LINUX, 47 | WINE, 48 | RETROARCH, 49 | PCSX2, 50 | RPCS3, 51 | }; // enum class Platform }}} 52 | 53 | // enum class Op {{{ 54 | enum class Op 55 | { 56 | ICON, 57 | ROM, 58 | LINUX, 59 | CORE, 60 | BIOS, 61 | KEYS, 62 | CONFIG, 63 | DATA, 64 | GUI, 65 | WINE, 66 | WINETRICKS, 67 | DXVK, 68 | VKD3D 69 | }; // enum class Op }}} 70 | 71 | // enum class Stage {{{ 72 | enum class Stage 73 | { 74 | FETCH, 75 | INIT, 76 | PROJECT, 77 | INSTALL, 78 | SEARCH, 79 | SELECT, 80 | TEST, 81 | DESKTOP, 82 | COMPRESS, 83 | PACKAGE, 84 | }; // enum class Stage }}} 85 | 86 | // is_enum_entry() {{{ 87 | template 88 | inline decltype(auto) is_enum_entry(T&& t) 89 | { 90 | return magic::enum_cast(ns_string::to_string(t), magic::case_insensitive).has_value(); 91 | } // }}} 92 | 93 | // from_string() {{{ 94 | template 95 | inline decltype(auto) from_string(T&& t) 96 | { 97 | auto opt = magic::enum_cast(ns_string::to_string(t), magic::case_insensitive); 98 | 99 | if ( ! opt ) { "Could not convert enum from '{}'"_throw(t); } // if 100 | 101 | return U{*opt}; 102 | } // }}} 103 | 104 | // to_string() {{{ 105 | template 106 | inline std::string to_string(T&& t) 107 | { 108 | return std::string(magic::enum_name(std::forward(t))); 109 | } // to_string() }}} 110 | 111 | // to_string_lower() {{{ 112 | template 113 | inline std::string to_string_lower(T&& t) 114 | { 115 | return ns_string::to_lower(ns_string::to_string(magic::enum_name(std::forward(t)))); 116 | } // to_string_lower() }}} 117 | 118 | // check_and() {{{ 119 | template 120 | constexpr bool check_and(T&& t, Args&&... flags) 121 | { 122 | return static_cast(t) & ( static_cast(flags) & ... ); 123 | } // check_and() }}} 124 | 125 | } // namespace ns_enum 126 | 127 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 128 | -------------------------------------------------------------------------------- /src/lib/db/fetch.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : fetch 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "../hope.hpp" 9 | #include "../db.hpp" 10 | #include "../../macro.hpp" 11 | 12 | namespace ns_db::ns_fetch 13 | { 14 | 15 | namespace 16 | { 17 | 18 | // Forward declarations 19 | class Fetch; 20 | std::expected read_impl(fs::path const& path_file_db); 21 | 22 | // platforms() {{{ 23 | struct CoreUrl { std::string core; std::string url; }; 24 | struct Platform 25 | { 26 | private: 27 | std::map m_url_layer; 28 | std::vector m_vec_core_url; 29 | public: 30 | virtual std::string get_layer(std::string identifier = "default") 31 | { 32 | ethrow_if(not m_url_layer.contains(identifier), "Layer '{}' not found for platform"_fmt(identifier)); 33 | return m_url_layer[identifier]; 34 | }; 35 | virtual std::vector get_cores() const { return m_vec_core_url; }; 36 | virtual ~Platform() {}; 37 | friend std::expected read_impl(fs::path const& path_file_db); 38 | }; 39 | struct Linux : public Platform {}; 40 | struct Retroarch : public Platform {}; 41 | struct Pcsx2 : public Platform {}; 42 | struct Rpcs3 : public Platform {}; 43 | struct Wine : public Platform {}; 44 | // platforms() }}} 45 | 46 | // class Fetch {{{ 47 | class Fetch 48 | { 49 | private: 50 | std::unique_ptr m_linux = std::make_unique(); 51 | std::unique_ptr m_retroarch = std::make_unique(); 52 | std::unique_ptr m_pcsx2 = std::make_unique(); 53 | std::unique_ptr m_rpcs3 = std::make_unique(); 54 | std::unique_ptr m_wine = std::make_unique(); 55 | std::string m_version; 56 | Fetch() = default; 57 | public: 58 | std::unique_ptr get_platform(ns_enum::Platform platform) const 59 | { 60 | switch (platform) 61 | { 62 | case ns_enum::Platform::LINUX : return std::make_unique(*m_linux); 63 | case ns_enum::Platform::RETROARCH : return std::make_unique(*m_retroarch); 64 | case ns_enum::Platform::PCSX2 : return std::make_unique(*m_pcsx2); 65 | case ns_enum::Platform::RPCS3 : return std::make_unique(*m_rpcs3); 66 | case ns_enum::Platform::WINE : return std::make_unique(*m_wine); 67 | } // switch 68 | throw std::runtime_error("Unknown platform"); 69 | } // get_platform 70 | 71 | friend std::expected read_impl(fs::path const& path_file_db); 72 | }; // class Fetch }}} 73 | 74 | // read_impl() {{{ 75 | inline std::expected read_impl(fs::path const& path_file_db) 76 | { 77 | return ns_db::from_file>(path_file_db, [&](auto&& db) -> std::expected 78 | { 79 | Fetch fetch; 80 | // Linux 81 | fetch.m_linux->m_url_layer["default"] = ehope(db.template value("linux", "layer")); 82 | // Pcsx2 83 | fetch.m_pcsx2->m_url_layer["default"] = ehope(db.template value("pcsx2", "layer")); 84 | // Rpcs3 85 | fetch.m_rpcs3->m_url_layer["default"] = ehope(db.template value("rpcs3", "layer")); 86 | // Wine 87 | auto layers = ehope(db.value("wine", "layer")); 88 | for(auto const& key : layers.keys()) 89 | { 90 | fetch.m_wine->m_url_layer[key] = ehope(db.template value("wine", "layer", key)); 91 | } // for 92 | // Retroarch 93 | fetch.m_retroarch->m_url_layer["default"] = ehope(db.template value("retroarch", "layer")); 94 | auto cores = ehope(db.value("retroarch", "core")); 95 | for(auto const& key : cores.keys()) 96 | { 97 | if (auto value = db.template value("retroarch", "core", key)) 98 | { 99 | fetch.m_retroarch->m_vec_core_url.push_back(CoreUrl{ key, *value }); 100 | } // if 101 | else 102 | { 103 | ns_log::write('e', "Could not get value for key '", key, "'"); 104 | } // else 105 | } // for 106 | return fetch; 107 | }, ns_db::Mode::READ).value_or(std::unexpected("Could not read 'fetch' database")); 108 | } // read_impl() }}} 109 | 110 | } // namespace 111 | 112 | // read() {{{ 113 | inline std::expected read(fs::path const& path_file_db) 114 | { 115 | return read_impl(path_file_db); 116 | } // read() }}} 117 | 118 | } // namespace ns_db::ns_fetch 119 | 120 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 121 | -------------------------------------------------------------------------------- /src/lib/hope.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | /// @file : hope 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | #define hope(expr,ferror,...) \ 11 | ({ \ 12 | auto _tmp = expr; \ 13 | if (!_tmp.has_value()) \ 14 | { \ 15 | ferror(__VA_ARGS__); \ 16 | ferror(_tmp.error()); \ 17 | return std::unexpected(_tmp.error()); \ 18 | } \ 19 | _tmp.value(); \ 20 | }) 21 | 22 | #define ehope(expr, ...) hope(expr, [](Ts&&... ts){ ns_log::write('e', std::forward(ts)...); }, __VA_ARGS__) 23 | #define ihope(expr, ...) hope(expr, [](Ts&&... ts){ ns_log::write('i', std::forward(ts)...); }, __VA_ARGS__) 24 | #define dhope(expr, ...) hope(expr, [](Ts&&... ts){ ns_log::write('d', std::forward(ts)...); }, __VA_ARGS__) 25 | 26 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 27 | -------------------------------------------------------------------------------- /src/lib/ipc.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : ipc 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "../common.hpp" 13 | #include "../std/concepts.hpp" 14 | #include "../std/string.hpp" 15 | #include "../std/filesystem.hpp" 16 | 17 | #include "../lib/log.hpp" 18 | 19 | namespace ns_ipc 20 | { 21 | 22 | namespace fs = std::filesystem; 23 | 24 | struct message_buffer 25 | { 26 | long message_type; 27 | char message_text[1024]; 28 | }; 29 | 30 | // class Ipc {{{ 31 | class Ipc 32 | { 33 | private: 34 | key_t m_key; 35 | int m_message_queue_id; 36 | message_buffer m_buffer; 37 | Ipc(); 38 | public: 39 | template 40 | void send(T&& t); 41 | friend Ipc& ipc(); 42 | }; // class Ipc }}} 43 | 44 | // Ipc::Ipc() {{{ 45 | inline Ipc::Ipc() 46 | : m_buffer({ .message_type = 1, .message_text = "" }) 47 | { 48 | fs::path path_file_self = ns_fs::ns_path::file_self()._ret; 49 | ns_log::write('i', "Starting IPC for ", path_file_self); 50 | 51 | std::string identifier = ns_string::to_string(path_file_self); 52 | ns_log::write('i', "key identifier: ", identifier); 53 | 54 | // Use a unique key for the message queue. 55 | if(m_key = ftok(identifier.c_str(), 65); m_key == -1 ) 56 | { 57 | perror("Could not generate token for message queue"); 58 | "Could not generate key for message queue with identifier '{}': {}"_throw(identifier, strerror(errno)); 59 | } // if 60 | ns_log::write('i', "Generated message_queue key: ", m_key); 61 | 62 | // Connect to the message queue 63 | if (m_message_queue_id = msgget(m_key, 0666); m_message_queue_id == -1 ) 64 | { 65 | perror("Could not create message queue"); 66 | "msgget failed, could not create message queue for identifier '{}': {}"_throw(identifier, strerror(errno)); 67 | } // if 68 | ns_log::write('i', "Message queue id: ", m_message_queue_id); 69 | } // Ipc::Ipc() }}} 70 | 71 | // Ipc::send() {{{ 72 | template 73 | void Ipc::send(T&& t) 74 | { 75 | std::string data = ns_string::to_string(t); 76 | // Limit data size 77 | size_t data_length = std::min(data.size(), sizeof(m_buffer)); 78 | // Copy the contents of std::string to the message_text buffer 79 | strncpy(m_buffer.message_text, data.c_str(), data_length); 80 | // Ensure null termination 81 | m_buffer.message_text[data_length] = '\0'; 82 | // Send message 83 | if ( msgsnd(m_message_queue_id, &m_buffer, data_length, 0) == -1 ) 84 | { 85 | perror("Failure to send message"); 86 | } // if 87 | } // Ipc::send() }}} 88 | 89 | // ipc() {{{ 90 | inline Ipc& ipc() 91 | { 92 | static Ipc ipc; 93 | return ipc; 94 | } // ipc() }}} 95 | 96 | } // namespace ns_ipc 97 | 98 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 99 | -------------------------------------------------------------------------------- /src/lib/log.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : log 4 | // @created : Sunday Jan 21, 2024 14:17:24 -03 5 | /// 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "../std/concepts.hpp" 14 | #include "../std/exception.hpp" 15 | 16 | namespace ns_log 17 | { 18 | 19 | namespace fs = std::filesystem; 20 | 21 | // logger_file() {{{ 22 | inline void logger_file(fs::path const& path_file_log) 23 | { 24 | // Configure 25 | el::Configurations default_conf; 26 | // To default 27 | default_conf.setToDefault(); 28 | // Values are always std::string 29 | default_conf.set(el::Level::Global 30 | , el::ConfigurationType::Format 31 | , "%levshort :: %msg"); 32 | // Configuration file 33 | default_conf.set(el::Level::Global 34 | , el::ConfigurationType::Filename 35 | , path_file_log); 36 | default_conf.set(el::Level::Global 37 | , el::ConfigurationType::ToStandardOutput 38 | , "false"); 39 | // default logger uses default configurations 40 | el::Loggers::reconfigureLogger("default", default_conf); 41 | } // logger_file() }}} 42 | 43 | // logger_stdout() {{{ 44 | inline void logger_stdout() 45 | { 46 | // Configure 47 | el::Configurations default_conf; 48 | // To default 49 | default_conf.setToDefault(); 50 | // Values are always std::string 51 | default_conf.set(el::Level::Global 52 | , el::ConfigurationType::Format 53 | , "%levshort :: %msg"); 54 | default_conf.set(el::Level::Global 55 | , el::ConfigurationType::ToStandardOutput 56 | , "true"); 57 | // default logger uses default configurations 58 | el::Loggers::reconfigureLogger("term", default_conf); 59 | } // logger_stdout() }}} 60 | 61 | // init() {{{ 62 | inline void init(int argc 63 | , char** argv 64 | , fs::path path_file_log) 65 | { 66 | // Remove log file if exists 67 | fs::remove(path_file_log); 68 | // Start easylogging 69 | START_EASYLOGGINGPP(argc, argv); 70 | // Create loggers 71 | logger_file(path_file_log); 72 | logger_stdout(); 73 | // Try to make canonical path for log file 74 | try 75 | { 76 | path_file_log = fs::canonical(path_file_log); 77 | LOG(INFO) << fmt::format(fmt::runtime("Log file {}"), path_file_log.string()); 78 | } // try 79 | catch (std::exception const& e) 80 | { 81 | LOG(ERROR) << fmt::format("Could not make canonical path for log file '{}'", path_file_log.c_str()); 82 | } // catch: 83 | } // init() }}} 84 | 85 | // write() {{{ 86 | template 87 | void write(char level, T&&... t) 88 | { 89 | std::stringstream ss; 90 | 91 | if constexpr ( sizeof...(t) > 0 ) 92 | { 93 | ( ss << ... << t ); 94 | } // if 95 | 96 | switch (level) 97 | { 98 | case 'i': 99 | for(std::string line; std::getline(ss, line);) 100 | { 101 | CLOG(INFO, "term") << line; 102 | CLOG(INFO, "default") << line; 103 | } 104 | break; 105 | case 'e': 106 | for(std::string line; std::getline(ss, line);) 107 | { 108 | CLOG(INFO, "term") << line; 109 | CLOG(INFO, "default") << line; 110 | }; break; 111 | case 'd': 112 | for(std::string line; std::getline(ss, line);) 113 | { 114 | CLOG(INFO, "default") << line; 115 | }; break; 116 | } // switch: level 117 | } // write() }}} 118 | 119 | // fn: exception {{{ 120 | inline void exception(auto&& fn) 121 | { 122 | if (auto expected = ns_exception::to_expected(fn); not expected) 123 | { 124 | write('e', expected.error()); 125 | } // if 126 | } // }}} 127 | 128 | // fn: ec {{{ 129 | template 130 | inline auto ec(F&& fn, Args&&... args) -> std::invoke_result_t 131 | { 132 | std::error_code ec; 133 | if constexpr ( std::same_as> ) 134 | { 135 | fn(std::forward(args)..., ec); 136 | if ( ec ) { ns_log::write('e', ec.message()); } // if 137 | } 138 | else 139 | { 140 | auto ret = fn(std::forward(args)..., ec); 141 | if ( ec ) { ns_log::write('e', ec.message()); } // if 142 | return ret; 143 | } // else 144 | } // }}} 145 | 146 | } // namespace ns_log 147 | 148 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 149 | -------------------------------------------------------------------------------- /src/lib/sha.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | /// @file : sha 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include // Include for SHA256 and SHA512 11 | #include // Include for SHA256 and SHA512 12 | #include // Include for StringSink and HashFilter 13 | #include // Include for HexEncoder 14 | 15 | #include "../common.hpp" 16 | #include "../macro.hpp" 17 | #include "../std/exception.hpp" 18 | 19 | namespace fs = std::filesystem; 20 | 21 | namespace ns_sha 22 | { 23 | 24 | enum class SHA_TYPE 25 | { 26 | SHA256, 27 | SHA512 28 | }; 29 | 30 | namespace 31 | { 32 | 33 | // check_sha_impl() {{{ 34 | inline bool check_sha_impl(fs::path path_file_src, fs::path path_file_sha, SHA_TYPE sha_type = SHA_TYPE::SHA256) 35 | { 36 | std::ifstream file_src(path_file_src, std::ifstream::binary); 37 | std::ifstream file_sha(path_file_sha, std::ifstream::in); 38 | 39 | if (!file_src.good()) { "Cannot open file '{}' "_throw(path_file_src); } 40 | if (!file_sha.good()) { "Cannot open file '{}' "_throw(path_file_sha); } 41 | 42 | ns_log::write('i', "Calculating SHA for: ", path_file_src); 43 | 44 | std::string sha_calculated; 45 | 46 | // Calculated SHA 47 | if ( sha_type == SHA_TYPE::SHA256 ) 48 | { 49 | CryptoPP::SHA256 hash; 50 | CryptoPP::FileSource(file_src, true, new CryptoPP::HashFilter(hash, new CryptoPP::HexEncoder(new CryptoPP::StringSink(sha_calculated)))); 51 | } // if 52 | else 53 | { 54 | CryptoPP::SHA512 hash; 55 | CryptoPP::FileSource(file_src, true, new CryptoPP::HashFilter(hash, new CryptoPP::HexEncoder(new CryptoPP::StringSink(sha_calculated)))); 56 | } // else 57 | 58 | // Reference SHA 59 | std::string sha_reference; 60 | std::getline(file_sha, sha_reference); 61 | if ( sha_reference.find(' ') != std::string::npos ) 62 | { 63 | sha_reference.erase(sha_reference.find(' ')); 64 | } // if 65 | 66 | // Normalize to uppercase 67 | sha_calculated = ns_string::to_upper(sha_calculated); 68 | sha_reference = ns_string::to_upper(sha_reference); 69 | 70 | ns_log::write('i', "SHA Calculated: ", sha_calculated); 71 | ns_log::write('i', "SHA Reference : ", sha_reference); 72 | 73 | return sha_calculated == sha_reference; 74 | } // check_sha_impl() }}} 75 | 76 | } // namespace 77 | 78 | // check_sha() {{{ 79 | inline bool check_sha(fs::path path_file_src, fs::path path_file_sha, SHA_TYPE sha_type = SHA_TYPE::SHA256) 80 | { 81 | auto expected_check = ns_exception::to_expected([&]{ return check_sha_impl(path_file_src, path_file_sha, sha_type); }); 82 | ereturn_if(not expected_check, expected_check.error(), false); 83 | return *expected_check; 84 | } // check_sha() }}} 85 | 86 | } // namespace ns_sha 87 | 88 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 89 | -------------------------------------------------------------------------------- /src/lib/zip.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : zip 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "../std/filesystem.hpp" 9 | 10 | #include 11 | 12 | namespace fs = std::filesystem; 13 | 14 | namespace ns_zip 15 | { 16 | 17 | // list_regular_files() {{{ 18 | inline std::vector list_regular_files(fs::path const& path_file_zip) 19 | { 20 | std::vector ret; 21 | 22 | libzippp::ZipArchive zf(path_file_zip); 23 | 24 | // Open zip file 25 | if ( not zf.open(libzippp::ZipArchive::ReadOnly) ) 26 | { 27 | "Failed to open zip file"_throw(path_file_zip); 28 | } // if 29 | 30 | std::vector entries = zf.getEntries(); 31 | 32 | for(auto it{entries.begin()}; it != entries.end(); ++it) 33 | { 34 | libzippp::ZipEntry entry = *it; 35 | 36 | if (entry.isDirectory()) { continue; } // if 37 | else { ret.push_back(entry.getName()); } // else 38 | } // for 39 | 40 | // Close file 41 | zf.close(); 42 | 43 | return ret; 44 | } // list_regular_files() }}} 45 | 46 | // extract() {{{ 47 | inline void extract(fs::path const& path_file_zip, fs::path const& path_dir_out) 48 | { 49 | libzippp::ZipArchive zf(path_file_zip); 50 | 51 | // Open zip file 52 | if ( not zf.open(libzippp::ZipArchive::ReadOnly) ) 53 | { 54 | "Failed to open zip file"_throw(path_file_zip); 55 | } // if 56 | 57 | ns_log::write('i', "Extracting ", path_file_zip); 58 | 59 | std::vector entries = zf.getEntries(); 60 | 61 | for(auto it{entries.begin()}; it != entries.end(); ++it) 62 | { 63 | libzippp::ZipEntry entry = *it; 64 | 65 | fs::path path_file_out = path_dir_out / entry.getName(); 66 | 67 | if (entry.isDirectory()) 68 | { 69 | // Create parent dirs 70 | ns_fs::ns_path::dir_create(path_file_out); 71 | } // if 72 | else 73 | { 74 | // Create parent dirs 75 | ns_fs::ns_path::dir_create(path_file_out.parent_path()); 76 | 77 | // Open file to write 78 | std::ofstream out_file(path_file_out, std::ios::binary); 79 | 80 | // Write to file from zip 81 | entry.readContent(out_file); 82 | 83 | // Close file 84 | out_file.close(); 85 | } // else 86 | 87 | } // for 88 | 89 | // Close file 90 | zf.close(); 91 | } // extract() }}} 92 | 93 | } // namespace ns_zip 94 | 95 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 96 | -------------------------------------------------------------------------------- /src/macro.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : macro 4 | /// 5 | 6 | #pragma once 7 | 8 | // Ec wrapper 9 | #define lec(fun, ...) \ 10 | ns_log::ec([](Args&&... args){ return fun(std::forward(args)...); }, __VA_ARGS__) 11 | 12 | // Throw 13 | #define qthrow_if(cond, msg) \ 14 | if (cond) { throw std::runtime_error(msg); } 15 | 16 | #define dthrow_if(cond, msg) \ 17 | if (cond) { ns_log::write('d', msg); throw std::runtime_error(msg); } 18 | 19 | #define ithrow_if(cond, msg) \ 20 | if (cond) { ns_log::write('i', msg); throw std::runtime_error(msg); } 21 | 22 | #define ethrow_if(cond, msg) \ 23 | if (cond) { ns_log::write('e', msg); throw std::runtime_error(msg); } 24 | 25 | // Exit 26 | #define qexit_if(cond, ret) \ 27 | if (cond) { exit(ret); } 28 | 29 | #define dexit_if(cond, msg, ret) \ 30 | if (cond) { ns_log::write('d', msg); exit(ret); } 31 | 32 | #define iexit_if(cond, msg, ret) \ 33 | if (cond) { ns_log::write('i', msg); exit(ret); } 34 | 35 | #define eexit_if(cond, msg, ret) \ 36 | if (cond) { ns_log::write('e', msg); exit(ret); } 37 | 38 | // Abort 39 | #define eabort_if(cond, msg) \ 40 | if ( (cond) ) { ns_log::write('e', msg); std::abort(); } 41 | 42 | // Return 43 | #define qreturn_if(cond, ...) \ 44 | if (cond) { return __VA_ARGS__; } 45 | 46 | #define dreturn_if(cond, msg, ...) \ 47 | if (cond) { ns_log::write('d', msg); return __VA_ARGS__; } 48 | 49 | #define ireturn_if(cond, msg, ...) \ 50 | if (cond) { ns_log::write('i', msg); return __VA_ARGS__; } 51 | 52 | #define ereturn_if(cond, msg, ...) \ 53 | if (cond) { ns_log::write('e', msg); return __VA_ARGS__; } 54 | 55 | // Break 56 | #define qbreak_if(cond) \ 57 | if ( (cond) ) { break; } 58 | 59 | #define ebreak_if(cond, msg) \ 60 | if ( (cond) ) { ns_log::write('e', msg); break; } 61 | 62 | #define ibreak_if(cond, msg) \ 63 | if ( (cond) ) { ns_log::write('i', msg); break; } 64 | 65 | #define dbreak_if(cond, msg) \ 66 | if ( (cond) ) { ns_log::write('d', msg); break; } 67 | 68 | // Continue 69 | #define qcontinue_if(cond) \ 70 | if ( (cond) ) { continue; } 71 | 72 | #define econtinue_if(cond, msg) \ 73 | if ( (cond) ) { ns_log::write('e', msg); continue; } 74 | 75 | #define icontinue_if(cond, msg) \ 76 | if ( (cond) ) { ns_log::write('i', msg); continue; } 77 | 78 | #define dcontinue_if(cond, msg) \ 79 | if ( (cond) ) { ns_log::write('d', msg); continue; } 80 | 81 | // Conditional log 82 | #define elog_if(cond, msg) \ 83 | if ( (cond) ) { ns_log::write('e', msg); } 84 | 85 | #define ilog_if(cond, msg) \ 86 | if ( (cond) ) { ns_log::write('i', msg); } 87 | 88 | #define dlog_if(cond, msg) \ 89 | if ( (cond) ) { ns_log::write('d', msg); } 90 | 91 | // Expected 92 | #define elog_unexpected(expr) \ 93 | if ( auto value = expr; not expr.has_value() ) { ns_log::write('e', expr.error()); } 94 | 95 | // Error log 96 | #define elogerror(expr) \ 97 | if ( auto error = expr ) { ns_log::write('e', *error); } 98 | 99 | #define ilogerror(expr) \ 100 | if ( auto error = expr ) { ns_log::write('i', *error); } 101 | 102 | #define dlogerror(expr) \ 103 | if ( auto error = expr ) { ns_log::write('d', *error); } 104 | -------------------------------------------------------------------------------- /src/std/concepts.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : concepts 4 | // @created : Tuesday Jan 23, 2024 19:59:31 -03 5 | /// 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace ns_concept 16 | { 17 | template 18 | concept Variant = requires(T){ std::variant_size_v; }; 19 | 20 | template 21 | concept Enum = std::is_enum_v; 22 | 23 | template 24 | concept Iterable = requires(T t) 25 | { 26 | { t.begin() } -> std::input_iterator; 27 | { t.end() } -> std::input_iterator; 28 | }; 29 | 30 | template 31 | concept IterableConst = requires(T t) 32 | { 33 | { t.cbegin() } -> std::input_iterator; 34 | { t.cend() } -> std::input_iterator; 35 | }; 36 | 37 | 38 | // Helper to check if a type is a specialization of std::vector 39 | template 40 | struct is_vector : std::false_type {}; 41 | 42 | template 43 | struct is_vector> : std::true_type {}; 44 | 45 | // Define a concept based on the helper trait 46 | template 47 | concept IsVector = is_vector::value; 48 | 49 | template 50 | concept IterableForward = std::forward_iterator; 51 | 52 | template 53 | concept StringConvertible = std::is_convertible_v, std::string>; 54 | 55 | template 56 | concept StringConstructible = std::constructible_from>; 57 | 58 | template 59 | concept Numeric = std::integral> 60 | or std::floating_point>; 61 | 62 | template 63 | concept StreamInsertable = requires(T t, std::ostream& os) 64 | { 65 | { os << t } -> std::same_as; 66 | }; 67 | 68 | template 69 | concept AsString = StringConvertible or StringConstructible or Numeric or StreamInsertable; 70 | 71 | } // namespace ns_concept 72 | 73 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 74 | -------------------------------------------------------------------------------- /src/std/copy.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : copy 4 | // @created : Friday Jan 19, 2024 19:36:20 -03 5 | /// 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "../common.hpp" 15 | 16 | 17 | namespace ns_copy 18 | { 19 | 20 | namespace fs = std::filesystem; 21 | 22 | // file() {{{ 23 | template > 24 | inline void file(fs::path const& path_src 25 | , fs::path const& path_dst 26 | , F&& f_progress_callback = [](auto&&,auto&&,auto&&){}) 27 | { 28 | std::ifstream file_src(path_src, std::ios::binary); 29 | std::ofstream file_dst(path_dst, std::ios::binary); 30 | 31 | if ( ! file_src.good() ) 32 | { 33 | "Could not open file for read: '{}'"_throw(path_src); 34 | } // if 35 | 36 | if ( ! file_dst.good() ) 37 | { 38 | "Could not open file for write: '{}'"_throw(path_dst); 39 | } // if 40 | 41 | // Calculate file size 42 | file_src.seekg(0, std::ios::end); 43 | size_t file_size = file_src.tellg(); 44 | file_src.seekg(0, std::ios::beg); 45 | 46 | // Create buffer 47 | size_t const size_buffer = 1024; 48 | char buffer[size_buffer]; 49 | 50 | // Keep track of total copied data 51 | size_t size_bytes_copied = 0; 52 | 53 | while (file_src.read(buffer, size_buffer) || file_src.gcount() != 0) 54 | { 55 | file_dst.write(buffer, file_src.gcount()); 56 | size_bytes_copied += file_src.gcount(); 57 | 58 | double progress = static_cast(size_bytes_copied) / file_size; 59 | 60 | // 100% handler after the loop 61 | if ( progress != 1.0 ) 62 | { 63 | f_progress_callback(progress, path_src, path_dst); 64 | } // if 65 | } // while 66 | 67 | // Ending callback 68 | f_progress_callback(1.0, path_src, path_dst); 69 | 70 | // Make file executable 71 | using std::filesystem::perms; 72 | fs::permissions(path_dst, perms::owner_all | perms::group_all | perms::others_read); 73 | } // file() }}} 74 | 75 | // callback_seconds {{{ 76 | template 77 | decltype(auto) callback_seconds(std::chrono::seconds seconds, F&& f) 78 | { 79 | return [=,time_ref = std::chrono::steady_clock::now()] (double percentage, fs::path src, fs::path dst) mutable 80 | { 81 | auto time_now = std::chrono::steady_clock::now(); 82 | if (std::chrono::duration_cast(time_now - time_ref) >= seconds or percentage == 1.0) 83 | { 84 | f(percentage, src, dst); 85 | time_ref = time_now; 86 | } // if 87 | }; 88 | } // function: callback_seconds }}} 89 | 90 | } // namespace ns_copy 91 | 92 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 93 | -------------------------------------------------------------------------------- /src/std/env.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : env 4 | /// 5 | 6 | #include 7 | #include 8 | 9 | #include "../common.hpp" 10 | 11 | #include "../std/filesystem.hpp" 12 | 13 | #pragma once 14 | 15 | // Environment variable handling {{{ 16 | namespace ns_env 17 | { 18 | 19 | namespace fs = std::filesystem; 20 | 21 | // enum class Replace {{{ 22 | enum class Replace 23 | { 24 | Y, 25 | N, 26 | }; // enum class Replace }}} 27 | 28 | // dir() {{{ 29 | // Fetches a directory path from an environment variable 30 | // Tries to create if not exists 31 | inline fs::path dir(const char* name) 32 | { 33 | // Get environment variable 34 | const char * value = std::getenv(name) ; 35 | // Check if variable exists 36 | if ( ! value ) 37 | { 38 | "Variable {} not set"_throw(name); 39 | } // if 40 | 41 | // Create if not exists 42 | fs::path path_env = ns_fs::ns_path::dir_exists(value)._ret; 43 | 44 | // Log 45 | // ns_log::write('d', "ns_env::dir ", name, " -> ", path_env); 46 | 47 | // Return validated directory path 48 | return path_env; 49 | } // dir() }}} 50 | 51 | // file() {{{ 52 | // Fetches a directory path from an environment variable 53 | // Tries to create if not exists 54 | inline fs::path file(const char* name) 55 | { 56 | // Get environment variable 57 | const char * value = std::getenv(name) ; 58 | 59 | // Check if variable exists 60 | if ( ! value ) 61 | { 62 | "Variable {} not set"_throw(name); 63 | } // if 64 | 65 | // Create if not exists 66 | fs::path path_env = ns_fs::ns_path::file_exists(value)._ret; 67 | 68 | // Log 69 | // ns_log::write('d', "ns_env::file ", name, " -> ", path_env); 70 | 71 | // Return validated directory path 72 | return path_env; 73 | } // file() }}} 74 | 75 | // set() {{{ 76 | // Sets an environment variable 77 | template 78 | void set(T&& name, U&& value, Replace replace) 79 | { 80 | // ns_log::write('d', "ns_env::set ", name, " -> ", value); 81 | setenv(ns_string::to_string(name).c_str(), ns_string::to_string(value).c_str(), (replace == Replace::Y)); 82 | } // set() }}} 83 | 84 | // concat() {{{ 85 | // Appends 'extra' to an environment variable 'name' 86 | inline void concat(const char* name, std::string const& extra) 87 | { 88 | // Append to var 89 | if ( const char* var_curr = std::getenv(name); var_curr ) 90 | { 91 | // ns_log::write('d', "ns_env::concat ", name, " -> ", extra); 92 | setenv(name, std::string{var_curr + extra}.c_str(), 1); 93 | } // if 94 | } // concat() }}} 95 | 96 | // get() {{{ 97 | // Get an env variable if exists 98 | inline const char* get(const char* name) 99 | { 100 | const char* ret = std::getenv(name); 101 | 102 | // ns_log::write('d', "ns_env::get ", name, " -> ", ret); 103 | 104 | return ret; 105 | } // get() }}} 106 | 107 | // get_or_throw() {{{ 108 | // Get an env variable 109 | template 110 | inline T get_or_throw(const char* name) 111 | { 112 | const char* value = std::getenv(name); 113 | 114 | if (not value) 115 | { 116 | throw std::runtime_error("Variable '{}' is undefined"_fmt(name)); 117 | } // if 118 | 119 | return value; 120 | } // get_or_throw() }}} 121 | 122 | // get_or_else() {{{ 123 | // Get an env variable 124 | inline std::string get_or_else(std::string_view name, std::string_view alternative) 125 | { 126 | const char* value = std::getenv(name.data()); 127 | return (value != nullptr)? value : alternative.data(); 128 | } // get_or_else() }}} 129 | 130 | } // namespace ns_env }}} 131 | 132 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 133 | -------------------------------------------------------------------------------- /src/std/exception.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : exception 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace std 13 | { 14 | 15 | template 16 | using error = optional; 17 | 18 | } // namespace std 19 | 20 | namespace ns_exception 21 | { 22 | 23 | template 24 | void ignore(F&& f) 25 | { 26 | try { f(); } catch (...) {} 27 | } // function: ignore 28 | 29 | template 30 | requires std::is_default_constructible_v> 31 | auto or_default(F&& f) -> std::invoke_result_t 32 | { 33 | try { return f(); } catch (...) { return std::invoke_result_t{}; } 34 | } // function: or_default 35 | 36 | template 37 | requires std::is_default_constructible_v 38 | and std::is_convertible_v> 39 | T or_else(F&& f, T&& t) 40 | { 41 | try { return f(); } catch (...) { return t; } 42 | } // function: or_else 43 | 44 | template 45 | requires std::same_as,std::invoke_result_t> 46 | decltype(auto) or_else(F&& f, G&& g) 47 | { 48 | if constexpr ( std::is_void_v> ) 49 | { 50 | try { f(); } catch (...) { g(); } 51 | } // if 52 | else 53 | { 54 | try { return f(); } catch (...) { return g(); } 55 | } // else 56 | } // function: or_else 57 | 58 | template 59 | auto to_optional(F&& f) -> std::optional> 60 | { 61 | try { return std::make_optional(f()); } catch (...) { return std::nullopt; } 62 | } // function: to_optional 63 | 64 | template 65 | inline std::error to_error(F&& f) 66 | { 67 | try { f(); return std::nullopt; } catch (std::exception const& e) { return std::make_optional(e.what()); } 68 | } // function: to_optional 69 | 70 | template 71 | requires (not std::is_void_v>) 72 | auto to_expected(F&& f) -> std::expected, std::string> 73 | { 74 | try { return f(); } catch (std::exception const& e) { return std::unexpected(e.what()); } 75 | } // function: to_optional 76 | 77 | template 78 | requires std::is_invocable_v 79 | and std::is_void_v> 80 | auto to_expected(F&& f, Args&&... args) -> std::expected 81 | { 82 | try 83 | { 84 | f(std::forward(args)...); 85 | return std::true_type{}; 86 | } 87 | catch (std::exception const& e) 88 | { 89 | return std::unexpected(std::string{e.what()}); 90 | } 91 | catch (...) 92 | { 93 | return std::unexpected(std::string{"Exception does not inherit from std::exception"}); 94 | } 95 | } // function: to_optional 96 | 97 | template 98 | requires std::is_invocable_v 99 | and (not std::is_void_v>) 100 | auto to_expected(F&& f, Args&&... args) -> std::expected, std::string> 101 | { 102 | try 103 | { 104 | return f(std::forward(args)...); 105 | } 106 | catch (std::exception const& e) 107 | { 108 | return std::unexpected(std::string{e.what()}); 109 | } 110 | catch (...) 111 | { 112 | return std::unexpected(std::string{"Exception does not inherit from std::exception"}); 113 | } 114 | } // function: to_optional 115 | 116 | } // namespace ns_exception 117 | 118 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 119 | -------------------------------------------------------------------------------- /src/std/fifo.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : fifo 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "../std/filesystem.hpp" 16 | 17 | #include "../common.hpp" 18 | 19 | namespace ns_fifo 20 | { 21 | 22 | namespace fs = std::filesystem; 23 | 24 | // class Fifo {{{ 25 | class Fifo 26 | { 27 | private: 28 | int m_fd; 29 | fs::path m_path_file_fifo; 30 | 31 | public: 32 | template 33 | Fifo(T&& t); 34 | ~Fifo(); 35 | template 36 | void push(T&& t); 37 | }; // class: Fifo }}} 38 | 39 | // Fifo::Fifo {{{ 40 | template 41 | Fifo::Fifo(T&& t) 42 | { 43 | m_path_file_fifo = ns_string::to_string(t); 44 | auto cstr_path_file_fifo = m_path_file_fifo.c_str(); 45 | 46 | // Remove the FIFO if it already exists 47 | if (struct stat buf; stat(cstr_path_file_fifo, &buf) == 0 && S_ISFIFO(buf.st_mode) ) 48 | { 49 | unlink(cstr_path_file_fifo); 50 | } // if 51 | 52 | // Create upper dirs 53 | if ( not ns_fs::ns_path::dir_create(fs::path(cstr_path_file_fifo).parent_path())._bool ) 54 | { 55 | "Failed to create upper directories for '{}'"_throw(cstr_path_file_fifo); 56 | } // if 57 | 58 | // Create a new FIFO 59 | if (mkfifo(cstr_path_file_fifo, 0666) != 0) 60 | { 61 | perror("mkfifo"); 62 | "mkfifo failed for '{}'"_throw(cstr_path_file_fifo); 63 | } // if 64 | 65 | // Open fifo as writer 66 | if (m_fd = open(cstr_path_file_fifo, O_WRONLY | O_NONBLOCK); m_fd == -1 ) 67 | { 68 | "Could not open fifo file descriptor for writing"_throw(); 69 | return; 70 | } // if 71 | } // }}} 72 | 73 | // Fifo::~Fifo {{{ 74 | inline Fifo::~Fifo() 75 | { 76 | close(m_fd); 77 | fs::remove(m_path_file_fifo); 78 | } // }}} 79 | 80 | // push() {{{ 81 | template 82 | void Fifo::push(T&& t) 83 | { 84 | auto data = ns_string::to_string(t); 85 | auto count_bytes_written = write(m_fd, data.c_str(), data.size()); 86 | if ( count_bytes_written == -1 ) 87 | { 88 | ns_log::write('e', "Failed to write data '", data, "' to fifo"); 89 | } // if 90 | } // push() }}} 91 | 92 | } // namespace ns_fifo 93 | 94 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 95 | -------------------------------------------------------------------------------- /src/std/string.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : string 4 | /// 5 | 6 | #pragma once 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "concepts.hpp" 16 | 17 | namespace ns_string 18 | { 19 | 20 | // replace_substrings() {{{ 21 | inline std::string replace_substrings(std::string string 22 | , std::string const& substring 23 | , std::string const& replacement) 24 | { 25 | boost::algorithm::replace_all(string, substring, replacement); 26 | return string; 27 | } // replace_substrings() }}} 28 | 29 | // to_string() {{{ 30 | template 31 | inline std::string to_string(T&& t) noexcept 32 | { 33 | if constexpr ( ns_concept::StringConvertible ) 34 | { 35 | return t; 36 | } // if 37 | else if constexpr ( ns_concept::StringConstructible ) 38 | { 39 | return std::string{t}; 40 | } // else if 41 | else if constexpr ( ns_concept::Numeric ) 42 | { 43 | return std::to_string(t); 44 | } // else if 45 | else 46 | { 47 | std::stringstream ss; 48 | ss << t; 49 | return ss.str(); 50 | } // else if 51 | } // to_string() }}} 52 | 53 | // to_lower() {{{ 54 | template 55 | std::string to_lower(T&& t) 56 | { 57 | std::string ret = to_string(t); 58 | std::ranges::for_each(ret, [](auto& c){ c = std::tolower(c); }); 59 | return ret; 60 | } // to_lower() }}} 61 | 62 | // to_upper() {{{ 63 | template 64 | std::string to_upper(T&& t) 65 | { 66 | std::string ret = to_string(t); 67 | std::ranges::for_each(ret, [](auto& c){ c = std::toupper(c); }); 68 | return ret; 69 | } // to_upper() }}} 70 | 71 | // from_container() {{{ 72 | template::value_type 74 | , typename F = std::function> 75 | std::string from_container(R&& r, char sep = ',', F f = [](V&& e) -> std::string { return e; }) 76 | { 77 | std::stringstream ret; 78 | for( auto it = r.begin(); it != r.end(); ++it ) 79 | { 80 | ret << f(*it); 81 | if ( std::next(it) != r.end() ) { ret << sep; } 82 | } // if 83 | return ret.str(); 84 | } // from_container() }}} 85 | 86 | // split() {{{ 87 | template 88 | std::vector split(T&& t, char delim = ' ') 89 | { 90 | std::vector out; 91 | 92 | std::string base = to_string(t); 93 | for (auto&& i : std::views::split(base, delim)) 94 | { 95 | auto substring = std::string(i.begin(), i.end()); 96 | if ( substring.empty() ) { continue; } 97 | out.push_back(substring); 98 | } // for 99 | 100 | return out; 101 | } // split() }}} 102 | 103 | } // namespace ns_string 104 | 105 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 106 | -------------------------------------------------------------------------------- /src/std/vector.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | // @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 3 | // @file : vector 4 | /// 5 | 6 | #pragma once 7 | 8 | #include "concepts.hpp" 9 | #include "string.hpp" 10 | 11 | namespace ns_vector 12 | { 13 | 14 | template 15 | T pop_back(V& vec) 16 | { 17 | T value = vec.front(); 18 | vec.erase(vec.begin()); 19 | return value; 20 | } 21 | 22 | template> 23 | inline R from_string(ns_concept::AsString auto&& s 24 | , char delimiter 25 | , std::function f = [](auto&& e){ return e; }) 26 | { 27 | R tokens; 28 | std::string token; 29 | std::istringstream stream_token(ns_string::to_string(s)); 30 | 31 | while (std::getline(stream_token, token, delimiter)) 32 | { 33 | tokens.push_back(f(token)); 34 | } // while 35 | 36 | return tokens; 37 | } // from_string 38 | 39 | 40 | } // namespace ns_vector 41 | 42 | /* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/ 43 | -------------------------------------------------------------------------------- /thumbnailer/appimage.thumbnailer: -------------------------------------------------------------------------------- 1 | [Thumbnailer Entry] 2 | Version=1.0 3 | Encoding=UTF-8 4 | Type=X-Thumbnailer 5 | Name=Appimage Thumbnailer 6 | MimeType=application/vnd.appimage; 7 | Exec=/usr/bin/thumbnailer-appimage %s %i %o 8 | -------------------------------------------------------------------------------- /thumbnailer/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ###################################################################### 4 | # @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 5 | # @file : install 6 | # @created : Monday Sep 12, 2022 22:02:01 -03 7 | ###################################################################### 8 | 9 | set -e 10 | 11 | # Install required packages 12 | if command -v apt; then 13 | sudo apt install -y tumbler squashfs-tools thunar 14 | elif command -v pacman; then 15 | sudo pacman -S --noconfirm tumbler squashfs-tools thunar 16 | else 17 | echo "Unsupported package manager, please install the packages manually" 18 | exit 1 19 | fi 20 | 21 | # Copy thumbnail generator 22 | sudo curl --output /usr/bin/thumbnailer-appimage https://gitlab.com/formigoni/gameimage/-/raw/master/thumbnailer/thumbnailer-appimage 23 | sudo chmod +x /usr/bin/thumbnailer-appimage 24 | 25 | # Copy tumbler entry 26 | mkdir -p ~/.local/share/thumbnailers 27 | curl --output ~/.local/share/thumbnailers/appimage.thumbnailer \ 28 | https://gitlab.com/formigoni/gameimage/-/raw/master/thumbnailer/appimage.thumbnailer 29 | 30 | # Remove 2GB limit on thumbnails 31 | mkdir -p ~/.config/tumbler 32 | cp /etc/xdg/tumbler/tumbler.rc ~/.config/tumbler/ 33 | awk -i inplace '/\[DesktopThumbnailer\]/,/MaxFileSize=(.*)/ { sub("MaxFileSize=.*", "MaxFileSize="); } 1' ~/.config/tumbler/tumbler.rc 34 | -------------------------------------------------------------------------------- /thumbnailer/thumbnailer-appimage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ###################################################################### 4 | # @author : Ruan E. Formigoni (ruanformigoni@gmail.com) 5 | # @file : test 6 | # @created : Wednesday Sep 14, 2022 19:59:11 -03 7 | ###################################################################### 8 | 9 | set -e 10 | 11 | # Create tmp dir to extract icon 12 | tmpdir="$(mktemp -d)" 13 | 14 | # Remove tmp dir in case of success or failure 15 | function clean() 16 | { 17 | rm -rf "$tmpdir" 18 | } 19 | 20 | trap clean EXIT ERR SIGINT 21 | 22 | cd "$tmpdir" 23 | 24 | # Shortcut for list files command 25 | cmd_list="unsquashfs -o $("$2" --appimage-offset 2>/dev/null) -ll \"$2\" " 26 | 27 | # Get icon path inside appimage 28 | thumbnail="$(eval "$cmd_list" | grep .DirIcon)" 29 | 30 | # Check if is symlink 31 | if [[ "$thumbnail" =~ -\> ]]; then 32 | thumbnail="$(echo "$thumbnail" | awk -F" -> " '{print $2}')" 33 | else 34 | thumbnail=".DirIcon" 35 | fi 36 | 37 | # Resolve symlinks (option -L in unsquashfs does not work for this) 38 | while eval "$cmd_list $thumbnail" | grep "$thumbnail -> "; do 39 | thumbnail=$(eval "$cmd_list" | pcregrep -o1 "$thumbnail -> (.*)") 40 | # Some appimages use local apppimage paths, e.g., ./usr/applications/... 41 | thumbnail="${thumbnail#./}" 42 | done 43 | 44 | # Extract icon 45 | unsquashfs -o "$("$2" --appimage-offset 2>/dev/null)" "$2" "$thumbnail" 2>&1 46 | 47 | # Update icon path to extracted path 48 | thumbnail="./squashfs-root/${thumbnail}" 49 | 50 | if [ -f "$thumbnail" ]; then 51 | if ! convert -thumbnail "$1" "./$thumbnail" "$3"; then 52 | gdbus call --session \ 53 | --dest=org.freedesktop.thumbnails.Cache1 \ 54 | --object-path /org/freedesktop/thumbnails/Cache1 \ 55 | --method org.freedesktop.thumbnails.Cache1.Delete "['$4']" >/dev/null 56 | fi 57 | fi 58 | --------------------------------------------------------------------------------