├── rootfs ├── etc │ ├── vconsole.conf │ ├── locale.conf │ ├── locale.gen │ ├── lightdm │ │ ├── lightdm.conf.d │ │ │ └── 10-kazeta-session.conf │ │ └── lightdm.conf │ ├── mkinitcpio.conf │ └── pacman.conf └── usr │ ├── bin │ ├── kazeta-bios │ ├── ethernet-connect │ ├── kazeta-mount │ ├── kazeta-session │ └── kazeta │ └── share │ ├── kazeta │ └── runtimes │ │ └── none.kzr │ ├── wayland-sessions │ └── kazeta.desktop │ └── polkit-1 │ └── rules.d │ ├── 40-kazeta.rules │ └── 40-system-tweaks.rules ├── bios ├── back.wav ├── logo.png ├── move.wav ├── reject.wav ├── select.wav ├── november.ttf ├── background.png ├── placeholder.png ├── Cargo.toml ├── src │ ├── save.rs │ └── main.rs └── Cargo.lock ├── aur-pkgs ├── build-aur-packages.sh └── build-aur-package.sh ├── .github └── workflows │ ├── build-builder.yml │ ├── main.yml │ └── build-system-image.yml ├── LICENSE ├── Dockerfile └── manifest /rootfs/etc/vconsole.conf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rootfs/etc/locale.conf: -------------------------------------------------------------------------------- 1 | LANG=en_US.UTF-8 -------------------------------------------------------------------------------- /rootfs/etc/locale.gen: -------------------------------------------------------------------------------- 1 | en_US.UTF-8 UTF-8 2 | -------------------------------------------------------------------------------- /bios/back.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/back.wav -------------------------------------------------------------------------------- /bios/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/logo.png -------------------------------------------------------------------------------- /bios/move.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/move.wav -------------------------------------------------------------------------------- /bios/reject.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/reject.wav -------------------------------------------------------------------------------- /bios/select.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/select.wav -------------------------------------------------------------------------------- /bios/november.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/november.ttf -------------------------------------------------------------------------------- /bios/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/background.png -------------------------------------------------------------------------------- /bios/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/bios/placeholder.png -------------------------------------------------------------------------------- /rootfs/etc/lightdm/lightdm.conf.d/10-kazeta-session.conf: -------------------------------------------------------------------------------- 1 | [Seat:*] 2 | autologin-session=kazeta 3 | -------------------------------------------------------------------------------- /rootfs/usr/bin/kazeta-bios: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/rootfs/usr/bin/kazeta-bios -------------------------------------------------------------------------------- /rootfs/usr/share/kazeta/runtimes/none.kzr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kazetaos/kazeta/HEAD/rootfs/usr/share/kazeta/runtimes/none.kzr -------------------------------------------------------------------------------- /rootfs/usr/share/wayland-sessions/kazeta.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Name=kazeta 4 | Comment=kazeta session 5 | Exec=/usr/bin/kazeta-session 6 | Type=Application 7 | -------------------------------------------------------------------------------- /rootfs/etc/mkinitcpio.conf: -------------------------------------------------------------------------------- 1 | # vim:set ft=sh 2 | 3 | MODULES=(dm_mod ext4 sha256 sha512 overlay) 4 | BINARIES=() 5 | FILES=() 6 | HOOKS=(microcode systemd modconf kms keyboard sd-vconsole block filesystems) 7 | COMPRESSION="xz" 8 | COMPRESSION_OPTIONS=(-v -9e) 9 | -------------------------------------------------------------------------------- /rootfs/etc/lightdm/lightdm.conf: -------------------------------------------------------------------------------- 1 | # Basic configuration for seat. The session should be filled in into a 2 | # file under /etc/lightdm/lightdm.conf.d/ 3 | [LightDM] 4 | run-directory=/run/lightdm 5 | logind-check-graphical=true 6 | [Seat:*] 7 | session-wrapper=/etc/lightdm/Xsession 8 | 9 | -------------------------------------------------------------------------------- /rootfs/usr/share/polkit-1/rules.d/40-kazeta.rules: -------------------------------------------------------------------------------- 1 | polkit.addRule(function(action, subject) { 2 | if (action.id == "org.freedesktop.policykit.exec" && subject.isInGroup("wheel") && action.lookup("program") == "/usr/bin/kazeta-mount") { 3 | return polkit.Result.YES; 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /aur-pkgs/build-aur-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | source manifest; 7 | 8 | sudo mkdir -p /workdir/aur-pkgs 9 | sudo chown build:build /workdir/aur-pkgs 10 | 11 | PIKAUR_CMD="PKGDEST=/workdir/aur-pkgs pikaur --noconfirm -Sw ${AUR_PACKAGES}" 12 | PIKAUR_RUN=(bash -c "${PIKAUR_CMD}") 13 | "${PIKAUR_RUN[@]}" -------------------------------------------------------------------------------- /bios/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kazeta-bios" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | macroquad = { version = "0.4.7", features = ["audio"] } 8 | gilrs = "0.10.6" 9 | futures = "0.3.30" 10 | dirs = "5.0.1" 11 | whoami = "1.4.1" 12 | sysinfo = "0.30.5" 13 | tar = "0.4.40" 14 | walkdir = "2.4.0" 15 | chrono = { version = "0.4", features = ["serde"] } 16 | -------------------------------------------------------------------------------- /rootfs/usr/share/polkit-1/rules.d/40-system-tweaks.rules: -------------------------------------------------------------------------------- 1 | polkit.addRule(function(action, subject) { 2 | if ((action.id == "org.freedesktop.timedate1.set-time" || 3 | action.id == "org.freedesktop.timedate1.set-timezone" || 4 | action.id == "org.freedesktop.login1.power-off" || 5 | action.id == "org.freedesktop.login1.reboot") && 6 | subject.isInGroup("wheel")) { 7 | return polkit.Result.YES; 8 | } 9 | }); -------------------------------------------------------------------------------- /rootfs/usr/bin/ethernet-connect: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ $EUID -ne 0 ]; then 4 | echo "$(basename $0) must be run as root" 5 | exit 1 6 | fi 7 | 8 | interface=$(ip addr | grep ": en" | head -1 | cut -d':' -f2 | tr -d ' ') 9 | 10 | echo "\ 11 | [Match] 12 | Name=${interface} 13 | [Network] 14 | DHCP=yes\ 15 | " > /etc/systemd/network/wired.network 16 | 17 | systemctl start systemd-networkd 18 | systemctl start systemd-resolved 19 | sleep 5 20 | -------------------------------------------------------------------------------- /aur-pkgs/build-aur-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | source manifest; 7 | 8 | sudo mkdir -p /temp/package 9 | sudo chown build:build /temp/package 10 | sudo chown build:build /workdir/aur-pkgs 11 | 12 | git clone --depth=1 https://aur.archlinux.org/${1}.git /temp/package 13 | 14 | PIKAUR_CMD="PKGDEST=/workdir/aur-pkgs pikaur --noconfirm --build-gpgdir /etc/pacman.d/gnupg -S -P /temp/package/PKGBUILD" 15 | PIKAUR_RUN=(bash -c "${PIKAUR_CMD}") 16 | "${PIKAUR_RUN[@]}" 17 | # if aur package is not successfully built, exit 18 | if [ $? -ne 0 ]; then 19 | echo "Build failed. Stopping..." 20 | exit -1 21 | fi 22 | # remove any epoch (:) in name, replace with -- since not allowed in artifacts 23 | find /workdir/aur-pkgs/*.pkg.tar* -type f -name '*:*' -execdir bash -c 'mv "$1" "${1//:/--}"' bash {} \; -------------------------------------------------------------------------------- /rootfs/usr/bin/kazeta-mount: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ $EUID -ne 0 ]; then 4 | echo "$(basename $0) must be run as root" 5 | exit 1 6 | fi 7 | 8 | if [ "$1" == "kzp" ]; then 9 | if [ "$2" == "--unmount" ]; then 10 | umount "$3" 11 | else 12 | mount "$2" "$3" 13 | fi 14 | 15 | exit 0 16 | fi 17 | 18 | if [ "$1" == "--unmount" ]; then 19 | umount "$2" 20 | umount "$3" 21 | else 22 | lowerdir="$1" 23 | upperdir="$2" 24 | workdir="$3" 25 | targetdir="$4" 26 | runtime="$5" 27 | runtimedir="$6" 28 | 29 | rm -rf "$workdir" 30 | rm -rf "$targetdir" 31 | rm -rf "$runtimedir" 32 | mkdir -p "$workdir" 33 | mkdir -p "$targetdir" 34 | mkdir -p "$runtimedir" 35 | 36 | mount "$runtime" "$runtimedir" 37 | 38 | # metacopy=on reduces the pressure vessel writes 39 | mount -t overlay overlay -o metacopy=on,lowerdir="$lowerdir:$runtimedir",upperdir="$upperdir",workdir="$workdir" "$targetdir" 40 | fi 41 | -------------------------------------------------------------------------------- /.github/workflows/build-builder.yml: -------------------------------------------------------------------------------- 1 | name: Build docker container 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }} 6 | 7 | on: 8 | workflow_dispatch: 9 | workflow_call: 10 | 11 | jobs: 12 | build: 13 | name: Build base docker image 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Login to GitHub Container Registry 18 | uses: docker/login-action@v3 19 | with: 20 | registry: ${{ env.REGISTRY }} 21 | username: ${{ github.actor }} 22 | password: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Extract metadata (tags, labels) for Docker 24 | id: meta 25 | uses: docker/metadata-action@v5 26 | with: 27 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 28 | - name: Build and push Docker image 29 | uses: docker/build-push-action@v6 30 | with: 31 | context: . 32 | push: true 33 | tags: ${{ steps.meta.outputs.tags }} 34 | labels: ${{ steps.meta.outputs.labels }} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Alesh Slovak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rootfs/usr/bin/kazeta-session: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # ensure inputplumber manages all controllers 4 | for i in $(seq 1 100); do 5 | inputplumber devices manage-all --enable 6 | 7 | if [ "$?" == "0" ]; then 8 | break 9 | fi 10 | 11 | sleep 0.1 12 | done 13 | 14 | # hack to fix bios app audio 15 | wpctl status > /dev/null 16 | 17 | # start session 18 | mv /var/kazeta/session.log /var/kazeta/session.log.old 19 | /usr/bin/kazeta > /var/kazeta/session.log 2>&1 & 20 | 21 | # default to using HDMI audio output and set a reasonable volume 22 | if [ ! -e /var/kazeta/state/wireplumber ]; then 23 | mkdir -p /var/kazeta/state/wireplumber 24 | 25 | for i in $(seq 1 60); do 26 | sink_id=$(wpctl status --name | grep output | grep hdmi | head -1 | grep -oE " [0-9]+\. " | tr -d ' .') 27 | wpctl set-default $sink_id 28 | 29 | if [ "$?" == "0" ]; then 30 | wpctl set-volume $sink_id 0.8 31 | break 32 | fi 33 | 34 | sleep 1 35 | done 36 | fi 37 | 38 | wait 39 | 40 | POWER_OFF=1 41 | RESTART_SESSION_SENTINEL=/var/kazeta/state/.RESTART_SESSION_SENTINEL 42 | if [ -f $RESTART_SESSION_SENTINEL ]; then 43 | rm -f $RESTART_SESSION_SENTINEL 44 | POWER_OFF=0 45 | fi 46 | 47 | if [ "$POWER_OFF" == "1" ]; then 48 | poweroff 49 | fi 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: System image build 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build-docker-image: 15 | name: Build and publish docker container 16 | uses: ./.github/workflows/build-builder.yml 17 | 18 | list-pkgbuilds: 19 | name: List Packages 20 | runs-on: ubuntu-latest 21 | outputs: 22 | aur-pkgs: ${{ steps.set-aur-pkgs.outputs.matrix }} 23 | pkgs: ${{ steps.set-pkgs.outputs.matrix }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - id: set-aur-pkgs 27 | run: source ./manifest ; echo "matrix=$(echo ${AUR_PACKAGES} | jq -R -s -c 'split(" ")')" >> $GITHUB_OUTPUT 28 | shell: bash 29 | - id: set-pkgs 30 | run: echo "matrix=$(ls -d pkgs/*/ | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT 31 | shell: bash 32 | 33 | aur-pkgbuild: 34 | needs: 35 | - build-docker-image 36 | - list-pkgbuilds 37 | name: Build AUR package 38 | runs-on: ubuntu-latest 39 | strategy: 40 | fail-fast: true 41 | matrix: 42 | package: ${{ fromJson(needs.list-pkgbuilds.outputs.aur-pkgs) }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Extract metadata (tags, labels) for Docker 46 | id: meta 47 | uses: docker/metadata-action@v5 48 | with: 49 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 50 | - name: Build packages 51 | run: | 52 | docker pull ${{ steps.meta.outputs.tags }} 53 | docker run --rm -v $(pwd):/workdir --entrypoint=/workdir/aur-pkgs/build-aur-package.sh ${{ steps.meta.outputs.tags }} ${{ matrix.package }} 54 | - name: Upload Package Archives 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: AUR-packages-${{ matrix.package }} 58 | path: aur-pkgs/*.pkg.tar* 59 | 60 | build: 61 | needs: 62 | - build-docker-image 63 | - aur-pkgbuild 64 | name: Build OS UNSTABLE image 65 | uses: ./.github/workflows/build-system-image.yml 66 | with: 67 | postfix: "[UNSTABLE]" 68 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM greyltc/archlinux-aur:latest 2 | LABEL contributor="shadowapex@gmail.com" 3 | COPY rootfs/etc/pacman.conf /etc/pacman.conf 4 | RUN echo -e "keyserver-options auto-key-retrieve" >> /etc/pacman.d/gnupg/gpg.conf && \ 5 | # Cannot check space in chroot 6 | sed -i '/CheckSpace/s/^/#/g' /etc/pacman.conf && \ 7 | pacman-key --init && \ 8 | pacman --noconfirm -Syyuu && \ 9 | pacman --noconfirm -S \ 10 | arch-install-scripts \ 11 | btrfs-progs \ 12 | fmt \ 13 | xcb-util-wm \ 14 | wget \ 15 | pyalpm \ 16 | python \ 17 | python-build \ 18 | python-flit-core \ 19 | python-installer \ 20 | python-hatchling \ 21 | python-markdown-it-py \ 22 | python-setuptools \ 23 | python-wheel \ 24 | sudo \ 25 | && \ 26 | pacman --noconfirm -S --needed git && \ 27 | echo "%wheel ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ 28 | useradd build -G wheel -m && \ 29 | su - build -c "git clone https://aur.archlinux.org/pikaur.git /tmp/pikaur" && \ 30 | su - build -c "cd /tmp/pikaur && makepkg -f" && \ 31 | pacman --noconfirm -U /tmp/pikaur/pikaur-*.pkg.tar.zst 32 | 33 | # Auto add PGP keys for users 34 | RUN mkdir -p /etc/gnupg/ && echo -e "keyserver-options auto-key-retrieve" >> /etc/gnupg/gpg.conf 35 | 36 | # Add a fake systemd-run script to workaround pikaur requirement. 37 | RUN echo -e "#!/bin/bash\nif [[ \"$1\" == \"--version\" ]]; then echo 'fake 244 version'; fi\nmkdir -p /var/cache/pikaur\n" >> /usr/bin/systemd-run && \ 38 | chmod +x /usr/bin/systemd-run 39 | 40 | # substitute check with !check to avoid running software from AUR in the build machine 41 | # also remove creation of debug packages. 42 | RUN sed -i '/BUILDENV/s/check/!check/g' /etc/makepkg.conf && \ 43 | sed -i '/OPTIONS/s/debug/!debug/g' /etc/makepkg.conf 44 | 45 | COPY manifest /manifest 46 | # Freeze packages and overwrite with overrides when needed 47 | RUN source /manifest && \ 48 | echo "Server=https://archive.archlinux.org/repos/${ARCHIVE_DATE}/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist && \ 49 | pacman --noconfirm -Syyuu; if [ -n "${PACKAGE_OVERRIDES}" ]; then wget --directory-prefix=/tmp/extra_pkgs ${PACKAGE_OVERRIDES}; pacman --noconfirm -U --overwrite '*' /tmp/extra_pkgs/*; rm -rf /tmp/extra_pkgs; fi 50 | 51 | USER build 52 | ENV BUILD_USER "build" 53 | ENV GNUPGHOME "/etc/pacman.d/gnupg" 54 | # Built image will be moved here. This should be a host mount to get the output. 55 | ENV OUTPUT_DIR /output 56 | 57 | WORKDIR /workdir 58 | -------------------------------------------------------------------------------- /.github/workflows/build-system-image.yml: -------------------------------------------------------------------------------- 1 | name: Build OS image 2 | env: 3 | REGISTRY: ghcr.io 4 | IMAGE_NAME: ${{ github.repository }} 5 | 6 | on: 7 | workflow_call: 8 | inputs: 9 | postfix: 10 | type: string 11 | description: Postfix used in release. 12 | default: '' 13 | 14 | jobs: 15 | build-system-image: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | outputs: 20 | version: ${{ steps.build_image.outputs.version }} 21 | display_name: ${{ steps.build_image.outputs.display_name }} 22 | display_version: ${{ steps.build_image.outputs.display_version }} 23 | image_filename: ${{ steps.build_image.outputs.image_filename }} 24 | steps: 25 | - name: Maximize build space 26 | run: | 27 | df -h 28 | sudo rm -rf /usr/share/dotnet 29 | sudo rm -rf /usr/share/swift 30 | sudo rm -rf /usr/share/java 31 | sudo rm -rf /usr/local/lib/android 32 | sudo rm -rf /opt/ghc 33 | sudo rm -rf /opt/hostedtoolcache 34 | sudo rm -rf /opt/az 35 | df -h 36 | - uses: actions/checkout@v4 37 | - name: Extract metadata (tags, labels) for Docker 38 | id: meta 39 | uses: docker/metadata-action@v5 40 | with: 41 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 42 | - uses: actions/download-artifact@v4 43 | with: 44 | pattern: AUR-packages* 45 | merge-multiple: true 46 | path: aur-pkgs/ 47 | - name: Build system image 48 | id: build_image 49 | run: | 50 | mkdir output 51 | docker pull ${{ steps.meta.outputs.tags }} 52 | docker run -u root --rm --entrypoint=/workdir/build-image.sh -v $(pwd):/workdir -v $(pwd)/output:/output -v $GITHUB_OUTPUT:$GITHUB_OUTPUT -e "GITHUB_OUTPUT=$GITHUB_OUTPUT" --privileged=true ${{ steps.meta.outputs.tags }} $(echo ${GITHUB_SHA} | cut -c1-7) 53 | echo -e "$(docker inspect --format='{{index .RepoDigests 0}}' ${{ steps.meta.outputs.tags }})" > output/container.txt 54 | - name: Create release 55 | id: create_release 56 | uses: softprops/action-gh-release@v2 57 | with: 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | tag_name: ${{ steps.build_image.outputs.version }} 60 | target_commitish: ${{ github.sha }} 61 | name: ${{ steps.build_image.outputs.display_name }} ${{ steps.build_image.outputs.display_version }} ${{ inputs.postfix }} 62 | draft: false 63 | prerelease: true 64 | fail_on_unmatched_files: true 65 | files: | 66 | output/${{ steps.build_image.outputs.image_filename }} 67 | output/build_info.txt 68 | output/sha256sum.txt 69 | output/container.txt -------------------------------------------------------------------------------- /manifest: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | export VERSION="2025-0" 4 | export SYSTEM_DESC="Kazeta" 5 | export SYSTEM_NAME="kazeta" 6 | export USERNAME="gamer" 7 | export SIZE="5000MB" 8 | export ARCHIVE_DATE=$(date -d 'yesterday' +%Y/%m/%d) 9 | export WEBSITE="https://kazeta.org" 10 | export DOCUMENTATION_URL="https://kazeta.org" 11 | export BUG_REPORT_URL="https://github.com/kazetaos/kazeta/issues" 12 | 13 | export KERNEL_PACKAGE="linux" 14 | 15 | export PACKAGES="\ 16 | accountsservice \ 17 | fuse2 \ 18 | fuse3 \ 19 | fuse-overlayfs \ 20 | gamescope \ 21 | htop \ 22 | inputplumber \ 23 | less \ 24 | lib32-libxfixes \ 25 | lib32-nvidia-utils \ 26 | lib32-pipewire \ 27 | lib32-vulkan-intel \ 28 | lib32-vulkan-mesa-layers \ 29 | lib32-vulkan-radeon \ 30 | lib32-sdl12-compat \ 31 | lightdm \ 32 | linux-firmware \ 33 | nvidia-open \ 34 | nvidia-utils \ 35 | pipewire-jack \ 36 | pipewire-pulse \ 37 | pipewire-alsa \ 38 | rtkit \ 39 | sudo \ 40 | vim \ 41 | vulkan-intel \ 42 | vulkan-mesa-layers \ 43 | vulkan-radeon \ 44 | wireplumber \ 45 | " 46 | 47 | export AUR_PACKAGES="\ 48 | downgrade \ 49 | frzr \ 50 | pikaur \ 51 | udev-media-automount \ 52 | " 53 | 54 | export SERVICES="\ 55 | fstrim.timer \ 56 | inputplumber \ 57 | lightdm \ 58 | " 59 | 60 | export FILES_TO_DELETE="\ 61 | /boot/initramfs-linux-fallback.img \ 62 | /usr/share/SFML \ 63 | /usr/share/doc \ 64 | /usr/share/gtk-doc \ 65 | /usr/share/help \ 66 | /usr/share/man \ 67 | " 68 | 69 | postinstallhook() { 70 | # Add sudo permissions 71 | sed -i '/%wheel ALL=(ALL:ALL) ALL/s/^# //g' /etc/sudoers 72 | 73 | # Disable SPDIF/IEC958 audio output 74 | sed -e '/\[Mapping iec958/,+5 s/^/#/' -i '/usr/share/alsa-card-profile/mixer/profile-sets/default.conf' 75 | 76 | # Set a default timezone, FNA/XNA (and probably others) need it 77 | ln -s /usr/share/zoneinfo/UTC /etc/localtime 78 | 79 | # Persist kazeta save data 80 | mkdir -p /home/${USERNAME}/.local/share 81 | ln -s /var/kazeta /home/${USERNAME}/.local/share/kazeta 82 | 83 | # Persist wireplumber settings 84 | mkdir -p /home/${USERNAME}/.local/state 85 | ln -s /var/kazeta/state/wireplumber /home/${USERNAME}/.local/state/wireplumber 86 | 87 | # Set permissions 88 | chown -R ${USERNAME}:${USERNAME} /home/${USERNAME} 89 | chown -R ${USERNAME}:${USERNAME} /var/kazeta 90 | 91 | # Automount storage to /run/media 92 | sed -i -e 's,mediadir=/media,mediadir=/run/media,' /usr/bin/media-automount 93 | 94 | # Drop filesystem type from mount directory name 95 | sed -i -e 's,}.$TYPE",}",' /usr/bin/media-automount 96 | 97 | # Force Xbox 360 emulation for all controller devices 98 | sed -i -e 's/- xbox-elite/- xb360/' /usr/share/inputplumber/devices/*.yaml 99 | sed -i -e 's/- xbox-series/- xb360/' /usr/share/inputplumber/devices/*.yaml 100 | sed -i -e 's/- ds5/- xb360/' /usr/share/inputplumber/devices/*.yaml 101 | sed -i -e 's/- ds5-edge/- xb360/' /usr/share/inputplumber/devices/*.yaml 102 | } 103 | -------------------------------------------------------------------------------- /rootfs/etc/pacman.conf: -------------------------------------------------------------------------------- 1 | # 2 | # /etc/pacman.conf 3 | # 4 | # See the pacman.conf(5) manpage for option and repository directives 5 | 6 | # 7 | # GENERAL OPTIONS 8 | # 9 | [options] 10 | # The following paths are commented out with their default values listed. 11 | # If you wish to use different paths, uncomment and update the paths. 12 | #RootDir = / 13 | #DBPath = /var/lib/pacman/ 14 | #CacheDir = /var/cache/pacman/pkg/ 15 | #LogFile = /var/log/pacman.log 16 | #GPGDir = /etc/pacman.d/gnupg/ 17 | #HookDir = /etc/pacman.d/hooks/ 18 | HoldPkg = pacman glibc 19 | #XferCommand = /usr/bin/curl -L -C - -f -o %o %u 20 | #XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u 21 | #CleanMethod = KeepInstalled 22 | Architecture = auto 23 | 24 | # Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup 25 | #IgnorePkg = 26 | #IgnoreGroup = 27 | 28 | #NoUpgrade = 29 | #NoExtract = 30 | 31 | # Misc options 32 | #UseSyslog 33 | #Color 34 | #NoProgressBar 35 | CheckSpace 36 | #VerbosePkgLists 37 | ParallelDownloads = 5 38 | 39 | # By default, pacman accepts packages signed by keys that its local keyring 40 | # trusts (see pacman-key and its man page), as well as unsigned packages. 41 | SigLevel = Required DatabaseOptional 42 | LocalFileSigLevel = Optional 43 | #RemoteFileSigLevel = Required 44 | 45 | # NOTE: You must run `pacman-key --init` before first using pacman; the local 46 | # keyring can then be populated with the keys of all official Arch Linux 47 | # packagers with `pacman-key --populate archlinux`. 48 | 49 | # 50 | # REPOSITORIES 51 | # - can be defined here or included from another file 52 | # - pacman will search repositories in the order defined here 53 | # - local/custom mirrors can be added here or in separate files 54 | # - repositories listed first will take precedence when packages 55 | # have identical names, regardless of version number 56 | # - URLs will have $repo replaced by the name of the current repo 57 | # - URLs will have $arch replaced by the name of the architecture 58 | # 59 | # Repository entries are of the format: 60 | # [repo-name] 61 | # Server = ServerName 62 | # Include = IncludePath 63 | # 64 | # The header [repo-name] is crucial - it must be present and 65 | # uncommented to enable the repo. 66 | # 67 | 68 | # The testing repositories are disabled by default. To enable, uncomment the 69 | # repo name header and Include lines. You can add preferred servers immediately 70 | # after the header, and they will be used before the default mirrors. 71 | 72 | #[core-testing] 73 | #Include = /etc/pacman.d/mirrorlist 74 | 75 | [core] 76 | Include = /etc/pacman.d/mirrorlist 77 | 78 | #[extra-testing] 79 | #Include = /etc/pacman.d/mirrorlist 80 | 81 | [extra] 82 | Include = /etc/pacman.d/mirrorlist 83 | 84 | # If you want to run 32 bit applications on your x86_64 system, 85 | # enable the multilib repositories as required here. 86 | 87 | #[multilib-testing] 88 | #Include = /etc/pacman.d/mirrorlist 89 | 90 | [multilib] 91 | Include = /etc/pacman.d/mirrorlist 92 | 93 | # An example of a custom package repository. See the pacman manpage for 94 | # tips on creating your own repositories. 95 | #[custom] 96 | #SigLevel = Optional TrustAll 97 | #Server = file:///home/custompkgs 98 | -------------------------------------------------------------------------------- /rootfs/usr/bin/kazeta: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -x 4 | 5 | BASE_DIR="$HOME/.local/share/kazeta" 6 | if [[ ! -d "${BASE_DIR}" ]]; then 7 | mkdir -p "${BASE_DIR}" 8 | fi 9 | 10 | BASE_EXT="/media" 11 | result=$(ls -1 /media | wc -l) 12 | if [[ "$result" == "0" ]]; then 13 | BASE_EXT="/run/media/${USER}" 14 | if [[ ! -d "${BASE_EXT}" ]]; then 15 | BASE_EXT="/run/media" 16 | fi 17 | fi 18 | 19 | function get_attribute { 20 | attribute=$1 21 | shift 22 | info_file="$@" 23 | cat "${info_file}" | grep "^${attribute}=" | head -1 | cut -d= -f2- 24 | } 25 | 26 | function start_playtime_capture { 27 | echo "running" > /tmp/kazeta_playtime_capture_state 28 | while [ "$(cat /tmp/kazeta_playtime_capture_state)" == "running" ]; do 29 | sleep 60 & 30 | echo $! > /tmp/kazeta_playtime_capture_sleep_pid 31 | wait 32 | date --iso-8601=seconds > .kazeta/var/playtime_end 33 | done 34 | echo "done" > /tmp/kazeta_playtime_capture_state 35 | } 36 | 37 | function stop_playtime_capture { 38 | echo "shutdown" > /tmp/kazeta_playtime_capture_state 39 | kill $(cat /tmp/kazeta_playtime_capture_sleep_pid) 40 | while [ "$(cat /tmp/kazeta_playtime_capture_state)" != "done" ]; do 41 | sleep 0.001 42 | done 43 | } 44 | 45 | function cleanup { 46 | stop_playtime_capture 47 | popd 48 | pkexec kazeta-mount --unmount "${target}" "${runtimedir}" 49 | pkexec kazeta-mount kzp --unmount "${cart_path}" 50 | rm -rf "${upper}/.kazeta/share" 51 | rmdir --ignore-fail-on-non-empty "${upper}/.kazeta" 52 | } 53 | 54 | # wait up to 2 seconds for a cart 55 | for i in $(seq 1 20); do 56 | cart_info=$(find ${BASE_EXT} -maxdepth 2 -name "*.kzi" | head -1) 57 | cart_pkg=$(find ${BASE_EXT} -maxdepth 2 -name "*.kzp" | head -1) 58 | if [[ -f "${cart_info}" ]] || [[ -f "${cart_pkg}" ]]; then 59 | break 60 | else 61 | sleep 0.1 62 | fi 63 | done 64 | 65 | if [[ ! -f "${cart_info}" ]] && [[ ! -f "${cart_pkg}" ]]; then 66 | # no cart found, start bios/memory management app 67 | gamescope --filter pixel -- kazeta-bios 68 | exit 0 69 | fi 70 | 71 | if [[ -f "${cart_pkg}" ]]; then 72 | cart_path="${BASE_DIR}/run/pkg" 73 | mkdir -p "${cart_path}" 74 | pkexec kazeta-mount kzp "${cart_pkg}" "${cart_path}" 75 | cart_info=$(find "${cart_path}" -maxdepth 2 -name "*.kzi" | head -1) 76 | media_path=$(dirname "${cart_pkg}") 77 | else 78 | cart_path=$(dirname "${cart_info}") 79 | media_path="${cart_path}" 80 | fi 81 | 82 | cart_id="$(get_attribute 'Id' ${cart_info})" 83 | cart_icon="${cart_path}/$(get_attribute 'Icon' ${cart_info})" 84 | 85 | mkdir -p "${BASE_DIR}/cache/${cart_id}" 86 | cp "${cart_info}" "${BASE_DIR}/cache/${cart_id}/metadata.kzi" 87 | if [[ -f "${cart_icon}" ]]; then 88 | cp "${cart_icon}" "${BASE_DIR}/cache/${cart_id}/icon.png" 89 | fi 90 | 91 | lower="${cart_path}" 92 | upper="${BASE_DIR}/saves/default/${cart_id}" 93 | work="${BASE_DIR}/run/work" 94 | target="${BASE_DIR}/run/cart" 95 | 96 | if [[ ! -d "${upper}" ]]; then 97 | rm -f "${upper}" 98 | mkdir -p "${upper}" 99 | fi 100 | 101 | runtime_name="$(get_attribute 'Runtime' ${cart_info})" 102 | if [ -n "$runtime_name" ]; then 103 | runtime="${media_path}/${runtime_name}" 104 | if [ ! -f "$runtime" ]; then 105 | runtime="${media_path}/${runtime_name}.kzr" 106 | if [ ! -f "$runtime" ]; then 107 | runtime="$(ls ${media_path}/${runtime_name}-*.kzr)" 108 | if [ ! -f "$runtime" ]; then 109 | runtime="/usr/share/kazeta/runtimes/none.kzr" 110 | fi 111 | fi 112 | fi 113 | else 114 | runtime="/usr/share/kazeta/runtimes/none.kzr" 115 | fi 116 | 117 | runtimedir="${BASE_DIR}/run/runtime" 118 | pkexec kazeta-mount "${lower}" "${upper}" "${work}" "${target}" "${runtime}" "${runtimedir}" 119 | trap cleanup EXIT 120 | 121 | export HOME="${BASE_DIR}/run/cart" 122 | 123 | unset XDG_CONFIG_HOME 124 | unset XDG_CACHE_HOME 125 | unset XDG_DATA_HOME 126 | unset XDG_STATE_HOME 127 | pushd "${HOME}" 128 | 129 | cart_exec="$(get_attribute 'Exec' ${cart_info})" 130 | cart_gsopts="$(get_attribute 'GamescopeOptions' ${cart_info})" 131 | echo "$cart_exec" > /tmp/kazeta-cart-exec 132 | 133 | # append previously captured playtime to the log 134 | if [ -f .kazeta/var/playtime_start ] && [ -f .kazeta/var/playtime_end ]; then 135 | echo "$(cat .kazeta/var/playtime_start) $(cat .kazeta/var/playtime_end)" >> .kazeta/var/playtime.log 136 | fi 137 | rm -f .kazeta/var/playtime_start 138 | rm -f .kazeta/var/playtime_end 139 | 140 | mkdir -p .kazeta/var 141 | date --iso-8601=seconds > .kazeta/var/playtime_start 142 | start_playtime_capture & 143 | gamescope ${cart_gsopts} -- ./.kazeta/share/run /tmp/kazeta-cart-exec | grep -v "pressure-vessel-wrap" > "${HOME}/.kazeta/var/run.log" 2>&1 144 | -------------------------------------------------------------------------------- /bios/src/save.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::path::PathBuf; 3 | use std::fs; 4 | use std::collections::VecDeque; 5 | use std::io::{self, BufRead, Write, Read}; 6 | use sysinfo::Disks; 7 | use tar::{Builder, Archive}; 8 | use std::sync::atomic::{AtomicU16, Ordering}; 9 | use std::sync::Arc; 10 | use walkdir; 11 | use std::process::Command; 12 | use chrono::DateTime; 13 | 14 | // Directories to exclude from size calculation and copying 15 | const EXCLUDED_DIRS: &[&str] = &[ 16 | ".cache", 17 | ".config/pulse/cookie", 18 | ".kazeta/share", 19 | ".kazeta/var/prefix/dosdevices", 20 | ".kazeta/var/prefix/drive_c/windows", 21 | ".kazeta/var/prefix/pfx" 22 | ]; 23 | 24 | fn should_exclude_path(path: &Path) -> bool { 25 | let path_str = path.to_str().unwrap_or(""); 26 | EXCLUDED_DIRS.iter().any(|&excluded| path_str.contains(excluded)) 27 | } 28 | 29 | /// Searches for files with a given extension within a directory up to a specified depth 30 | /// 31 | /// # Arguments 32 | /// * `dir` - The directory to search in 33 | /// * `extension` - The file extension to search for (without the dot, e.g., "txt", "rs") 34 | /// * `max_depth` - Maximum depth to search (0 = only current directory) 35 | /// * `find_first` - If true, stops after finding the first match 36 | /// 37 | /// # Returns 38 | /// * `Result, io::Error>` - Vector of found file paths or an error 39 | /// 40 | /// # Note 41 | /// This function ignores permission errors and other I/O errors for individual files/directories 42 | /// and continues searching. It only returns an error if the initial directory is inaccessible. 43 | /// Searches breadth-first (higher level directories first). 44 | pub fn find_files_by_extension>( 45 | dir: P, 46 | extension: &str, 47 | max_depth: usize, 48 | find_first: bool, 49 | ) -> Result, io::Error> { 50 | let dir_path = dir.as_ref(); 51 | 52 | // Check if initial directory exists and is accessible 53 | if !dir_path.exists() { 54 | return Err(io::Error::new( 55 | io::ErrorKind::NotFound, 56 | format!("Directory does not exist: {}", dir_path.display()) 57 | )); 58 | } 59 | 60 | // Try to read the initial directory to ensure it's accessible 61 | fs::read_dir(dir_path)?; 62 | 63 | let mut results = Vec::new(); 64 | search_breadth_first(dir_path, extension, max_depth, find_first, &mut results); 65 | Ok(results) 66 | } 67 | 68 | fn search_breadth_first( 69 | start_dir: &Path, 70 | extension: &str, 71 | max_depth: usize, 72 | find_first: bool, 73 | results: &mut Vec, 74 | ) { 75 | let mut queue = VecDeque::new(); 76 | queue.push_back((start_dir.to_path_buf(), 0)); 77 | 78 | while let Some((current_dir, depth)) = queue.pop_front() { 79 | if depth > max_depth { 80 | continue; 81 | } 82 | 83 | let entries = match fs::read_dir(¤t_dir) { 84 | Ok(entries) => entries, 85 | Err(_) => continue, // Skip directories we can't read 86 | }; 87 | 88 | let mut subdirs = Vec::new(); 89 | 90 | // First, process all files in the current directory 91 | for entry in entries { 92 | let entry = match entry { 93 | Ok(entry) => entry, 94 | Err(_) => continue, // Skip entries we can't read 95 | }; 96 | 97 | let path = entry.path(); 98 | 99 | let metadata = match path.metadata() { 100 | Ok(metadata) => metadata, 101 | Err(_) => continue, // Skip files/dirs we can't get metadata for 102 | }; 103 | 104 | if metadata.is_file() { 105 | // Check if file has the desired extension 106 | if let Some(file_ext) = path.extension() { 107 | if file_ext.to_string_lossy().eq_ignore_ascii_case(extension) { 108 | results.push(path); 109 | if find_first { 110 | return; // Exit immediately if we only want the first match 111 | } 112 | } 113 | } 114 | } else if metadata.is_dir() && depth < max_depth { 115 | // Collect subdirectories to process later 116 | subdirs.push(path); 117 | } 118 | } 119 | 120 | // Then add subdirectories to the queue for next level processing 121 | for subdir in subdirs { 122 | queue.push_back((subdir, depth + 1)); 123 | } 124 | } 125 | } 126 | 127 | pub fn get_save_dir_from_drive_name(drive_name: &str) -> String { 128 | let base_dir = dirs::home_dir().unwrap().join(".local/share/kazeta"); 129 | if drive_name == "internal" || drive_name.is_empty() { 130 | let save_dir = base_dir.join("saves/default"); 131 | if !save_dir.exists() { 132 | fs::create_dir_all(&save_dir).unwrap_or_else(|e| { 133 | eprintln!("Failed to create save directory: {}", e); 134 | }); 135 | } 136 | save_dir.to_string_lossy().into_owned() 137 | } else { 138 | let base_ext = if Path::new("/media").read_dir().map(|mut d| d.next().is_none()).unwrap_or(true) { 139 | if Path::new(&format!("/run/media/{}", whoami::username())).exists() { 140 | format!("/run/media/{}", whoami::username()) 141 | } else { 142 | "/run/media".to_string() 143 | } 144 | } else { 145 | "/media".to_string() 146 | }; 147 | 148 | let save_dir = Path::new(&base_ext).join(drive_name).join("kazeta/saves"); 149 | if !save_dir.exists() { 150 | fs::create_dir_all(&save_dir).unwrap_or_else(|e| { 151 | eprintln!("Failed to create save directory: {}", e); 152 | }); 153 | } 154 | save_dir.to_string_lossy().into_owned() 155 | } 156 | } 157 | 158 | pub fn get_cache_dir_from_drive_name(drive_name: &str) -> String { 159 | let base_dir = dirs::home_dir().unwrap().join(".local/share/kazeta"); 160 | if drive_name == "internal" || drive_name.is_empty() { 161 | let cache_dir = base_dir.join("cache"); 162 | if !cache_dir.exists() { 163 | fs::create_dir_all(&cache_dir).unwrap_or_else(|e| { 164 | eprintln!("Failed to create cache directory: {}", e); 165 | }); 166 | } 167 | cache_dir.to_string_lossy().into_owned() 168 | } else { 169 | let base_ext = if Path::new("/media").read_dir().map(|mut d| d.next().is_none()).unwrap_or(true) { 170 | if Path::new(&format!("/run/media/{}", whoami::username())).exists() { 171 | format!("/run/media/{}", whoami::username()) 172 | } else { 173 | "/run/media".to_string() 174 | } 175 | } else { 176 | "/media".to_string() 177 | }; 178 | 179 | let cache_dir = Path::new(&base_ext).join(drive_name).join("kazeta/cache"); 180 | if !cache_dir.exists() { 181 | fs::create_dir_all(&cache_dir).unwrap_or_else(|e| { 182 | eprintln!("Failed to create cache directory: {}", e); 183 | }); 184 | } 185 | cache_dir.to_string_lossy().into_owned() 186 | } 187 | } 188 | 189 | fn get_attribute(info_file: &Path, attribute: &str) -> io::Result { 190 | let file = fs::File::open(info_file)?; 191 | let reader = io::BufReader::new(file); 192 | 193 | for line in reader.lines() { 194 | let line = line?; 195 | if line.starts_with(&format!("{}=", attribute)) { 196 | return Ok(line[attribute.len() + 1..].to_string()); 197 | } 198 | } 199 | 200 | Ok(String::new()) 201 | } 202 | 203 | pub fn list_devices() -> io::Result> { 204 | let mut devices = Vec::new(); 205 | let disks = Disks::new_with_refreshed_list(); 206 | 207 | // Add internal drive 208 | let base_dir = dirs::home_dir().unwrap().join(".local/share/kazeta"); 209 | let base_dir_str = base_dir.to_str().unwrap(); 210 | 211 | // Find the disk that contains our base directory 212 | let internal_disk = disks.iter() 213 | .find(|disk| { 214 | let mount_point = disk.mount_point().to_str().unwrap(); 215 | base_dir_str.starts_with(mount_point) 216 | }) 217 | .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not find internal disk"))?; 218 | 219 | let free_space = (internal_disk.available_space() / 1024 / 1024) as u32; // Convert to MB 220 | devices.push(("internal".to_string(), free_space)); 221 | 222 | // Add external drives 223 | let base_ext = if Path::new("/media").read_dir().map(|mut d| d.next().is_none()).unwrap_or(true) { 224 | if Path::new(&format!("/run/media/{}", whoami::username())).exists() { 225 | format!("/run/media/{}", whoami::username()) 226 | } else { 227 | "/run/media".to_string() 228 | } 229 | } else { 230 | "/media".to_string() 231 | }; 232 | 233 | // Find all disks mounted under the external base directory 234 | for disk in disks.iter() { 235 | let mount_point = disk.mount_point().to_str().unwrap(); 236 | if mount_point.starts_with(&base_ext) { 237 | let name = mount_point.split('/').last().unwrap().to_string(); 238 | if name == "frzr_efi" { 239 | // ignore internal frzr partition 240 | continue; 241 | } 242 | let free_space = (disk.available_space() / 1024 / 1024) as u32; // Convert to MB 243 | devices.push((name, free_space)); 244 | } 245 | } 246 | 247 | Ok(devices) 248 | } 249 | 250 | pub fn has_save_dir(drive_name: &str) -> bool { 251 | if drive_name == "internal" { 252 | return true; 253 | } 254 | 255 | let save_dir = get_save_dir_from_drive_name(drive_name); 256 | Path::new(&save_dir).exists() 257 | } 258 | 259 | pub fn is_cart(drive_name: &str) -> bool { 260 | if drive_name == "internal" { 261 | return false; 262 | } 263 | 264 | let save_dir = get_save_dir_from_drive_name(drive_name); 265 | let mount_point: String = Path::new(&save_dir).parent().unwrap().parent().unwrap().display().to_string(); 266 | 267 | if let Ok(files) = find_files_by_extension(mount_point, "kzi", 1, true) { 268 | if files.len() > 0 { 269 | return true; 270 | } 271 | } 272 | 273 | false 274 | } 275 | 276 | pub fn is_cart_connected() -> bool { 277 | if let Ok(files) = find_files_by_extension("/run/media", "kzi", 2, true) { 278 | if files.len() > 0 { 279 | return true; 280 | } 281 | } 282 | 283 | false 284 | } 285 | 286 | pub fn get_save_details(drive_name: &str) -> io::Result> { 287 | let save_dir = get_save_dir_from_drive_name(drive_name); 288 | let cache_dir = get_cache_dir_from_drive_name(drive_name); 289 | eprintln!("Getting save details from directory: {}", save_dir); 290 | let mut details = Vec::new(); 291 | 292 | for entry in fs::read_dir(save_dir)? { 293 | let entry = entry?; 294 | let path = entry.path(); 295 | let file_name = path.file_name() 296 | .and_then(|n| n.to_str()) 297 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Invalid filename"))?; 298 | 299 | // Remove .tar extension if present 300 | let cart_id = if file_name.ends_with(".tar") { 301 | &file_name[..file_name.len() - 4] 302 | } else { 303 | file_name 304 | }; 305 | 306 | let metadata_path = Path::new(&cache_dir).join(cart_id).join("metadata.kzi"); 307 | let name = get_attribute(&metadata_path, "Name").unwrap_or_else(|e| { 308 | eprintln!("Failed to read metadata for {}: {}", cart_id, e); 309 | String::new() 310 | }); 311 | let icon = format!("{}/{}/icon.png", cache_dir, cart_id); 312 | 313 | details.push((cart_id.to_string(), name, icon)); 314 | } 315 | 316 | // Sort details alphabetically by name, fallback to cart_id if name is empty 317 | details.sort_by(|a, b| { 318 | let name_a = if a.1.is_empty() { &a.0 } else { &a.1 }; 319 | let name_b = if b.1.is_empty() { &b.0 } else { &b.1 }; 320 | name_a.to_lowercase().cmp(&name_b.to_lowercase()) 321 | }); 322 | 323 | eprintln!("Found {} save details", details.len()); 324 | Ok(details) 325 | } 326 | 327 | pub fn delete_save(cart_id: &str, from_drive: &str) -> Result<(), String> { 328 | let from_dir = get_save_dir_from_drive_name(from_drive); 329 | let from_cache = get_cache_dir_from_drive_name(from_drive); 330 | 331 | // Check if save exists 332 | let save_path = Path::new(&from_dir).join(cart_id); 333 | let save_path_tar = Path::new(&from_dir).join(format!("{}.tar", cart_id)); 334 | if !save_path.exists() && !save_path_tar.exists() { 335 | return Err(format!("Save file for {} does not exist on '{}' drive", cart_id, from_drive)); 336 | } 337 | 338 | // Delete save file 339 | if from_drive == "internal" { 340 | fs::remove_dir_all(save_path).map_err(|e| e.to_string())?; 341 | } else { 342 | fs::remove_file(save_path_tar).map_err(|e| e.to_string())?; 343 | } 344 | 345 | // Delete cache 346 | let cache_path = Path::new(&from_cache).join(cart_id); 347 | if cache_path.exists() { 348 | fs::remove_dir_all(cache_path).map_err(|e| e.to_string())?; 349 | } 350 | 351 | Ok(()) 352 | } 353 | 354 | pub fn copy_save(cart_id: &str, from_drive: &str, to_drive: &str, progress: Arc) -> Result<(), String> { 355 | let from_dir = get_save_dir_from_drive_name(from_drive); 356 | let to_dir = get_save_dir_from_drive_name(to_drive); 357 | let from_cache = get_cache_dir_from_drive_name(from_drive); 358 | let to_cache = get_cache_dir_from_drive_name(to_drive); 359 | 360 | if from_drive == to_drive { 361 | return Err("Cannot copy to same location".to_string()); 362 | } 363 | 364 | // Check if source save exists 365 | let from_path = Path::new(&from_dir).join(cart_id); 366 | let from_path_tar = Path::new(&from_dir).join(format!("{}.tar", cart_id)); 367 | if !from_path.exists() && !from_path_tar.exists() { 368 | return Err(format!("Save file for {} does not exist on '{}' drive", cart_id, from_drive)); 369 | } 370 | 371 | // Check if destination save already exists 372 | let to_path = Path::new(&to_dir).join(cart_id); 373 | let to_path_tar = Path::new(&to_dir).join(format!("{}.tar", cart_id)); 374 | if to_path.exists() || to_path_tar.exists() { 375 | return Err(format!("Save file for {} already exists on '{}'", cart_id, to_drive)); 376 | } 377 | 378 | // Create destination directories 379 | fs::create_dir_all(&to_dir).map_err(|e| e.to_string())?; 380 | fs::create_dir_all(&to_cache).map_err(|e| e.to_string())?; 381 | 382 | // Copy save data 383 | let result = if from_drive == "internal" { 384 | // Internal to external: create tar archive 385 | eprintln!("Starting internal to external copy for {}", cart_id); 386 | let file = fs::File::create(&to_path_tar).map_err(|e| format!("Failed to create destination file: {}", e))?; 387 | let mut builder = Builder::new(file); 388 | 389 | // Calculate total size for progress reporting 390 | let mut total_size = 0; 391 | for entry in walkdir::WalkDir::new(&from_path) 392 | .into_iter() 393 | .filter_map(|e| e.ok()) 394 | .filter(|e| { 395 | let path = e.path(); 396 | // Skip excluded directories and their contents 397 | !should_exclude_path(path) && 398 | path.is_file() 399 | }) { 400 | total_size += entry.metadata().map_err(|e| format!("Failed to get metadata: {}", e))?.len(); 401 | } 402 | 403 | eprintln!("Total size to archive: {} bytes", total_size); 404 | if total_size == 0 { 405 | return Err("No files found to archive".to_string()); 406 | } 407 | 408 | // Add the entire directory to the archive, excluding ignored directories 409 | let mut current_size = 0; 410 | for entry in walkdir::WalkDir::new(&from_path) 411 | .into_iter() 412 | .filter_map(|e| e.ok()) 413 | .filter(|e| { 414 | let path = e.path(); 415 | // Skip excluded directories and their contents 416 | !should_exclude_path(path) && 417 | path.is_file() 418 | }) { 419 | let path = entry.path(); 420 | // Get the relative path from the source directory 421 | let name = path.strip_prefix(&from_path) 422 | .map_err(|e| format!("Failed to get relative path: {}", e))? 423 | .to_str() 424 | .ok_or_else(|| "Invalid path encoding".to_string())?; 425 | 426 | let file_size = entry.metadata().map_err(|e| format!("Failed to get file metadata: {}", e))?.len(); 427 | eprintln!("Adding file to archive: {} ({} bytes)", name, file_size); 428 | 429 | let mut file = fs::File::open(path).map_err(|e| format!("Failed to open source file: {}", e))?; 430 | 431 | // Create a new header with the correct path 432 | let mut header = tar::Header::new_gnu(); 433 | header.set_path(name).map_err(|e| format!("Failed to set path in header: {}", e))?; 434 | header.set_size(file_size); 435 | header.set_cksum(); 436 | 437 | // Write the header and file contents 438 | builder.append(&header, &mut file).map_err(|e| format!("Failed to append file to archive: {}", e))?; 439 | sync_to_disk(); 440 | 441 | current_size += file_size; 442 | progress.store((current_size * 100 / total_size) as u16, Ordering::SeqCst); 443 | } 444 | 445 | eprintln!("Finished creating archive, final size: {} bytes", current_size); 446 | if current_size == 0 { 447 | return Err("No files were added to the archive".to_string()); 448 | } 449 | 450 | builder.finish().map_err(|e| format!("Failed to finish archive: {}", e))?; 451 | sync_to_disk(); 452 | 453 | // Verify the archive was created and has content 454 | let archive_size = fs::metadata(&to_path_tar).map_err(|e| format!("Failed to get archive metadata: {}", e))?.len(); 455 | eprintln!("Archive file size: {} bytes", archive_size); 456 | if archive_size == 0 { 457 | return Err("Created archive is empty".to_string()); 458 | } 459 | 460 | Ok(()) 461 | } else if to_drive == "internal" { 462 | // External to internal: extract tar archive 463 | eprintln!("Starting external to internal copy for {}", cart_id); 464 | fs::create_dir_all(&to_path).map_err(|e| format!("Failed to create destination directory: {}", e))?; 465 | 466 | let file = fs::File::open(&from_path_tar).map_err(|e| format!("Failed to open source archive: {}", e))?; 467 | let file_size = file.metadata().map_err(|e| format!("Failed to get archive metadata: {}", e))?.len(); 468 | eprintln!("Archive size: {} bytes", file_size); 469 | 470 | let mut archive = Archive::new(file); 471 | let mut current_size = 0; 472 | 473 | for entry in archive.entries().map_err(|e| format!("Failed to read archive entries: {}", e))? { 474 | let mut entry = entry.map_err(|e| format!("Failed to read archive entry: {}", e))?; 475 | let path = entry.path().map_err(|e| format!("Failed to get entry path: {}", e))?; 476 | let entry_size = entry.header().size().unwrap_or(0); 477 | eprintln!("Extracting: {} ({} bytes)", path.display(), entry_size); 478 | 479 | // Ensure the parent directory exists 480 | if let Some(parent) = path.parent() { 481 | fs::create_dir_all(to_path.join(parent)) 482 | .map_err(|e| format!("Failed to create parent directory: {}", e))?; 483 | } 484 | 485 | // Extract the file 486 | entry.unpack_in(&to_path) 487 | .map_err(|e| format!("Failed to extract file: {}", e))?; 488 | 489 | current_size += entry_size; 490 | progress.store((current_size * 100 / file_size) as u16, Ordering::SeqCst); 491 | } 492 | 493 | // Verify extraction 494 | let mut extracted_size = 0; 495 | for entry in walkdir::WalkDir::new(&to_path) 496 | .into_iter() 497 | .filter_map(|e| e.ok()) 498 | .filter(|e| e.path().is_file()) { 499 | extracted_size += entry.metadata() 500 | .map_err(|e| format!("Failed to get extracted file metadata: {}", e))? 501 | .len(); 502 | } 503 | eprintln!("Total extracted size: {} bytes", extracted_size); 504 | 505 | if extracted_size == 0 { 506 | return Err("No files were extracted from the archive".to_string()); 507 | } 508 | 509 | Ok(()) 510 | } else { 511 | // External to external: direct copy with progress 512 | let file_size = fs::metadata(&from_path_tar).map_err(|e| e.to_string())?.len(); 513 | let mut source = fs::File::open(&from_path_tar).map_err(|e| e.to_string())?; 514 | let mut dest = fs::File::create(&to_path_tar).map_err(|e| e.to_string())?; 515 | 516 | let mut buffer = [0; 8192]; 517 | let mut current_size = 0; 518 | loop { 519 | let bytes_read = source.read(&mut buffer).map_err(|e| e.to_string())?; 520 | if bytes_read == 0 { 521 | break; 522 | } 523 | dest.write_all(&buffer[..bytes_read]).map_err(|e| e.to_string())?; 524 | sync_to_disk(); 525 | 526 | current_size += bytes_read as u64; 527 | progress.store((current_size * 100 / file_size) as u16, Ordering::SeqCst); 528 | } 529 | Ok(()) 530 | }; 531 | 532 | // If the main copy operation failed, clean up and return error 533 | if let Err(e) = result { 534 | // Clean up by removing the top-level directories 535 | if to_drive == "internal" { 536 | fs::remove_dir_all(&to_path).ok(); 537 | } else { 538 | fs::remove_file(&to_path_tar).ok(); 539 | } 540 | fs::remove_dir_all(Path::new(&to_cache).join(cart_id)).ok(); 541 | return Err(e); 542 | } 543 | 544 | // Copy cache files 545 | let to_cache_path = Path::new(&to_cache).join(cart_id); 546 | fs::remove_dir_all(&to_cache_path).ok(); // Ignore errors if directory doesn't exist 547 | fs::create_dir_all(&to_cache_path).map_err(|e| e.to_string())?; 548 | 549 | // Copy metadata.kzi if it exists 550 | let from_metadata = Path::new(&from_cache).join(cart_id).join("metadata.kzi"); 551 | let to_metadata = to_cache_path.join("metadata.kzi"); 552 | if from_metadata.exists() { 553 | fs::copy(&from_metadata, &to_metadata).map_err(|e| e.to_string())?; 554 | } 555 | 556 | // Copy icon.png if it exists 557 | let from_icon = Path::new(&from_cache).join(cart_id).join("icon.png"); 558 | let to_icon = to_cache_path.join("icon.png"); 559 | if from_icon.exists() { 560 | fs::copy(&from_icon, &to_icon).map_err(|e| e.to_string())?; 561 | } 562 | 563 | sync_to_disk(); 564 | Ok(()) 565 | } 566 | 567 | /// Calculate total playtime for a game from its .kazeta/var/playtime.log file 568 | /// Returns playtime in hours with one decimal place 569 | pub fn calculate_playtime(cart_id: &str, drive_name: &str) -> f32 { 570 | println!("Calculating playtime for {} on {}", cart_id, drive_name); 571 | let save_dir = get_save_dir_from_drive_name(drive_name); 572 | 573 | // Check if this is a tar file (external drive) or directory (internal drive) 574 | let tar_path = Path::new(&save_dir).join(format!("{}.tar", cart_id)); 575 | let dir_path = Path::new(&save_dir).join(cart_id); 576 | 577 | if tar_path.exists() { 578 | // External drive: read from tar archive 579 | calculate_playtime_from_tar(&tar_path, cart_id) 580 | } else if dir_path.exists() { 581 | // Internal drive: read from directory 582 | calculate_playtime_from_dir(&dir_path, cart_id) 583 | } else { 584 | // Neither exists 585 | 0.0 586 | } 587 | } 588 | 589 | /// Calculate playtime from a tar archive (external drives) 590 | fn calculate_playtime_from_tar(tar_path: &Path, _cart_id: &str) -> f32 { 591 | let file = match fs::File::open(tar_path) { 592 | Ok(file) => file, 593 | Err(e) => { 594 | eprintln!("Failed to open tar file {}: {}", tar_path.display(), e); 595 | return 0.0; 596 | } 597 | }; 598 | 599 | let mut archive = tar::Archive::new(file); 600 | let entries = match archive.entries() { 601 | Ok(entries) => entries, 602 | Err(e) => { 603 | eprintln!("Failed to read archive entries: {}", e); 604 | return 0.0; 605 | } 606 | }; 607 | 608 | let mut content = String::new(); 609 | let mut start_content = String::new(); 610 | let mut end_content = String::new(); 611 | 612 | for entry_result in entries { 613 | let mut entry = match entry_result { 614 | Ok(entry) => entry, 615 | Err(e) => { 616 | eprintln!("Failed to read tar entry: {}", e); 617 | continue; 618 | } 619 | }; 620 | 621 | let path = match entry.path() { 622 | Ok(path) => path, 623 | Err(e) => { 624 | eprintln!("Failed to get tar entry path: {}", e); 625 | continue; 626 | } 627 | }; 628 | 629 | if path.display().to_string() == ".kazeta/var/playtime.log" { 630 | let _ = entry.read_to_string(&mut content); 631 | } else if path.display().to_string() == ".kazeta/var/playtime_start" { 632 | let _ = entry.read_to_string(&mut start_content); 633 | } else if path.display().to_string() == ".kazeta/var/playtime_end" { 634 | let _ = entry.read_to_string(&mut end_content); 635 | } 636 | } 637 | 638 | parse_playtime_content(&format!("{}\n{} {}", content.trim(), start_content.trim(), end_content.trim())) 639 | } 640 | 641 | /// Calculate playtime from a directory (internal drives) 642 | fn calculate_playtime_from_dir(dir_path: &Path, _cart_id: &str) -> f32 { 643 | let playtime_log_path = dir_path.join(".kazeta/var/playtime.log"); 644 | let playtime_start_path = dir_path.join(".kazeta/var/playtime_start"); 645 | let playtime_end_path = dir_path.join(".kazeta/var/playtime_end"); 646 | 647 | let content = match fs::read_to_string(&playtime_log_path) { 648 | Ok(content) => content.trim().to_string(), 649 | Err(_) => "".to_string(), 650 | }; 651 | 652 | let start_content = match fs::read_to_string(&playtime_start_path) { 653 | Ok(content) => content.trim().to_string(), 654 | Err(_) => "".to_string(), 655 | }; 656 | 657 | let end_content = match fs::read_to_string(&playtime_end_path) { 658 | Ok(content) => content.trim().to_string(), 659 | Err(_) => "".to_string(), 660 | }; 661 | 662 | return parse_playtime_content(&format!("{}\n{} {}", content.trim(), start_content.trim(), end_content.trim())); 663 | } 664 | 665 | /// Parse playtime content from a string (common logic for both tar and directory) 666 | fn parse_playtime_content(content: &str) -> f32 { 667 | let mut total_seconds: i64 = 0; 668 | 669 | for line in content.lines() { 670 | let parts: Vec<&str> = line.split_whitespace().collect(); 671 | if parts.len() != 2 { 672 | continue; 673 | } 674 | 675 | let start_time = match DateTime::parse_from_rfc3339(parts[0]) { 676 | Ok(dt) => dt, 677 | Err(e) => { 678 | eprintln!("Failed to parse start time '{}': {}", parts[0], e); 679 | continue; 680 | } 681 | }; 682 | 683 | let end_time = match DateTime::parse_from_rfc3339(parts[1]) { 684 | Ok(dt) => dt, 685 | Err(e) => { 686 | eprintln!("Failed to parse end time '{}': {}", parts[1], e); 687 | continue; 688 | } 689 | }; 690 | 691 | let duration = end_time.signed_duration_since(start_time); 692 | total_seconds += duration.num_seconds(); 693 | } 694 | 695 | // Convert to hours rounded to one decimal place 696 | ((total_seconds as f64 / 360.0).round() / 10.0) as f32 697 | } 698 | 699 | /// Calculate save data size for a game (lazy calculation) 700 | /// Returns size in MB with one decimal place 701 | pub fn calculate_save_size(cart_id: &str, drive_name: &str) -> f32 { 702 | println!("Calculating save size for {} on {}", cart_id, drive_name); 703 | let save_dir = get_save_dir_from_drive_name(drive_name); 704 | 705 | // Check if this is a tar file (external drive) or directory (internal drive) 706 | let tar_path = Path::new(&save_dir).join(format!("{}.tar", cart_id)); 707 | let dir_path = Path::new(&save_dir).join(cart_id); 708 | 709 | let size_bytes = if tar_path.exists() { 710 | // External drive: get tar file size 711 | calculate_size_from_tar(&tar_path) 712 | } else if dir_path.exists() { 713 | // Internal drive: calculate directory size 714 | calculate_size_from_dir(&dir_path) 715 | } else { 716 | // Neither exists 717 | return 0.0; 718 | }; 719 | 720 | // Convert to MB with one decimal place, rounding up to nearest 0.1 MB if non-zero 721 | let size_mb = size_bytes as f64 / 1024.0 / 1024.0; 722 | if size_mb > 0.0 { 723 | ((size_mb * 10.0).ceil() / 10.0) as f32 724 | } else { 725 | 0.0 726 | } 727 | } 728 | 729 | /// Calculate size from a tar archive (external drives) 730 | fn calculate_size_from_tar(tar_path: &Path) -> u64 { 731 | let metadata = match fs::metadata(tar_path) { 732 | Ok(metadata) => metadata, 733 | Err(e) => { 734 | eprintln!("Failed to get tar file metadata: {}", e); 735 | return 0; 736 | } 737 | }; 738 | metadata.len() 739 | } 740 | 741 | /// Calculate size from a directory (internal drives) 742 | fn calculate_size_from_dir(dir_path: &Path) -> u64 { 743 | let mut total_size = 0u64; 744 | 745 | for entry in walkdir::WalkDir::new(dir_path) 746 | .into_iter() 747 | .filter_map(|e| e.ok()) 748 | .filter(|e| { 749 | let path = e.path(); 750 | // Skip excluded directories and their contents 751 | !should_exclude_path(path) && 752 | path.is_file() 753 | }) { 754 | if let Ok(metadata) = entry.metadata() { 755 | total_size += metadata.len(); 756 | } 757 | } 758 | total_size 759 | } 760 | 761 | fn sync_to_disk() { 762 | if let Ok(output) = Command::new("sync") 763 | .output() 764 | .map_err(|e| format!("Failed to execute sync command: {}", e)) { 765 | 766 | if !output.status.success() { 767 | println!("Sync command failed with status: {}", output.status); 768 | } 769 | } 770 | } -------------------------------------------------------------------------------- /bios/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "allocator-api2" 13 | version = "0.2.21" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 16 | 17 | [[package]] 18 | name = "android-tzdata" 19 | version = "0.1.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 22 | 23 | [[package]] 24 | name = "android_system_properties" 25 | version = "0.1.5" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 28 | dependencies = [ 29 | "libc", 30 | ] 31 | 32 | [[package]] 33 | name = "audir-sles" 34 | version = "0.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5" 37 | 38 | [[package]] 39 | name = "audrey" 40 | version = "0.3.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8" 43 | dependencies = [ 44 | "dasp_frame", 45 | "dasp_sample", 46 | "hound", 47 | "lewton", 48 | ] 49 | 50 | [[package]] 51 | name = "autocfg" 52 | version = "1.4.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.3.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 61 | 62 | [[package]] 63 | name = "bitflags" 64 | version = "2.9.1" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 67 | 68 | [[package]] 69 | name = "bumpalo" 70 | version = "3.17.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 73 | 74 | [[package]] 75 | name = "bytemuck" 76 | version = "1.23.0" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 79 | 80 | [[package]] 81 | name = "byteorder" 82 | version = "1.5.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 85 | 86 | [[package]] 87 | name = "cc" 88 | version = "1.2.32" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" 91 | dependencies = [ 92 | "shlex", 93 | ] 94 | 95 | [[package]] 96 | name = "cfg-if" 97 | version = "1.0.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 100 | 101 | [[package]] 102 | name = "cfg_aliases" 103 | version = "0.2.1" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 106 | 107 | [[package]] 108 | name = "chrono" 109 | version = "0.4.41" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 112 | dependencies = [ 113 | "android-tzdata", 114 | "iana-time-zone", 115 | "js-sys", 116 | "num-traits", 117 | "serde", 118 | "wasm-bindgen", 119 | "windows-link", 120 | ] 121 | 122 | [[package]] 123 | name = "color_quant" 124 | version = "1.1.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 127 | 128 | [[package]] 129 | name = "core-foundation" 130 | version = "0.9.4" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 133 | dependencies = [ 134 | "core-foundation-sys", 135 | "libc", 136 | ] 137 | 138 | [[package]] 139 | name = "core-foundation-sys" 140 | version = "0.8.7" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 143 | 144 | [[package]] 145 | name = "crc32fast" 146 | version = "1.4.2" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 149 | dependencies = [ 150 | "cfg-if", 151 | ] 152 | 153 | [[package]] 154 | name = "crossbeam-deque" 155 | version = "0.8.6" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 158 | dependencies = [ 159 | "crossbeam-epoch", 160 | "crossbeam-utils", 161 | ] 162 | 163 | [[package]] 164 | name = "crossbeam-epoch" 165 | version = "0.9.18" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 168 | dependencies = [ 169 | "crossbeam-utils", 170 | ] 171 | 172 | [[package]] 173 | name = "crossbeam-utils" 174 | version = "0.8.21" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 177 | 178 | [[package]] 179 | name = "dasp_frame" 180 | version = "0.11.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" 183 | dependencies = [ 184 | "dasp_sample", 185 | ] 186 | 187 | [[package]] 188 | name = "dasp_sample" 189 | version = "0.11.0" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 192 | 193 | [[package]] 194 | name = "dirs" 195 | version = "5.0.1" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" 198 | dependencies = [ 199 | "dirs-sys", 200 | ] 201 | 202 | [[package]] 203 | name = "dirs-sys" 204 | version = "0.4.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" 207 | dependencies = [ 208 | "libc", 209 | "option-ext", 210 | "redox_users", 211 | "windows-sys 0.48.0", 212 | ] 213 | 214 | [[package]] 215 | name = "either" 216 | version = "1.15.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 219 | 220 | [[package]] 221 | name = "equivalent" 222 | version = "1.0.2" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 225 | 226 | [[package]] 227 | name = "errno" 228 | version = "0.3.12" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 231 | dependencies = [ 232 | "libc", 233 | "windows-sys 0.59.0", 234 | ] 235 | 236 | [[package]] 237 | name = "fdeflate" 238 | version = "0.3.7" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 241 | dependencies = [ 242 | "simd-adler32", 243 | ] 244 | 245 | [[package]] 246 | name = "filetime" 247 | version = "0.2.25" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 250 | dependencies = [ 251 | "cfg-if", 252 | "libc", 253 | "libredox", 254 | "windows-sys 0.59.0", 255 | ] 256 | 257 | [[package]] 258 | name = "flate2" 259 | version = "1.1.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 262 | dependencies = [ 263 | "crc32fast", 264 | "miniz_oxide", 265 | ] 266 | 267 | [[package]] 268 | name = "fnv" 269 | version = "1.0.7" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 272 | 273 | [[package]] 274 | name = "foldhash" 275 | version = "0.1.5" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 278 | 279 | [[package]] 280 | name = "fontdue" 281 | version = "0.9.3" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" 284 | dependencies = [ 285 | "hashbrown", 286 | "ttf-parser", 287 | ] 288 | 289 | [[package]] 290 | name = "futures" 291 | version = "0.3.31" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 294 | dependencies = [ 295 | "futures-channel", 296 | "futures-core", 297 | "futures-executor", 298 | "futures-io", 299 | "futures-sink", 300 | "futures-task", 301 | "futures-util", 302 | ] 303 | 304 | [[package]] 305 | name = "futures-channel" 306 | version = "0.3.31" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 309 | dependencies = [ 310 | "futures-core", 311 | "futures-sink", 312 | ] 313 | 314 | [[package]] 315 | name = "futures-core" 316 | version = "0.3.31" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 319 | 320 | [[package]] 321 | name = "futures-executor" 322 | version = "0.3.31" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 325 | dependencies = [ 326 | "futures-core", 327 | "futures-task", 328 | "futures-util", 329 | ] 330 | 331 | [[package]] 332 | name = "futures-io" 333 | version = "0.3.31" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 336 | 337 | [[package]] 338 | name = "futures-macro" 339 | version = "0.3.31" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 342 | dependencies = [ 343 | "proc-macro2", 344 | "quote", 345 | "syn", 346 | ] 347 | 348 | [[package]] 349 | name = "futures-sink" 350 | version = "0.3.31" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 353 | 354 | [[package]] 355 | name = "futures-task" 356 | version = "0.3.31" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 359 | 360 | [[package]] 361 | name = "futures-util" 362 | version = "0.3.31" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 365 | dependencies = [ 366 | "futures-channel", 367 | "futures-core", 368 | "futures-io", 369 | "futures-macro", 370 | "futures-sink", 371 | "futures-task", 372 | "memchr", 373 | "pin-project-lite", 374 | "pin-utils", 375 | "slab", 376 | ] 377 | 378 | [[package]] 379 | name = "getrandom" 380 | version = "0.2.16" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 383 | dependencies = [ 384 | "cfg-if", 385 | "libc", 386 | "wasi", 387 | ] 388 | 389 | [[package]] 390 | name = "gilrs" 391 | version = "0.10.10" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "a556964c6d62458084356ce9770676f5104bd667e12e9a795691076e8a17c5cf" 394 | dependencies = [ 395 | "fnv", 396 | "gilrs-core", 397 | "log", 398 | "uuid", 399 | "vec_map", 400 | ] 401 | 402 | [[package]] 403 | name = "gilrs-core" 404 | version = "0.5.15" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "732dadc05170599ddec9a89653f10d7a2af54da9181b3fa6e2bd49907ec8f7e4" 407 | dependencies = [ 408 | "core-foundation", 409 | "inotify", 410 | "io-kit-sys", 411 | "js-sys", 412 | "libc", 413 | "libudev-sys", 414 | "log", 415 | "nix", 416 | "uuid", 417 | "vec_map", 418 | "wasm-bindgen", 419 | "web-sys", 420 | "windows 0.58.0", 421 | ] 422 | 423 | [[package]] 424 | name = "glam" 425 | version = "0.27.0" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9" 428 | 429 | [[package]] 430 | name = "hashbrown" 431 | version = "0.15.3" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 434 | dependencies = [ 435 | "allocator-api2", 436 | "equivalent", 437 | "foldhash", 438 | ] 439 | 440 | [[package]] 441 | name = "hound" 442 | version = "3.5.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" 445 | 446 | [[package]] 447 | name = "iana-time-zone" 448 | version = "0.1.63" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 451 | dependencies = [ 452 | "android_system_properties", 453 | "core-foundation-sys", 454 | "iana-time-zone-haiku", 455 | "js-sys", 456 | "log", 457 | "wasm-bindgen", 458 | "windows-core 0.58.0", 459 | ] 460 | 461 | [[package]] 462 | name = "iana-time-zone-haiku" 463 | version = "0.1.2" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 466 | dependencies = [ 467 | "cc", 468 | ] 469 | 470 | [[package]] 471 | name = "image" 472 | version = "0.24.9" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 475 | dependencies = [ 476 | "bytemuck", 477 | "byteorder", 478 | "color_quant", 479 | "num-traits", 480 | "png", 481 | ] 482 | 483 | [[package]] 484 | name = "inotify" 485 | version = "0.10.2" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" 488 | dependencies = [ 489 | "bitflags 1.3.2", 490 | "inotify-sys", 491 | "libc", 492 | ] 493 | 494 | [[package]] 495 | name = "inotify-sys" 496 | version = "0.1.5" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 499 | dependencies = [ 500 | "libc", 501 | ] 502 | 503 | [[package]] 504 | name = "io-kit-sys" 505 | version = "0.4.1" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" 508 | dependencies = [ 509 | "core-foundation-sys", 510 | "mach2", 511 | ] 512 | 513 | [[package]] 514 | name = "js-sys" 515 | version = "0.3.77" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 518 | dependencies = [ 519 | "once_cell", 520 | "wasm-bindgen", 521 | ] 522 | 523 | [[package]] 524 | name = "kazeta-bios" 525 | version = "0.1.0" 526 | dependencies = [ 527 | "chrono", 528 | "dirs", 529 | "futures", 530 | "gilrs", 531 | "macroquad", 532 | "sysinfo", 533 | "tar", 534 | "walkdir", 535 | "whoami", 536 | ] 537 | 538 | [[package]] 539 | name = "lewton" 540 | version = "0.9.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0" 543 | dependencies = [ 544 | "byteorder", 545 | "ogg", 546 | "smallvec", 547 | ] 548 | 549 | [[package]] 550 | name = "libc" 551 | version = "0.2.172" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 554 | 555 | [[package]] 556 | name = "libredox" 557 | version = "0.1.3" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 560 | dependencies = [ 561 | "bitflags 2.9.1", 562 | "libc", 563 | "redox_syscall", 564 | ] 565 | 566 | [[package]] 567 | name = "libudev-sys" 568 | version = "0.1.4" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" 571 | dependencies = [ 572 | "libc", 573 | "pkg-config", 574 | ] 575 | 576 | [[package]] 577 | name = "linux-raw-sys" 578 | version = "0.9.4" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 581 | 582 | [[package]] 583 | name = "log" 584 | version = "0.4.27" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 587 | 588 | [[package]] 589 | name = "mach2" 590 | version = "0.4.2" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 593 | dependencies = [ 594 | "libc", 595 | ] 596 | 597 | [[package]] 598 | name = "macroquad" 599 | version = "0.4.14" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "d2befbae373456143ef55aa93a73594d080adfb111dc32ec96a1123a3e4ff4ae" 602 | dependencies = [ 603 | "fontdue", 604 | "glam", 605 | "image", 606 | "macroquad_macro", 607 | "miniquad", 608 | "quad-rand", 609 | "quad-snd", 610 | ] 611 | 612 | [[package]] 613 | name = "macroquad_macro" 614 | version = "0.1.8" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "64b1d96218903768c1ce078b657c0d5965465c95a60d2682fd97443c9d2483dd" 617 | 618 | [[package]] 619 | name = "malloc_buf" 620 | version = "0.0.6" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 623 | dependencies = [ 624 | "libc", 625 | ] 626 | 627 | [[package]] 628 | name = "maybe-uninit" 629 | version = "2.0.0" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 632 | 633 | [[package]] 634 | name = "memchr" 635 | version = "2.7.4" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 638 | 639 | [[package]] 640 | name = "miniquad" 641 | version = "0.4.8" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "2fb3e758e46dbc45716a8a49ca9edc54b15bcca826277e80b1f690708f67f9e3" 644 | dependencies = [ 645 | "libc", 646 | "ndk-sys", 647 | "objc-rs", 648 | "winapi", 649 | ] 650 | 651 | [[package]] 652 | name = "miniz_oxide" 653 | version = "0.8.8" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 656 | dependencies = [ 657 | "adler2", 658 | "simd-adler32", 659 | ] 660 | 661 | [[package]] 662 | name = "ndk-sys" 663 | version = "0.2.2" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" 666 | 667 | [[package]] 668 | name = "nix" 669 | version = "0.29.0" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 672 | dependencies = [ 673 | "bitflags 2.9.1", 674 | "cfg-if", 675 | "cfg_aliases", 676 | "libc", 677 | ] 678 | 679 | [[package]] 680 | name = "ntapi" 681 | version = "0.4.1" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" 684 | dependencies = [ 685 | "winapi", 686 | ] 687 | 688 | [[package]] 689 | name = "num-traits" 690 | version = "0.2.19" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 693 | dependencies = [ 694 | "autocfg", 695 | ] 696 | 697 | [[package]] 698 | name = "objc-rs" 699 | version = "0.2.8" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8" 702 | dependencies = [ 703 | "malloc_buf", 704 | ] 705 | 706 | [[package]] 707 | name = "ogg" 708 | version = "0.7.1" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651" 711 | dependencies = [ 712 | "byteorder", 713 | ] 714 | 715 | [[package]] 716 | name = "once_cell" 717 | version = "1.21.3" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 720 | 721 | [[package]] 722 | name = "option-ext" 723 | version = "0.2.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 726 | 727 | [[package]] 728 | name = "pin-project-lite" 729 | version = "0.2.16" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 732 | 733 | [[package]] 734 | name = "pin-utils" 735 | version = "0.1.0" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 738 | 739 | [[package]] 740 | name = "pkg-config" 741 | version = "0.3.32" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 744 | 745 | [[package]] 746 | name = "png" 747 | version = "0.17.16" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 750 | dependencies = [ 751 | "bitflags 1.3.2", 752 | "crc32fast", 753 | "fdeflate", 754 | "flate2", 755 | "miniz_oxide", 756 | ] 757 | 758 | [[package]] 759 | name = "proc-macro2" 760 | version = "1.0.95" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 763 | dependencies = [ 764 | "unicode-ident", 765 | ] 766 | 767 | [[package]] 768 | name = "quad-alsa-sys" 769 | version = "0.3.2" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5" 772 | dependencies = [ 773 | "libc", 774 | ] 775 | 776 | [[package]] 777 | name = "quad-rand" 778 | version = "0.2.3" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" 781 | 782 | [[package]] 783 | name = "quad-snd" 784 | version = "0.2.8" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "cba0c4943fc67147fbe9d1eb731fb9e678bfc9d926507eebbbfe0103e154e5b0" 787 | dependencies = [ 788 | "audir-sles", 789 | "audrey", 790 | "libc", 791 | "quad-alsa-sys", 792 | "winapi", 793 | ] 794 | 795 | [[package]] 796 | name = "quote" 797 | version = "1.0.40" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 800 | dependencies = [ 801 | "proc-macro2", 802 | ] 803 | 804 | [[package]] 805 | name = "rayon" 806 | version = "1.10.0" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 809 | dependencies = [ 810 | "either", 811 | "rayon-core", 812 | ] 813 | 814 | [[package]] 815 | name = "rayon-core" 816 | version = "1.12.1" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 819 | dependencies = [ 820 | "crossbeam-deque", 821 | "crossbeam-utils", 822 | ] 823 | 824 | [[package]] 825 | name = "redox_syscall" 826 | version = "0.5.12" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 829 | dependencies = [ 830 | "bitflags 2.9.1", 831 | ] 832 | 833 | [[package]] 834 | name = "redox_users" 835 | version = "0.4.6" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 838 | dependencies = [ 839 | "getrandom", 840 | "libredox", 841 | "thiserror", 842 | ] 843 | 844 | [[package]] 845 | name = "rustix" 846 | version = "1.0.7" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 849 | dependencies = [ 850 | "bitflags 2.9.1", 851 | "errno", 852 | "libc", 853 | "linux-raw-sys", 854 | "windows-sys 0.59.0", 855 | ] 856 | 857 | [[package]] 858 | name = "rustversion" 859 | version = "1.0.20" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 862 | 863 | [[package]] 864 | name = "same-file" 865 | version = "1.0.6" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 868 | dependencies = [ 869 | "winapi-util", 870 | ] 871 | 872 | [[package]] 873 | name = "serde" 874 | version = "1.0.219" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 877 | dependencies = [ 878 | "serde_derive", 879 | ] 880 | 881 | [[package]] 882 | name = "serde_derive" 883 | version = "1.0.219" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 886 | dependencies = [ 887 | "proc-macro2", 888 | "quote", 889 | "syn", 890 | ] 891 | 892 | [[package]] 893 | name = "shlex" 894 | version = "1.3.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 897 | 898 | [[package]] 899 | name = "simd-adler32" 900 | version = "0.3.7" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 903 | 904 | [[package]] 905 | name = "slab" 906 | version = "0.4.9" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 909 | dependencies = [ 910 | "autocfg", 911 | ] 912 | 913 | [[package]] 914 | name = "smallvec" 915 | version = "0.6.14" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 918 | dependencies = [ 919 | "maybe-uninit", 920 | ] 921 | 922 | [[package]] 923 | name = "syn" 924 | version = "2.0.101" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 927 | dependencies = [ 928 | "proc-macro2", 929 | "quote", 930 | "unicode-ident", 931 | ] 932 | 933 | [[package]] 934 | name = "sysinfo" 935 | version = "0.30.13" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" 938 | dependencies = [ 939 | "cfg-if", 940 | "core-foundation-sys", 941 | "libc", 942 | "ntapi", 943 | "once_cell", 944 | "rayon", 945 | "windows 0.52.0", 946 | ] 947 | 948 | [[package]] 949 | name = "tar" 950 | version = "0.4.44" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" 953 | dependencies = [ 954 | "filetime", 955 | "libc", 956 | "xattr", 957 | ] 958 | 959 | [[package]] 960 | name = "thiserror" 961 | version = "1.0.69" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 964 | dependencies = [ 965 | "thiserror-impl", 966 | ] 967 | 968 | [[package]] 969 | name = "thiserror-impl" 970 | version = "1.0.69" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 973 | dependencies = [ 974 | "proc-macro2", 975 | "quote", 976 | "syn", 977 | ] 978 | 979 | [[package]] 980 | name = "ttf-parser" 981 | version = "0.21.1" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" 984 | 985 | [[package]] 986 | name = "unicode-ident" 987 | version = "1.0.18" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 990 | 991 | [[package]] 992 | name = "uuid" 993 | version = "1.16.0" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" 996 | 997 | [[package]] 998 | name = "vec_map" 999 | version = "0.8.2" 1000 | source = "registry+https://github.com/rust-lang/crates.io-index" 1001 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1002 | 1003 | [[package]] 1004 | name = "walkdir" 1005 | version = "2.5.0" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 1008 | dependencies = [ 1009 | "same-file", 1010 | "winapi-util", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "wasi" 1015 | version = "0.11.0+wasi-snapshot-preview1" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1018 | 1019 | [[package]] 1020 | name = "wasite" 1021 | version = "0.1.0" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 1024 | 1025 | [[package]] 1026 | name = "wasm-bindgen" 1027 | version = "0.2.100" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1030 | dependencies = [ 1031 | "cfg-if", 1032 | "once_cell", 1033 | "rustversion", 1034 | "wasm-bindgen-macro", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "wasm-bindgen-backend" 1039 | version = "0.2.100" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1042 | dependencies = [ 1043 | "bumpalo", 1044 | "log", 1045 | "proc-macro2", 1046 | "quote", 1047 | "syn", 1048 | "wasm-bindgen-shared", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "wasm-bindgen-macro" 1053 | version = "0.2.100" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1056 | dependencies = [ 1057 | "quote", 1058 | "wasm-bindgen-macro-support", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "wasm-bindgen-macro-support" 1063 | version = "0.2.100" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1066 | dependencies = [ 1067 | "proc-macro2", 1068 | "quote", 1069 | "syn", 1070 | "wasm-bindgen-backend", 1071 | "wasm-bindgen-shared", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "wasm-bindgen-shared" 1076 | version = "0.2.100" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1079 | dependencies = [ 1080 | "unicode-ident", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "web-sys" 1085 | version = "0.3.77" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 1088 | dependencies = [ 1089 | "js-sys", 1090 | "wasm-bindgen", 1091 | ] 1092 | 1093 | [[package]] 1094 | name = "whoami" 1095 | version = "1.6.0" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" 1098 | dependencies = [ 1099 | "redox_syscall", 1100 | "wasite", 1101 | "web-sys", 1102 | ] 1103 | 1104 | [[package]] 1105 | name = "winapi" 1106 | version = "0.3.9" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1109 | dependencies = [ 1110 | "winapi-i686-pc-windows-gnu", 1111 | "winapi-x86_64-pc-windows-gnu", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "winapi-i686-pc-windows-gnu" 1116 | version = "0.4.0" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1119 | 1120 | [[package]] 1121 | name = "winapi-util" 1122 | version = "0.1.9" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1125 | dependencies = [ 1126 | "windows-sys 0.59.0", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "winapi-x86_64-pc-windows-gnu" 1131 | version = "0.4.0" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1134 | 1135 | [[package]] 1136 | name = "windows" 1137 | version = "0.52.0" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 1140 | dependencies = [ 1141 | "windows-core 0.52.0", 1142 | "windows-targets 0.52.6", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "windows" 1147 | version = "0.58.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 1150 | dependencies = [ 1151 | "windows-core 0.58.0", 1152 | "windows-targets 0.52.6", 1153 | ] 1154 | 1155 | [[package]] 1156 | name = "windows-core" 1157 | version = "0.52.0" 1158 | source = "registry+https://github.com/rust-lang/crates.io-index" 1159 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1160 | dependencies = [ 1161 | "windows-targets 0.52.6", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "windows-core" 1166 | version = "0.58.0" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" 1169 | dependencies = [ 1170 | "windows-implement", 1171 | "windows-interface", 1172 | "windows-result", 1173 | "windows-strings", 1174 | "windows-targets 0.52.6", 1175 | ] 1176 | 1177 | [[package]] 1178 | name = "windows-implement" 1179 | version = "0.58.0" 1180 | source = "registry+https://github.com/rust-lang/crates.io-index" 1181 | checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" 1182 | dependencies = [ 1183 | "proc-macro2", 1184 | "quote", 1185 | "syn", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "windows-interface" 1190 | version = "0.58.0" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" 1193 | dependencies = [ 1194 | "proc-macro2", 1195 | "quote", 1196 | "syn", 1197 | ] 1198 | 1199 | [[package]] 1200 | name = "windows-link" 1201 | version = "0.1.3" 1202 | source = "registry+https://github.com/rust-lang/crates.io-index" 1203 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1204 | 1205 | [[package]] 1206 | name = "windows-result" 1207 | version = "0.2.0" 1208 | source = "registry+https://github.com/rust-lang/crates.io-index" 1209 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 1210 | dependencies = [ 1211 | "windows-targets 0.52.6", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "windows-strings" 1216 | version = "0.1.0" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 1219 | dependencies = [ 1220 | "windows-result", 1221 | "windows-targets 0.52.6", 1222 | ] 1223 | 1224 | [[package]] 1225 | name = "windows-sys" 1226 | version = "0.48.0" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1229 | dependencies = [ 1230 | "windows-targets 0.48.5", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "windows-sys" 1235 | version = "0.59.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1238 | dependencies = [ 1239 | "windows-targets 0.52.6", 1240 | ] 1241 | 1242 | [[package]] 1243 | name = "windows-targets" 1244 | version = "0.48.5" 1245 | source = "registry+https://github.com/rust-lang/crates.io-index" 1246 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1247 | dependencies = [ 1248 | "windows_aarch64_gnullvm 0.48.5", 1249 | "windows_aarch64_msvc 0.48.5", 1250 | "windows_i686_gnu 0.48.5", 1251 | "windows_i686_msvc 0.48.5", 1252 | "windows_x86_64_gnu 0.48.5", 1253 | "windows_x86_64_gnullvm 0.48.5", 1254 | "windows_x86_64_msvc 0.48.5", 1255 | ] 1256 | 1257 | [[package]] 1258 | name = "windows-targets" 1259 | version = "0.52.6" 1260 | source = "registry+https://github.com/rust-lang/crates.io-index" 1261 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1262 | dependencies = [ 1263 | "windows_aarch64_gnullvm 0.52.6", 1264 | "windows_aarch64_msvc 0.52.6", 1265 | "windows_i686_gnu 0.52.6", 1266 | "windows_i686_gnullvm", 1267 | "windows_i686_msvc 0.52.6", 1268 | "windows_x86_64_gnu 0.52.6", 1269 | "windows_x86_64_gnullvm 0.52.6", 1270 | "windows_x86_64_msvc 0.52.6", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "windows_aarch64_gnullvm" 1275 | version = "0.48.5" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1278 | 1279 | [[package]] 1280 | name = "windows_aarch64_gnullvm" 1281 | version = "0.52.6" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1284 | 1285 | [[package]] 1286 | name = "windows_aarch64_msvc" 1287 | version = "0.48.5" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1290 | 1291 | [[package]] 1292 | name = "windows_aarch64_msvc" 1293 | version = "0.52.6" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1296 | 1297 | [[package]] 1298 | name = "windows_i686_gnu" 1299 | version = "0.48.5" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1302 | 1303 | [[package]] 1304 | name = "windows_i686_gnu" 1305 | version = "0.52.6" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1308 | 1309 | [[package]] 1310 | name = "windows_i686_gnullvm" 1311 | version = "0.52.6" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1314 | 1315 | [[package]] 1316 | name = "windows_i686_msvc" 1317 | version = "0.48.5" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1320 | 1321 | [[package]] 1322 | name = "windows_i686_msvc" 1323 | version = "0.52.6" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1326 | 1327 | [[package]] 1328 | name = "windows_x86_64_gnu" 1329 | version = "0.48.5" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1332 | 1333 | [[package]] 1334 | name = "windows_x86_64_gnu" 1335 | version = "0.52.6" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1338 | 1339 | [[package]] 1340 | name = "windows_x86_64_gnullvm" 1341 | version = "0.48.5" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1344 | 1345 | [[package]] 1346 | name = "windows_x86_64_gnullvm" 1347 | version = "0.52.6" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1350 | 1351 | [[package]] 1352 | name = "windows_x86_64_msvc" 1353 | version = "0.48.5" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1356 | 1357 | [[package]] 1358 | name = "windows_x86_64_msvc" 1359 | version = "0.52.6" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1362 | 1363 | [[package]] 1364 | name = "xattr" 1365 | version = "1.5.0" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" 1368 | dependencies = [ 1369 | "libc", 1370 | "rustix", 1371 | ] 1372 | -------------------------------------------------------------------------------- /bios/src/main.rs: -------------------------------------------------------------------------------- 1 | use macroquad::{audio, prelude::*}; 2 | use std::sync::{Arc, Mutex}; 3 | use std::thread; 4 | use std::time; 5 | use std::collections::HashMap; 6 | use gilrs::{Gilrs, Button, Axis}; 7 | use std::panic; 8 | use futures; 9 | use std::sync::atomic::{AtomicU16, Ordering, AtomicBool}; 10 | use std::fs; 11 | use std::path::Path; 12 | use std::process; 13 | 14 | mod save; 15 | 16 | const SCREEN_WIDTH: i32 = 640; 17 | const SCREEN_HEIGHT: i32 = 360; 18 | const TILE_SIZE: f32 = 32.0; 19 | const PADDING: f32 = 16.0; 20 | const FONT_SIZE: u16 = 16; 21 | const GRID_OFFSET: f32 = 52.0; 22 | const GRID_WIDTH: usize = 13; 23 | const GRID_HEIGHT: usize = 5; 24 | const UI_BG_COLOR: Color = Color {r: 0.0, g: 0.0, b: 0.0, a: 0.5 }; 25 | const UI_BG_COLOR_DARK: Color = Color {r: 0.0, g: 0.0, b: 0.0, a: 0.3 }; 26 | const UI_BG_COLOR_DIALOG: Color = Color {r: 0.0, g: 0.0, b: 0.0, a: 0.8 }; 27 | const SELECTED_OFFSET: f32 = 5.0; 28 | 29 | struct SoundEffects { 30 | cursor_move: audio::Sound, 31 | select: audio::Sound, 32 | reject: audio::Sound, 33 | back: audio::Sound, 34 | } 35 | 36 | impl SoundEffects { 37 | async fn new() -> Self { 38 | SoundEffects { 39 | cursor_move: audio::load_sound_from_bytes(include_bytes!("../move.wav")).await.unwrap(), 40 | select: audio::load_sound_from_bytes(include_bytes!("../select.wav")).await.unwrap(), 41 | reject: audio::load_sound_from_bytes(include_bytes!("../reject.wav")).await.unwrap(), 42 | back: audio::load_sound_from_bytes(include_bytes!("../back.wav")).await.unwrap(), 43 | } 44 | } 45 | 46 | fn play_cursor_move(&self) { 47 | audio::play_sound(&self.cursor_move, audio::PlaySoundParams { 48 | looped: false, 49 | volume: 0.25, 50 | }); 51 | } 52 | 53 | fn play_select(&self) { 54 | audio::play_sound(&self.select, audio::PlaySoundParams { 55 | looped: false, 56 | volume: 0.75, 57 | }); 58 | } 59 | 60 | fn play_reject(&self) { 61 | audio::play_sound(&self.reject, audio::PlaySoundParams { 62 | looped: false, 63 | volume: 0.75, 64 | }); 65 | } 66 | 67 | fn play_back(&self) { 68 | audio::play_sound(&self.back, audio::PlaySoundParams { 69 | looped: false, 70 | volume: 0.5, 71 | }); 72 | } 73 | } 74 | 75 | fn window_conf() -> Conf { 76 | Conf { 77 | window_title: "kazeta-bios".to_owned(), 78 | window_resizable: false, 79 | window_width: SCREEN_WIDTH, 80 | window_height: SCREEN_HEIGHT, 81 | high_dpi: false, 82 | fullscreen: false, 83 | 84 | ..Default::default() 85 | } 86 | } 87 | 88 | #[derive(Clone, Debug)] 89 | struct Memory { 90 | id: String, 91 | name: Option, 92 | drive_name: String, // Store which drive this save is on 93 | } 94 | 95 | #[derive(Clone, Debug)] 96 | struct StorageMedia { 97 | id: String, 98 | free: u32, 99 | } 100 | 101 | struct DialogOption { 102 | text: String, 103 | value: String, 104 | disabled: bool, 105 | } 106 | 107 | struct Dialog { 108 | id: String, 109 | desc: Option, 110 | options: Vec, 111 | selection: usize, 112 | } 113 | 114 | struct CopyOperationState { 115 | progress: u16, 116 | running: bool, 117 | should_clear_dialogs: bool, 118 | error_message: Option, 119 | } 120 | 121 | struct DrawContext { 122 | font: Font, 123 | } 124 | 125 | #[derive(Clone, Debug, PartialEq)] 126 | enum Screen { 127 | MainMenu, 128 | SaveData, 129 | FadingOut, 130 | } 131 | 132 | // UI Focus for Save Data Screen 133 | #[derive(Clone, Debug, PartialEq)] 134 | enum UIFocus { 135 | Grid, 136 | StorageLeft, 137 | StorageRight, 138 | } 139 | 140 | #[derive(Clone, Debug, PartialEq)] 141 | enum ShakeTarget { 142 | None, 143 | LeftArrow, 144 | RightArrow, 145 | Dialog, 146 | PlayOption, 147 | } 148 | 149 | struct AnimationState { 150 | shake_time: f32, // Current shake animation time 151 | shake_target: ShakeTarget, // Which element is currently shaking 152 | cursor_animation_time: f32, // Time counter for cursor animations 153 | cursor_transition_time: f32, // Time counter for cursor transition animation 154 | dialog_transition_time: f32, // Time counter for dialog transition animation 155 | dialog_transition_progress: f32, // Progress of dialog transition (0.0 to 1.0) 156 | dialog_transition_start_pos: Vec2, // Starting position for icon transition 157 | dialog_transition_end_pos: Vec2, // Ending position for icon transition 158 | } 159 | 160 | impl AnimationState { 161 | const SHAKE_DURATION: f32 = 0.2; // Duration of shake animation in seconds 162 | const SHAKE_INTENSITY: f32 = 3.0; // How far the arrow shakes 163 | const CURSOR_ANIMATION_SPEED: f32 = 10.0; // Speed of cursor color animation 164 | const CURSOR_TRANSITION_DURATION: f32 = 0.15; // Duration of cursor transition animation 165 | const DIALOG_TRANSITION_DURATION: f32 = 0.4; // Duration of dialog transition animation 166 | 167 | fn new() -> Self { 168 | AnimationState { 169 | shake_time: 0.0, 170 | shake_target: ShakeTarget::None, 171 | cursor_animation_time: 0.0, 172 | cursor_transition_time: 0.0, 173 | dialog_transition_time: 0.0, 174 | dialog_transition_progress: 0.0, 175 | dialog_transition_start_pos: Vec2::ZERO, 176 | dialog_transition_end_pos: Vec2::ZERO, 177 | } 178 | } 179 | 180 | fn calculate_shake_offset(&self, target: ShakeTarget) -> f32 { 181 | if self.shake_target == target && self.shake_time > 0.0 { 182 | (self.shake_time / Self::SHAKE_DURATION * std::f32::consts::PI * 8.0).sin() * Self::SHAKE_INTENSITY 183 | } else { 184 | 0.0 185 | } 186 | } 187 | 188 | fn update_shake(&mut self, delta_time: f32) { 189 | // Update shake animation 190 | if self.shake_time > 0.0 { 191 | self.shake_time = (self.shake_time - delta_time).max(0.0); 192 | if self.shake_time <= 0.0 { 193 | self.shake_target = ShakeTarget::None; 194 | } 195 | } 196 | } 197 | 198 | fn update_cursor_animation(&mut self, delta_time: f32) { 199 | // Update cursor animation 200 | self.cursor_animation_time = (self.cursor_animation_time + delta_time * Self::CURSOR_ANIMATION_SPEED) % (2.0 * std::f32::consts::PI); 201 | // Update cursor transition 202 | if self.cursor_transition_time > 0.0 { 203 | self.cursor_transition_time = (self.cursor_transition_time - delta_time).max(0.0); 204 | } 205 | } 206 | 207 | fn trigger_shake(&mut self, is_left: bool) { 208 | if is_left { 209 | self.shake_target = ShakeTarget::LeftArrow; 210 | self.shake_time = Self::SHAKE_DURATION; 211 | } else { 212 | self.shake_target = ShakeTarget::RightArrow; 213 | self.shake_time = Self::SHAKE_DURATION; 214 | } 215 | } 216 | 217 | fn trigger_dialog_shake(&mut self) { 218 | self.shake_target = ShakeTarget::Dialog; 219 | self.shake_time = Self::SHAKE_DURATION; 220 | } 221 | 222 | fn trigger_play_option_shake(&mut self) { 223 | self.shake_target = ShakeTarget::PlayOption; 224 | self.shake_time = Self::SHAKE_DURATION; 225 | } 226 | 227 | fn trigger_transition(&mut self) { 228 | self.cursor_transition_time = Self::CURSOR_TRANSITION_DURATION; 229 | } 230 | 231 | fn get_cursor_color(&self) -> Color { 232 | let c = (self.cursor_animation_time.sin() * 0.5 + 0.5).max(0.3); 233 | Color { r: c, g: c, b: c, a: c } 234 | } 235 | 236 | fn get_cursor_scale(&self) -> f32 { 237 | if self.cursor_transition_time > 0.0 { 238 | let t = self.cursor_transition_time / Self::CURSOR_TRANSITION_DURATION; 239 | // Start at 1.5x size and smoothly transition to 1.0x 240 | 1.0 + 0.5 * t 241 | } else { 242 | 1.0 243 | } 244 | } 245 | 246 | fn update_dialog_transition(&mut self, delta_time: f32) { 247 | if self.dialog_transition_time > 0.0 { 248 | self.dialog_transition_time = (self.dialog_transition_time - delta_time).max(0.0); 249 | self.dialog_transition_progress = 1.0 - (self.dialog_transition_time / Self::DIALOG_TRANSITION_DURATION); 250 | } 251 | } 252 | 253 | fn trigger_dialog_transition(&mut self, start_pos: Vec2, end_pos: Vec2) { 254 | self.dialog_transition_time = Self::DIALOG_TRANSITION_DURATION; 255 | self.dialog_transition_progress = 0.0; 256 | self.dialog_transition_start_pos = start_pos; 257 | self.dialog_transition_end_pos = end_pos; 258 | } 259 | 260 | fn get_dialog_transition_pos(&self) -> Vec2 { 261 | let t = self.dialog_transition_progress; 262 | // Use smooth easing function 263 | let t = t * t * (3.0 - 2.0 * t); 264 | self.dialog_transition_start_pos.lerp(self.dialog_transition_end_pos, t) 265 | } 266 | } 267 | 268 | struct InputState { 269 | up: bool, 270 | down: bool, 271 | left: bool, 272 | right: bool, 273 | select: bool, 274 | next: bool, 275 | prev: bool, 276 | cycle: bool, 277 | back: bool, 278 | analog_was_neutral: bool, 279 | ui_focus: UIFocus, 280 | } 281 | 282 | impl InputState { 283 | const ANALOG_DEADZONE: f32 = 0.5; // Increased deadzone for less sensitivity 284 | 285 | fn new() -> Self { 286 | InputState { 287 | up: false, 288 | down: false, 289 | left: false, 290 | right: false, 291 | select: false, 292 | next: false, 293 | prev: false, 294 | cycle: false, 295 | back: false, 296 | analog_was_neutral: true, 297 | ui_focus: UIFocus::Grid, 298 | } 299 | } 300 | 301 | fn update_keyboard(&mut self) { 302 | self.up = is_key_pressed(KeyCode::Up); 303 | self.down = is_key_pressed(KeyCode::Down); 304 | self.left = is_key_pressed(KeyCode::Left); 305 | self.right = is_key_pressed(KeyCode::Right); 306 | self.select = is_key_pressed(KeyCode::Enter); 307 | self.next = is_key_pressed(KeyCode::RightBracket); 308 | self.prev = is_key_pressed(KeyCode::LeftBracket); 309 | self.back = is_key_pressed(KeyCode::Backspace); 310 | self.cycle = is_key_pressed(KeyCode::Tab); 311 | } 312 | 313 | fn update_controller(&mut self, gilrs: &mut Gilrs) { 314 | // Handle button events 315 | while let Some(ev) = gilrs.next_event() { 316 | match ev.event { 317 | gilrs::EventType::ButtonPressed(Button::DPadUp, _) => self.up = true, 318 | gilrs::EventType::ButtonReleased(Button::DPadUp, _) => self.up = false, 319 | gilrs::EventType::ButtonPressed(Button::DPadDown, _) => self.down = true, 320 | gilrs::EventType::ButtonReleased(Button::DPadDown, _) => self.down = false, 321 | gilrs::EventType::ButtonPressed(Button::DPadLeft, _) => self.left = true, 322 | gilrs::EventType::ButtonReleased(Button::DPadLeft, _) => self.left = false, 323 | gilrs::EventType::ButtonPressed(Button::DPadRight, _) => self.right = true, 324 | gilrs::EventType::ButtonReleased(Button::DPadRight, _) => self.right = false, 325 | gilrs::EventType::ButtonPressed(Button::South, _) => self.select = true, 326 | gilrs::EventType::ButtonReleased(Button::South, _) => self.select = false, 327 | gilrs::EventType::ButtonPressed(Button::RightTrigger, _) => self.next = true, 328 | gilrs::EventType::ButtonReleased(Button::RightTrigger, _) => self.next = false, 329 | gilrs::EventType::ButtonPressed(Button::LeftTrigger, _) => self.prev = true, 330 | gilrs::EventType::ButtonReleased(Button::LeftTrigger, _) => self.prev = false, 331 | gilrs::EventType::ButtonPressed(Button::East, _) => self.back = true, 332 | gilrs::EventType::ButtonReleased(Button::East, _) => self.back = false, 333 | _ => {} 334 | } 335 | } 336 | 337 | // Handle analog stick input 338 | for (_, gamepad) in gilrs.gamepads() { 339 | let x = gamepad.value(Axis::LeftStickX); 340 | let y = gamepad.value(Axis::LeftStickY); 341 | 342 | // Apply deadzone to analog values 343 | let apply_deadzone = |value: f32| { 344 | if value.abs() < Self::ANALOG_DEADZONE { 345 | 0.0 346 | } else { 347 | value 348 | } 349 | }; 350 | 351 | let x = apply_deadzone(x); 352 | let y = apply_deadzone(y); 353 | 354 | // Check if stick is in neutral position 355 | let is_neutral = x.abs() < Self::ANALOG_DEADZONE && y.abs() < Self::ANALOG_DEADZONE; 356 | 357 | // Only trigger movement if stick was in neutral position last frame 358 | if self.analog_was_neutral { 359 | self.up = self.up || y > Self::ANALOG_DEADZONE; 360 | self.down = self.down || y < -Self::ANALOG_DEADZONE; 361 | self.left = self.left || x < -Self::ANALOG_DEADZONE; 362 | self.right = self.right || x > Self::ANALOG_DEADZONE; 363 | } 364 | 365 | // Update neutral state for next frame 366 | self.analog_was_neutral = is_neutral; 367 | } 368 | } 369 | } 370 | 371 | fn pixel_pos(v: f32) -> f32 { 372 | PADDING + v*TILE_SIZE + v*PADDING 373 | } 374 | 375 | fn copy_memory(memory: &Memory, from_media: &StorageMedia, to_media: &StorageMedia, state: Arc>) { 376 | // Initialize the copy operation state 377 | if let Ok(mut copy_state) = state.lock() { 378 | copy_state.progress = 0; 379 | copy_state.running = true; 380 | copy_state.error_message = None; 381 | } 382 | 383 | // Small delay to show the operation has started 384 | thread::sleep(time::Duration::from_millis(500)); 385 | 386 | // Create progress tracking 387 | let progress = Arc::new(AtomicU16::new(0)); 388 | let progress_clone = progress.clone(); 389 | let state_clone = state.clone(); 390 | 391 | // Spawn a thread to monitor progress from the copy operation 392 | let monitor_handle = thread::spawn(move || { 393 | loop { 394 | let current_progress = progress_clone.load(Ordering::SeqCst); 395 | 396 | // Update the UI state with the current progress 397 | if let Ok(mut copy_state) = state_clone.lock() { 398 | // Only update if the operation is still running 399 | if copy_state.running { 400 | copy_state.progress = current_progress; 401 | } else { 402 | // Operation completed, exit the monitoring loop 403 | break; 404 | } 405 | } 406 | 407 | // If we've reached 100%, the copy operation should be finishing soon 408 | if current_progress >= 100 { 409 | break; 410 | } 411 | 412 | thread::sleep(time::Duration::from_millis(50)); 413 | } 414 | }); 415 | 416 | // Perform the actual copy operation 417 | let copy_result = save::copy_save(&memory.id, &from_media.id, &to_media.id, progress); 418 | 419 | // Handle the result 420 | match copy_result { 421 | Ok(_) => { 422 | // Ensure progress shows 100% on success 423 | if let Ok(mut copy_state) = state.lock() { 424 | copy_state.progress = 100; 425 | } 426 | 427 | // Pause for 1.5 seconds to show completion clearly while keeping the operation running 428 | thread::sleep(time::Duration::from_millis(1500)); 429 | 430 | // Mark operation as complete (this will allow the monitoring thread to exit) 431 | if let Ok(mut copy_state) = state.lock() { 432 | copy_state.running = false; 433 | copy_state.should_clear_dialogs = true; 434 | } 435 | 436 | // Wait for the monitoring thread to finish 437 | monitor_handle.join().ok(); 438 | }, 439 | Err(e) => { 440 | // Handle error case (this will also stop the monitoring thread) 441 | if let Ok(mut copy_state) = state.lock() { 442 | copy_state.running = false; 443 | copy_state.should_clear_dialogs = true; 444 | copy_state.error_message = Some(format!("Failed to copy save: {}", e)); 445 | } 446 | 447 | // Wait for the monitoring thread to finish 448 | monitor_handle.join().ok(); 449 | } 450 | } 451 | } 452 | 453 | async fn load_memories(media: &StorageMedia, cache: &mut HashMap, queue: &mut Vec<(String, String)>) -> Vec { 454 | let mut memories = Vec::new(); 455 | 456 | if let Ok(details) = save::get_save_details(&media.id) { 457 | for (cart_id, name, icon_path) in details { 458 | if !cache.contains_key(&cart_id) { 459 | queue.push((cart_id.clone(), icon_path.clone())); 460 | } 461 | 462 | let m = Memory { 463 | id: cart_id, 464 | name: Some(name), 465 | drive_name: media.id.clone(), 466 | }; 467 | memories.push(m); 468 | } 469 | } 470 | 471 | memories 472 | } 473 | 474 | fn text(ctx : &DrawContext, text : &str, x : f32, y: f32) { 475 | draw_text_ex(&text.to_uppercase(), x+1.0, y+1.0, TextParams { 476 | font: Some(&ctx.font), 477 | font_size: FONT_SIZE, 478 | color: Color {r:0.0, g:0.0, b:0.0, a:0.9}, 479 | ..Default::default() 480 | }); 481 | draw_text_ex(&text.to_uppercase(), x, y, TextParams { 482 | font: Some(&ctx.font), 483 | font_size: FONT_SIZE, 484 | ..Default::default() 485 | }); 486 | } 487 | 488 | fn text_disabled(ctx : &DrawContext, text : &str, x : f32, y: f32) { 489 | draw_text_ex(&text.to_uppercase(), x+1.0, y+1.0, TextParams { 490 | font: Some(&ctx.font), 491 | font_size: FONT_SIZE, 492 | color: Color {r:0.0, g:0.0, b:0.0, a:1.0}, 493 | ..Default::default() 494 | }); 495 | draw_text_ex(&text.to_uppercase(), x, y, TextParams { 496 | font: Some(&ctx.font), 497 | font_size: FONT_SIZE, 498 | color: Color {r:0.4, g:0.4, b:0.4, a:1.0}, 499 | ..Default::default() 500 | }); 501 | } 502 | 503 | #[derive(Clone, Debug)] 504 | struct StorageMediaState { 505 | 506 | // all storage media, including disabled media 507 | all_media: Vec, 508 | 509 | // media that can actually be used 510 | media: Vec, 511 | 512 | // the index of selection in 'media' 513 | selected: usize, 514 | 515 | needs_memory_refresh: bool, 516 | } 517 | 518 | impl StorageMediaState { 519 | fn new() -> Self { 520 | StorageMediaState { 521 | all_media: Vec::new(), 522 | media: Vec::new(), 523 | selected: 0, 524 | needs_memory_refresh: false, 525 | } 526 | } 527 | 528 | fn update_media(&mut self) { 529 | let mut all_new_media = Vec::new(); 530 | 531 | if let Ok(devices) = save::list_devices() { 532 | for (id, free) in devices { 533 | all_new_media.push(StorageMedia { 534 | id, 535 | free, 536 | }); 537 | } 538 | } 539 | 540 | // Done if media list has not changed 541 | if self.all_media.len() == all_new_media.len() && 542 | !self.all_media.iter().zip(all_new_media.iter()).any(|(a, b)| a.id != b.id) { 543 | 544 | // update free space 545 | self.all_media = all_new_media; 546 | for media in &mut self.media { 547 | if let Some(pos) = self.all_media.iter().position(|m| m.id == media.id) { 548 | media.free = self.all_media.get(pos).unwrap().free 549 | } 550 | } 551 | 552 | return; 553 | } 554 | 555 | let new_media: Vec = all_new_media 556 | .clone() 557 | .into_iter() 558 | .filter(|m| save::has_save_dir(&m.id) && !save::is_cart(&m.id)) 559 | .collect(); 560 | 561 | // Try to keep the same device selected if it still exists 562 | let mut new_pos = 0; 563 | if let Some(old_selected_media) = self.media.get(self.selected) { 564 | if let Some(pos) = new_media.iter().position(|m| m.id == old_selected_media.id) { 565 | new_pos = pos; 566 | } 567 | } 568 | 569 | self.all_media = all_new_media; 570 | self.media = new_media; 571 | self.selected = new_pos; 572 | self.needs_memory_refresh = true; 573 | } 574 | } 575 | 576 | /// Get playtime for a specific game, using cache when available 577 | fn get_game_playtime(memory: &Memory, playtime_cache: &mut PlaytimeCache) -> f32 { 578 | let cache_key = (memory.id.clone(), memory.drive_name.clone()); 579 | 580 | if let Some(&cached_playtime) = playtime_cache.get(&cache_key) { 581 | cached_playtime 582 | } else { 583 | let calculated_playtime = save::calculate_playtime(&memory.id, &memory.drive_name); 584 | playtime_cache.insert(cache_key, calculated_playtime); 585 | calculated_playtime 586 | } 587 | } 588 | 589 | /// Get size for a specific game, using cache when available 590 | fn get_game_size(memory: &Memory, size_cache: &mut SizeCache) -> f32 { 591 | let cache_key = (memory.id.clone(), memory.drive_name.clone()); 592 | 593 | if let Some(&cached_size) = size_cache.get(&cache_key) { 594 | cached_size 595 | } else { 596 | let calculated_size = save::calculate_save_size(&memory.id, &memory.drive_name); 597 | size_cache.insert(cache_key, calculated_size); 598 | calculated_size 599 | } 600 | } 601 | 602 | // Playtime cache to avoid recalculating playtime for the same game on the same drive 603 | type PlaytimeCacheKey = (String, String); // (cart_id, drive_name) 604 | type PlaytimeCache = HashMap; 605 | 606 | // Size cache to avoid recalculating size for the same game on the same drive 607 | type SizeCacheKey = (String, String); // (cart_id, drive_name) 608 | type SizeCache = HashMap; 609 | 610 | fn get_memory_index(selected_memory: usize, scroll_offset: usize) -> usize { 611 | selected_memory + GRID_WIDTH * scroll_offset 612 | } 613 | 614 | fn calculate_icon_transition_positions(selected_memory: usize) -> (Vec2, Vec2) { 615 | let xp = (selected_memory % GRID_WIDTH) as f32; 616 | let yp = (selected_memory / GRID_WIDTH) as f32; 617 | let grid_pos = Vec2::new( 618 | pixel_pos(xp), 619 | pixel_pos(yp) + GRID_OFFSET 620 | ); 621 | let dialog_pos = Vec2::new(PADDING, PADDING); 622 | (grid_pos, dialog_pos) 623 | } 624 | 625 | fn render_data_view( 626 | ctx: &DrawContext, 627 | selected_memory: usize, 628 | memories: &Vec, 629 | icon_cache: &HashMap, 630 | storage_state: &Arc>, 631 | placeholder: &Texture2D, 632 | scroll_offset: usize, 633 | input_state: &mut InputState, 634 | animation_state: &mut AnimationState, 635 | playtime_cache: &mut PlaytimeCache, 636 | size_cache: &mut SizeCache, 637 | ) { 638 | let xp = (selected_memory % GRID_WIDTH) as f32; 639 | let yp = (selected_memory / GRID_WIDTH) as f32; 640 | 641 | // Draw grid selection highlight when focused on grid 642 | if let UIFocus::Grid = input_state.ui_focus { 643 | let cursor_color = animation_state.get_cursor_color(); 644 | let cursor_thickness = 6.0; 645 | let cursor_scale = animation_state.get_cursor_scale(); 646 | 647 | let base_size = TILE_SIZE + 6.0; 648 | let scaled_size = base_size * cursor_scale; 649 | let offset = (scaled_size - base_size) / 2.0; 650 | 651 | draw_rectangle_lines( 652 | pixel_pos(xp)-3.0-SELECTED_OFFSET - offset, 653 | pixel_pos(yp)-3.0-SELECTED_OFFSET+GRID_OFFSET - offset, 654 | scaled_size, 655 | scaled_size, 656 | cursor_thickness, 657 | cursor_color 658 | ); 659 | } 660 | 661 | for x in 0..GRID_WIDTH { 662 | for y in 0..GRID_HEIGHT { 663 | let memory_index = get_memory_index(x + GRID_WIDTH * y, scroll_offset); 664 | 665 | if xp as usize == x && yp as usize == y { 666 | if let UIFocus::Grid = input_state.ui_focus { 667 | draw_rectangle(pixel_pos(x as f32)-SELECTED_OFFSET, pixel_pos(y as f32)-SELECTED_OFFSET+GRID_OFFSET, TILE_SIZE, TILE_SIZE, UI_BG_COLOR); 668 | } else { 669 | draw_rectangle(pixel_pos(x as f32)-2.0, pixel_pos(y as f32)+GRID_OFFSET-2.0, TILE_SIZE+4.0, TILE_SIZE+4.0, UI_BG_COLOR); 670 | } 671 | } else { 672 | draw_rectangle(pixel_pos(x as f32)-2.0, pixel_pos(y as f32)+GRID_OFFSET-2.0, TILE_SIZE+4.0, TILE_SIZE+4.0, UI_BG_COLOR); 673 | } 674 | 675 | let Some(mem) = memories.get(memory_index) else { 676 | continue; 677 | }; 678 | 679 | // Skip rendering the icon at its grid position during transitions 680 | if xp as usize == x && yp as usize == y && animation_state.dialog_transition_time > 0.0 { 681 | continue; 682 | } 683 | 684 | let icon = match icon_cache.get(&mem.id) { 685 | Some(icon) => icon, 686 | None => placeholder, 687 | }; 688 | 689 | let params = DrawTextureParams { 690 | dest_size: Some(Vec2 {x: TILE_SIZE, y: TILE_SIZE }), 691 | source: Some(Rect { x: 0.0, y: 0.0, h: icon.height(), w: icon.width() }), 692 | rotation: 0.0, 693 | flip_x: false, 694 | flip_y: false, 695 | pivot: None 696 | }; 697 | if xp as usize == x && yp as usize == y { 698 | if let UIFocus::Grid = input_state.ui_focus { 699 | draw_texture_ex(&icon, pixel_pos(x as f32)-SELECTED_OFFSET, pixel_pos(y as f32)-SELECTED_OFFSET+GRID_OFFSET, WHITE, params); 700 | } else { 701 | draw_texture_ex(&icon, pixel_pos(x as f32), pixel_pos(y as f32)+GRID_OFFSET, WHITE, params); 702 | } 703 | } else { 704 | draw_texture_ex(&icon, pixel_pos(x as f32), pixel_pos(y as f32)+GRID_OFFSET, WHITE, params); 705 | } 706 | } 707 | } 708 | 709 | // Storage media info area with navigation 710 | const STORAGE_INFO_WIDTH: f32 = 512.0; 711 | const STORAGE_INFO_X: f32 = TILE_SIZE*2.0; 712 | const STORAGE_INFO_Y: f32 = 16.0; 713 | const STORAGE_INFO_HEIGHT: f32 = 36.0; 714 | const NAV_ARROW_SIZE: f32 = 10.0; 715 | const NAV_ARROW_OUTLINE: f32 = 1.0; 716 | 717 | // Draw storage info background 718 | draw_rectangle(STORAGE_INFO_X, STORAGE_INFO_Y, STORAGE_INFO_WIDTH, STORAGE_INFO_HEIGHT, UI_BG_COLOR); 719 | draw_rectangle_lines(STORAGE_INFO_X-4.0, STORAGE_INFO_Y-4.0, STORAGE_INFO_WIDTH+8.0, STORAGE_INFO_HEIGHT+8.0, 4.0, UI_BG_COLOR_DARK); 720 | 721 | if let Ok(state) = storage_state.lock() { 722 | if !state.media.is_empty() { 723 | // Draw left arrow background 724 | let left_box_x = PADDING; // Align with leftmost grid column 725 | let left_box_y = STORAGE_INFO_Y + STORAGE_INFO_HEIGHT/2.0 - TILE_SIZE/2.0; 726 | let left_shake = animation_state.calculate_shake_offset(ShakeTarget::LeftArrow); 727 | 728 | if let UIFocus::StorageLeft = input_state.ui_focus { 729 | let cursor_color = animation_state.get_cursor_color(); 730 | let cursor_thickness = 6.0; 731 | let cursor_scale = animation_state.get_cursor_scale(); 732 | 733 | let base_size = TILE_SIZE + 6.0; 734 | let scaled_size = base_size * cursor_scale; 735 | let offset = (scaled_size - base_size) / 2.0; 736 | 737 | draw_rectangle(left_box_x-SELECTED_OFFSET + left_shake, left_box_y-SELECTED_OFFSET, TILE_SIZE, TILE_SIZE, UI_BG_COLOR); 738 | draw_rectangle_lines( 739 | left_box_x-3.0-SELECTED_OFFSET + left_shake - offset, 740 | left_box_y-3.0-SELECTED_OFFSET - offset, 741 | scaled_size, 742 | scaled_size, 743 | cursor_thickness, 744 | cursor_color 745 | ); 746 | } else { 747 | draw_rectangle(left_box_x-2.0 + left_shake, left_box_y-2.0, TILE_SIZE+4.0, TILE_SIZE+4.0, UI_BG_COLOR); 748 | } 749 | 750 | let left_offset = if let UIFocus::StorageLeft = input_state.ui_focus { 751 | SELECTED_OFFSET 752 | } else { 753 | 0.0 754 | }; 755 | 756 | let left_points = [ 757 | Vec2::new(4.0 + left_box_x + TILE_SIZE/2.0 - NAV_ARROW_SIZE - left_offset + left_shake, left_box_y + TILE_SIZE/2.0 - left_offset), 758 | Vec2::new(4.0 + left_box_x + TILE_SIZE/2.0 - left_offset + left_shake, left_box_y + TILE_SIZE/2.0 - NAV_ARROW_SIZE - left_offset), 759 | Vec2::new(4.0 + left_box_x + TILE_SIZE/2.0 - left_offset + left_shake, left_box_y + TILE_SIZE/2.0 + NAV_ARROW_SIZE - left_offset), 760 | ]; 761 | let left_color = if state.selected > 0 { 762 | WHITE 763 | } else { 764 | Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 } // Dark gray when disabled 765 | }; 766 | draw_triangle(left_points[0], left_points[1], left_points[2], left_color); 767 | draw_triangle_lines(left_points[0], left_points[1], left_points[2], NAV_ARROW_OUTLINE, BLACK); 768 | 769 | // Draw right arrow background 770 | let right_box_x = PADDING + (GRID_WIDTH as f32 - 1.0) * (TILE_SIZE + PADDING); // Align with rightmost grid column 771 | let right_box_y = STORAGE_INFO_Y + STORAGE_INFO_HEIGHT/2.0 - TILE_SIZE/2.0; 772 | let right_shake = animation_state.calculate_shake_offset(ShakeTarget::RightArrow); 773 | 774 | if let UIFocus::StorageRight = input_state.ui_focus { 775 | let cursor_color = animation_state.get_cursor_color(); 776 | let cursor_thickness = 6.0; 777 | let cursor_scale = animation_state.get_cursor_scale(); 778 | 779 | let base_size = TILE_SIZE + 6.0; 780 | let scaled_size = base_size * cursor_scale; 781 | let offset = (scaled_size - base_size) / 2.0; 782 | 783 | draw_rectangle(right_box_x-SELECTED_OFFSET + right_shake, right_box_y-SELECTED_OFFSET, TILE_SIZE, TILE_SIZE, UI_BG_COLOR); 784 | draw_rectangle_lines( 785 | right_box_x-3.0-SELECTED_OFFSET + right_shake - offset, 786 | right_box_y-3.0-SELECTED_OFFSET - offset, 787 | scaled_size, 788 | scaled_size, 789 | cursor_thickness, 790 | cursor_color 791 | ); 792 | } else { 793 | draw_rectangle(right_box_x-2.0 + right_shake, right_box_y-2.0, TILE_SIZE+4.0, TILE_SIZE+4.0, UI_BG_COLOR); 794 | } 795 | 796 | let right_offset = if let UIFocus::StorageRight = input_state.ui_focus { 797 | SELECTED_OFFSET 798 | } else { 799 | 0.0 800 | }; 801 | let right_points = [ 802 | Vec2::new(right_box_x + TILE_SIZE/2.0 + NAV_ARROW_SIZE - 4.0 - right_offset + right_shake, right_box_y + TILE_SIZE/2.0 - right_offset), 803 | Vec2::new(right_box_x + TILE_SIZE/2.0 - 4.0 - right_offset + right_shake, right_box_y + TILE_SIZE/2.0 - NAV_ARROW_SIZE - right_offset), 804 | Vec2::new(right_box_x + TILE_SIZE/2.0 - 4.0 - right_offset + right_shake, right_box_y + TILE_SIZE/2.0 + NAV_ARROW_SIZE - right_offset), 805 | ]; 806 | let right_color = if state.selected < state.media.len() - 1 { 807 | WHITE 808 | } else { 809 | Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 } // Dark gray when disabled 810 | }; 811 | draw_triangle(right_points[0], right_points[1], right_points[2], right_color); 812 | draw_triangle_lines(right_points[0], right_points[1], right_points[2], NAV_ARROW_OUTLINE, BLACK); 813 | 814 | // Draw storage info text 815 | text(&ctx, &state.media[state.selected].id, STORAGE_INFO_X + 2.0, STORAGE_INFO_Y + 17.0); 816 | text(&ctx, &format!("{} MB Free", state.media[state.selected].free as f32), STORAGE_INFO_X + 2.0, STORAGE_INFO_Y + 33.0); 817 | } 818 | } 819 | 820 | // Draw highlight box for save info 821 | draw_rectangle(16.0, 309.0, SCREEN_WIDTH as f32 - 32.0, 40.0, UI_BG_COLOR); 822 | draw_rectangle_lines(12.0, 305.0, SCREEN_WIDTH as f32 - 24.0, 48.0, 4.0, UI_BG_COLOR_DARK); 823 | 824 | let memory_index = get_memory_index(selected_memory, scroll_offset); 825 | if input_state.ui_focus == UIFocus::Grid { 826 | if let Some(selected_mem) = memories.get(memory_index) { 827 | let desc = match selected_mem.name.clone() { 828 | Some(name) => name, 829 | None => selected_mem.id.clone(), 830 | }; 831 | 832 | let playtime = get_game_playtime(selected_mem, playtime_cache); 833 | let size = get_game_size(selected_mem, size_cache); 834 | text(&ctx, &desc, 19.0, 327.0); 835 | text(&ctx, &format!("{:.1} MB | {:.1} H", size, playtime), 19.0, 345.0); 836 | } 837 | } 838 | 839 | // Draw scroll indicators last so they appear on top 840 | const SCROLL_INDICATOR_SIZE: f32 = 8.0; // Size from center to edge 841 | const SCROLL_INDICATOR_DISTANCE_TOP: f32 = -13.0; // Distance from grid edge 842 | const SCROLL_INDICATOR_DISTANCE_BOTTOM: f32 = 4.0; // Distance from grid edge 843 | const SCROLL_INDICATOR_OUTLINE: f32 = 1.0; // Outline thickness 844 | 845 | if scroll_offset > 0 { 846 | // Up arrow (pointing up) 847 | let points = [ 848 | Vec2::new(SCREEN_WIDTH as f32 / 2.0, GRID_OFFSET - SCROLL_INDICATOR_DISTANCE_TOP - SCROLL_INDICATOR_SIZE), 849 | Vec2::new(SCREEN_WIDTH as f32 / 2.0 - SCROLL_INDICATOR_SIZE, GRID_OFFSET - SCROLL_INDICATOR_DISTANCE_TOP), 850 | Vec2::new(SCREEN_WIDTH as f32 / 2.0 + SCROLL_INDICATOR_SIZE, GRID_OFFSET - SCROLL_INDICATOR_DISTANCE_TOP), 851 | ]; 852 | draw_triangle(points[0], points[1], points[2], WHITE); 853 | draw_triangle_lines(points[0], points[1], points[2], SCROLL_INDICATOR_OUTLINE, BLACK); 854 | } 855 | 856 | let next_row_start = get_memory_index(GRID_WIDTH * GRID_HEIGHT, scroll_offset); 857 | if next_row_start < memories.len() { 858 | // Down arrow (pointing down) 859 | let grid_bottom = GRID_OFFSET + GRID_HEIGHT as f32 * (TILE_SIZE + PADDING); 860 | let points = [ 861 | Vec2::new(SCREEN_WIDTH as f32 / 2.0, grid_bottom + SCROLL_INDICATOR_DISTANCE_BOTTOM + SCROLL_INDICATOR_SIZE), 862 | Vec2::new(SCREEN_WIDTH as f32 / 2.0 - SCROLL_INDICATOR_SIZE, grid_bottom + SCROLL_INDICATOR_DISTANCE_BOTTOM), 863 | Vec2::new(SCREEN_WIDTH as f32 / 2.0 + SCROLL_INDICATOR_SIZE, grid_bottom + SCROLL_INDICATOR_DISTANCE_BOTTOM), 864 | ]; 865 | draw_triangle(points[0], points[1], points[2], WHITE); 866 | draw_triangle_lines(points[0], points[1], points[2], SCROLL_INDICATOR_OUTLINE, BLACK); 867 | } 868 | } 869 | 870 | fn render_dialog( 871 | ctx: &DrawContext, 872 | dialog: &Dialog, 873 | memories: &Vec, 874 | selected_memory: usize, 875 | icon_cache: &HashMap, 876 | copy_op_state: &Arc>, 877 | placeholder: &Texture2D, 878 | scroll_offset: usize, 879 | animation_state: &AnimationState, 880 | playtime_cache: &mut PlaytimeCache, 881 | size_cache: &mut SizeCache, 882 | ) { 883 | let (copy_progress, copy_running) = { 884 | if let Ok(state) = copy_op_state.lock() { 885 | (state.progress, state.running) 886 | } else { 887 | (0, false) 888 | } 889 | }; 890 | 891 | // Only show dialog background and content when animation is complete 892 | if animation_state.dialog_transition_progress >= 1.0 { 893 | draw_rectangle(0.0, 0.0, SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32, UI_BG_COLOR_DIALOG); 894 | } 895 | 896 | // draw game icon and name 897 | let memory_index = get_memory_index(selected_memory, scroll_offset); 898 | if let Some(mem) = memories.get(memory_index) { 899 | let icon = match icon_cache.get(&mem.id) { 900 | Some(icon) => icon, 901 | None => &placeholder, 902 | }; 903 | 904 | let params = DrawTextureParams { 905 | dest_size: Some(Vec2 {x: TILE_SIZE, y: TILE_SIZE }), 906 | source: Some(Rect { x: 0.0, y: 0.0, h: icon.height(), w: icon.width() }), 907 | rotation: 0.0, 908 | flip_x: false, 909 | flip_y: false, 910 | pivot: None 911 | }; 912 | 913 | // Use transition position for icon 914 | let icon_pos = animation_state.get_dialog_transition_pos(); 915 | draw_texture_ex(&icon, icon_pos.x, icon_pos.y, WHITE, params); 916 | 917 | // Only show text when animation is complete 918 | if animation_state.dialog_transition_progress >= 1.0 { 919 | let desc = match mem.name.clone() { 920 | Some(name) => name, 921 | None => mem.id.clone(), 922 | }; 923 | 924 | let playtime = get_game_playtime(mem, playtime_cache); 925 | let size = get_game_size(mem, size_cache); 926 | text(&ctx, &desc, TILE_SIZE*2.0, TILE_SIZE-1.0); 927 | text(&ctx, &format!("{:.1} MB | {:.1} H", size, playtime), TILE_SIZE*2.0, TILE_SIZE*1.5+1.0); 928 | } 929 | }; 930 | 931 | if copy_running { 932 | draw_rectangle_lines( 933 | (FONT_SIZE*3) as f32, 934 | SCREEN_HEIGHT as f32 / 2.0, 935 | (SCREEN_WIDTH as u16 - FONT_SIZE*6) as f32, 936 | 1.2*FONT_SIZE as f32, 937 | 4.0, 938 | Color {r: 1.0, g: 1.0, b: 1.0, a: 1.0 } 939 | ); 940 | draw_rectangle( 941 | (FONT_SIZE*3) as f32 + 0.2*FONT_SIZE as f32, 942 | SCREEN_HEIGHT as f32 / 2.0 + 0.2*FONT_SIZE as f32, 943 | ((SCREEN_WIDTH as u16 - FONT_SIZE*6) as f32 - 0.4*FONT_SIZE as f32) * (copy_progress as f32 / 100.0), 944 | 0.8*FONT_SIZE as f32, 945 | Color {r: 1.0, g: 1.0, b: 1.0, a: 1.0 } 946 | ); 947 | } else if animation_state.dialog_transition_progress >= 1.0 { 948 | if let Some(desc) = dialog.desc.clone() { 949 | text(&ctx, &desc, (SCREEN_WIDTH as f32 - measure_text(&desc, Some(&ctx.font), FONT_SIZE, 1.0).width) / 2.0, (FONT_SIZE*7) as f32); 950 | } 951 | 952 | // Find the longest option text for centering 953 | let longest_option = dialog.options.iter() 954 | .map(|opt| opt.text.len()) 955 | .max() 956 | .unwrap_or(0); 957 | 958 | // Calculate the width of the longest option in pixels 959 | let longest_width = measure_text(&dialog.options.iter() 960 | .find(|opt| opt.text.len() == longest_option) 961 | .map(|opt| opt.text.to_uppercase()) 962 | .unwrap_or_default(), 963 | Some(&ctx.font), 964 | FONT_SIZE, 965 | 1.0).width; 966 | 967 | // Calculate the starting X position to center all options 968 | let options_start_x = (SCREEN_WIDTH as f32 - longest_width) / 2.0; 969 | 970 | // Add padding to the selection rectangle 971 | const SELECTION_PADDING_X: f32 = 16.0; // Padding on each side 972 | const SELECTION_PADDING_Y: f32 = 4.0; // Padding on top and bottom 973 | 974 | for (i, option) in dialog.options.iter().enumerate() { 975 | let y_pos = (FONT_SIZE*10 + FONT_SIZE*2*(i as u16)) as f32; 976 | let shake_offset = if option.disabled { 977 | animation_state.calculate_shake_offset(ShakeTarget::Dialog) 978 | } else { 979 | 0.0 980 | }; 981 | if option.disabled { 982 | text_disabled(&ctx, &option.text, options_start_x + shake_offset, y_pos); 983 | } else { 984 | text(&ctx, &option.text, options_start_x, y_pos); 985 | } 986 | } 987 | 988 | // Draw selection rectangle with padding 989 | let selection_y = (FONT_SIZE*9 + FONT_SIZE*2*(dialog.selection as u16)) as f32; 990 | let selected_option = &dialog.options[dialog.selection]; 991 | let selection_shake = if selected_option.disabled { 992 | animation_state.calculate_shake_offset(ShakeTarget::Dialog) 993 | } else { 994 | 0.0 995 | }; 996 | 997 | let cursor_color = animation_state.get_cursor_color(); 998 | let cursor_scale = animation_state.get_cursor_scale(); 999 | let base_width = longest_width + (SELECTION_PADDING_X * 2.0); 1000 | let base_height = 1.2*FONT_SIZE as f32 + (SELECTION_PADDING_Y * 2.0); 1001 | let scaled_width = base_width * cursor_scale; 1002 | let scaled_height = base_height * cursor_scale; 1003 | let offset_x = (scaled_width - base_width) / 2.0; 1004 | let offset_y = (scaled_height - base_height) / 2.0; 1005 | 1006 | draw_rectangle_lines( 1007 | options_start_x - SELECTION_PADDING_X + selection_shake - offset_x, 1008 | selection_y - SELECTION_PADDING_Y - offset_y, 1009 | scaled_width, 1010 | scaled_height, 1011 | 4.0, 1012 | cursor_color 1013 | ); 1014 | } 1015 | } 1016 | 1017 | fn create_confirm_delete_dialog() -> Dialog { 1018 | Dialog { 1019 | id: "confirm_delete".to_string(), 1020 | desc: Some("PERMANENTLY DELETE THIS SAVE DATA?".to_string()), 1021 | options: vec![ 1022 | DialogOption { 1023 | text: "DELETE".to_string(), 1024 | value: "DELETE".to_string(), 1025 | disabled: false, 1026 | }, 1027 | DialogOption { 1028 | text: "CANCEL".to_string(), 1029 | value: "CANCEL".to_string(), 1030 | disabled: false, 1031 | } 1032 | ], 1033 | selection: 1, 1034 | } 1035 | } 1036 | 1037 | fn create_copy_storage_dialog(storage_state: &Arc>) -> Dialog { 1038 | let mut options = Vec::new(); 1039 | if let Ok(state) = storage_state.lock() { 1040 | for drive in state.media.iter() { 1041 | if drive.id == state.media[state.selected].id { 1042 | continue; 1043 | } 1044 | options.push(DialogOption { 1045 | text: format!("{} ({} MB Free)", drive.id.clone(), drive.free), 1046 | value: drive.id.clone(), 1047 | disabled: false, 1048 | }); 1049 | } 1050 | } 1051 | options.push(DialogOption { 1052 | text: "CANCEL".to_string(), 1053 | value: "CANCEL".to_string(), 1054 | disabled: false, 1055 | }); 1056 | 1057 | Dialog { 1058 | id: "copy_storage_select".to_string(), 1059 | desc: Some("WHERE TO COPY THIS SAVE DATA?".to_string()), 1060 | options, 1061 | selection: 0, 1062 | } 1063 | } 1064 | 1065 | fn create_main_dialog(storage_state: &Arc>) -> Dialog { 1066 | let has_external_devices = if let Ok(state) = storage_state.lock() { 1067 | state.media.len() > 1 1068 | } else { 1069 | false 1070 | }; 1071 | 1072 | let options = vec![ 1073 | DialogOption { 1074 | text: "COPY".to_string(), 1075 | value: "COPY".to_string(), 1076 | disabled: !has_external_devices, 1077 | }, 1078 | DialogOption { 1079 | text: "DELETE".to_string(), 1080 | value: "DELETE".to_string(), 1081 | disabled: false, 1082 | }, 1083 | DialogOption { 1084 | text: "CANCEL".to_string(), 1085 | value: "CANCEL".to_string(), 1086 | disabled: false, 1087 | }, 1088 | ]; 1089 | 1090 | Dialog { 1091 | id: "main".to_string(), 1092 | desc: None, 1093 | options, 1094 | selection: 0, 1095 | } 1096 | } 1097 | 1098 | async fn check_save_exists(memory: &Memory, target_media: &StorageMedia, icon_cache: &mut HashMap, icon_queue: &mut Vec<(String, String)>) -> bool { 1099 | let target_memories = load_memories(target_media, icon_cache, icon_queue).await; 1100 | target_memories.iter().any(|m| m.id == memory.id) 1101 | } 1102 | 1103 | fn create_save_exists_dialog() -> Dialog { 1104 | Dialog { 1105 | id: "save_exists".to_string(), 1106 | desc: Some("THIS SAVE DATA ALREADY EXISTS AT THE SELECTED DESTINATION".to_string()), 1107 | options: vec![ 1108 | DialogOption { 1109 | text: "OK".to_string(), 1110 | value: "OK".to_string(), 1111 | disabled: false, 1112 | } 1113 | ], 1114 | selection: 0, 1115 | } 1116 | } 1117 | 1118 | fn create_error_dialog(message: String) -> Dialog { 1119 | Dialog { 1120 | id: "error".to_string(), 1121 | desc: Some(message), 1122 | options: vec![ 1123 | DialogOption { 1124 | text: "OK".to_string(), 1125 | value: "OK".to_string(), 1126 | disabled: false, 1127 | } 1128 | ], 1129 | selection: 0, 1130 | } 1131 | } 1132 | 1133 | #[derive(Clone, Debug, PartialEq)] 1134 | enum DialogState { 1135 | None, 1136 | Opening, 1137 | Open, 1138 | Closing, 1139 | } 1140 | 1141 | fn render_main_menu( 1142 | ctx: &DrawContext, 1143 | menu_options: &[&str], 1144 | selected_option: usize, 1145 | play_option_enabled: bool, 1146 | animation_state: &AnimationState, 1147 | logo: &Texture2D, 1148 | ) { 1149 | const MENU_START_Y: f32 = 120.0; 1150 | const MENU_OPTION_HEIGHT: f32 = 40.0; 1151 | const MENU_PADDING: f32 = 16.0; 1152 | 1153 | // Draw background 1154 | draw_rectangle(0.0, 0.0, SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32, UI_BG_COLOR); 1155 | 1156 | // Draw menu options 1157 | for (i, option) in menu_options.iter().enumerate() { 1158 | let y_pos = MENU_START_Y + (i as f32 * MENU_OPTION_HEIGHT); 1159 | 1160 | // Draw selected option highlight 1161 | if i == selected_option { 1162 | let cursor_color = animation_state.get_cursor_color(); 1163 | let cursor_scale = animation_state.get_cursor_scale(); 1164 | let base_width = measure_text(option, Some(&ctx.font), FONT_SIZE, 1.0).width + (MENU_PADDING * 2.0); 1165 | let base_height = FONT_SIZE as f32 + (MENU_PADDING * 2.0); 1166 | let scaled_width = base_width * cursor_scale; 1167 | let scaled_height = base_height * cursor_scale; 1168 | let offset_x = (scaled_width - base_width) / 2.0; 1169 | let offset_y = (scaled_height - base_height) / 2.0; 1170 | let mut x_pos = (SCREEN_WIDTH as f32 - base_width) / 2.0; 1171 | 1172 | // Apply shake effect to selection highlight for disabled play option 1173 | if i == 1 && !play_option_enabled { 1174 | let shake_offset = animation_state.calculate_shake_offset(ShakeTarget::PlayOption); 1175 | x_pos += shake_offset; 1176 | } 1177 | 1178 | draw_rectangle_lines(x_pos - offset_x, y_pos - 7.0 - offset_y, scaled_width, scaled_height/1.5, 4.0, cursor_color); 1179 | } 1180 | 1181 | // Draw text 1182 | let text_width = measure_text(option, Some(&ctx.font), FONT_SIZE, 1.0).width; 1183 | let mut x_pos = (SCREEN_WIDTH as f32 - text_width) / 2.0; 1184 | let y_pos_text = y_pos + MENU_PADDING; 1185 | 1186 | // Apply shake effect to disabled play option when selected 1187 | if i == 1 && !play_option_enabled { 1188 | let shake_offset = animation_state.calculate_shake_offset(ShakeTarget::PlayOption); 1189 | x_pos += shake_offset; 1190 | } 1191 | 1192 | if i == 1 && !play_option_enabled { 1193 | text_disabled(&ctx, option, x_pos, y_pos_text); 1194 | } else { 1195 | text(&ctx, option, x_pos, y_pos_text); 1196 | } 1197 | } 1198 | 1199 | // Draw logo and version number 1200 | draw_texture(logo, (SCREEN_WIDTH as f32 - 166.0)/2.0, 30.0, WHITE); 1201 | text(&ctx, "V2025.0", SCREEN_WIDTH as f32 - 90.0, SCREEN_HEIGHT as f32 - 20.0); 1202 | } 1203 | 1204 | #[macroquad::main(window_conf)] 1205 | async fn main() { 1206 | let mut dialogs: Vec = Vec::new(); 1207 | let mut dialog_state = DialogState::None; 1208 | let font = load_ttf_font_from_bytes(include_bytes!("../november.ttf")).unwrap(); 1209 | let background = Texture2D::from_file_with_format(include_bytes!("../background.png"), Some(ImageFormat::Png)); 1210 | let logo = Texture2D::from_file_with_format(include_bytes!("../logo.png"), Some(ImageFormat::Png)); 1211 | let placeholder = Texture2D::from_file_with_format(include_bytes!("../placeholder.png"), Some(ImageFormat::Png)); 1212 | let mut icon_cache: HashMap = HashMap::new(); 1213 | let mut icon_queue: Vec<(String, String)> = Vec::new(); 1214 | let mut playtime_cache: PlaytimeCache = HashMap::new(); 1215 | let mut size_cache: SizeCache = HashMap::new(); 1216 | let mut scroll_offset = 0; 1217 | 1218 | let ctx : DrawContext = DrawContext { 1219 | font: font, 1220 | }; 1221 | 1222 | // Initialize sound effects 1223 | let sound_effects = SoundEffects::new().await; 1224 | 1225 | // Initialize gamepad support 1226 | let mut gilrs = Gilrs::new().unwrap(); 1227 | let mut input_state = InputState::new(); 1228 | let mut animation_state = AnimationState::new(); 1229 | 1230 | // Screen state 1231 | const MAIN_MENU_OPTIONS: [&str; 2] = ["DATA", "PLAY"]; 1232 | let mut current_screen = Screen::MainMenu; 1233 | let mut main_menu_selection: usize = 0; 1234 | let mut play_option_enabled: bool = false; 1235 | 1236 | // Fade state 1237 | let mut fade_start_time: Option = None; 1238 | const FADE_DURATION: f64 = 1.0; // 1 second fade 1239 | const FADE_LINGER_DURATION: f64 = 0.5; // 0.5 seconds to linger on black screen 1240 | 1241 | // Create thread-safe cart connection status 1242 | let cart_connected = Arc::new(AtomicBool::new(false)); 1243 | let cart_check_thread_running = Arc::new(AtomicBool::new(false)); 1244 | 1245 | // Spawn background thread for cart connection detection (only active during main menu) 1246 | let cart_connected_clone = cart_connected.clone(); 1247 | let cart_check_thread_running_clone = cart_check_thread_running.clone(); 1248 | thread::spawn(move || { 1249 | while cart_check_thread_running_clone.load(Ordering::Relaxed) { 1250 | let is_connected = save::is_cart_connected(); 1251 | cart_connected_clone.store(is_connected, Ordering::Relaxed); 1252 | thread::sleep(time::Duration::from_secs(1)); 1253 | } 1254 | }); 1255 | 1256 | // Create thread-safe storage media state 1257 | let storage_state = Arc::new(Mutex::new(StorageMediaState::new())); 1258 | 1259 | // Initialize storage media list 1260 | if let Ok(mut state) = storage_state.lock() { 1261 | state.update_media(); 1262 | }; 1263 | 1264 | // Spawn background thread for storage media detection 1265 | let thread_storage_state = storage_state.clone(); 1266 | thread::spawn(move || { 1267 | loop { 1268 | thread::sleep(time::Duration::from_secs(1)); 1269 | if let Ok(mut state) = thread_storage_state.lock() { 1270 | state.update_media(); 1271 | } 1272 | } 1273 | }); 1274 | 1275 | let mut memories = Vec::new(); 1276 | let mut selected_memory = 0; 1277 | 1278 | let copy_op_state = Arc::new(Mutex::new(CopyOperationState { 1279 | progress: 0, 1280 | running: false, 1281 | should_clear_dialogs: false, 1282 | error_message: None, 1283 | })); 1284 | 1285 | 1286 | let mut bgx = 0.0; 1287 | 1288 | let color_targets: [Color; 6] = [ 1289 | Color { r: 1.0, g: 0.5, b: 0.5, a: 1.0 }, 1290 | Color { r: 1.0, g: 1.0, b: 0.5, a: 1.0 }, 1291 | Color { r: 0.5, g: 1.0, b: 0.5, a: 1.0 }, 1292 | Color { r: 0.5, g: 1.0, b: 1.0, a: 1.0 }, 1293 | Color { r: 0.5, g: 0.5, b: 1.0, a: 1.0 }, 1294 | Color { r: 1.0, g: 0.5, b: 1.0, a: 1.0 }, 1295 | ]; 1296 | 1297 | let mut bg_color = color_targets[0].clone(); 1298 | let mut tg_color = color_targets[1].clone(); 1299 | 1300 | let mut target = 1; 1301 | 1302 | const DELTA: f32 = 0.0001; 1303 | 1304 | loop { 1305 | draw_texture(&background, bgx-(SCREEN_WIDTH as f32), 0.0, bg_color); 1306 | draw_texture(&background, bgx, 0.0, bg_color); 1307 | bgx = (bgx + 0.1) % (SCREEN_WIDTH as f32); 1308 | 1309 | if bg_color.r < tg_color.r { 1310 | bg_color.r += DELTA; 1311 | } else if bg_color.r > tg_color.r { 1312 | bg_color.r -= DELTA; 1313 | } 1314 | 1315 | if bg_color.g < tg_color.g { 1316 | bg_color.g += DELTA; 1317 | } else if bg_color.g > tg_color.g { 1318 | bg_color.g -= DELTA; 1319 | } 1320 | 1321 | if bg_color.b < tg_color.b { 1322 | bg_color.b += DELTA; 1323 | } else if bg_color.b > tg_color.b { 1324 | bg_color.b -= DELTA; 1325 | } 1326 | 1327 | if (bg_color.r - tg_color.r).abs() < 0.01 && (bg_color.g - tg_color.g).abs() < 0.01 && (bg_color.b - tg_color.b).abs() < 0.01 { 1328 | target = (target + 1) % 6; 1329 | tg_color = color_targets[target].clone(); 1330 | } 1331 | 1332 | let mut action_dialog_id = String::new(); 1333 | let mut action_option_value = String::new(); 1334 | 1335 | // Update input state from both keyboard and controller 1336 | input_state.update_keyboard(); 1337 | input_state.update_controller(&mut gilrs); 1338 | 1339 | // Update animations 1340 | animation_state.update_shake(get_frame_time()); 1341 | animation_state.update_cursor_animation(get_frame_time()); 1342 | animation_state.update_dialog_transition(get_frame_time()); 1343 | 1344 | // Manage cart check thread based on current screen 1345 | let should_thread_run = current_screen == Screen::MainMenu; 1346 | let thread_is_running = cart_check_thread_running.load(Ordering::Relaxed); 1347 | 1348 | if should_thread_run && !thread_is_running { 1349 | // Entered main menu, start cart check thread 1350 | cart_check_thread_running.store(true, Ordering::Relaxed); 1351 | let cart_connected_clone = cart_connected.clone(); 1352 | let cart_check_thread_running_clone = cart_check_thread_running.clone(); 1353 | thread::spawn(move || { 1354 | while cart_check_thread_running_clone.load(Ordering::Relaxed) { 1355 | let is_connected = save::is_cart_connected(); 1356 | cart_connected_clone.store(is_connected, Ordering::Relaxed); 1357 | thread::sleep(time::Duration::from_secs(1)); 1358 | } 1359 | }); 1360 | } else if !should_thread_run && thread_is_running { 1361 | // Left main menu, stop cart check thread 1362 | cart_check_thread_running.store(false, Ordering::Relaxed); 1363 | } 1364 | 1365 | // Update dialog state based on animation 1366 | if animation_state.dialog_transition_time <= 0.0 { 1367 | match dialog_state { 1368 | DialogState::Opening => { 1369 | dialog_state = DialogState::Open; 1370 | }, 1371 | DialogState::Closing => { 1372 | dialog_state = DialogState::None; 1373 | dialogs.clear(); 1374 | }, 1375 | _ => {} 1376 | } 1377 | } 1378 | 1379 | // Handle screen-specific rendering and input 1380 | match current_screen { 1381 | Screen::FadingOut => { 1382 | // During fade, only render, don't process input 1383 | // Render the current background and UI elements first 1384 | render_main_menu(&ctx, &MAIN_MENU_OPTIONS, main_menu_selection, play_option_enabled, &animation_state, &logo); 1385 | 1386 | // Calculate fade progress 1387 | if let Some(start_time) = fade_start_time { 1388 | let elapsed = get_time() - start_time; 1389 | let fade_progress = (elapsed / FADE_DURATION).min(1.0); 1390 | 1391 | // Draw fade overlay 1392 | let alpha = fade_progress as f32; 1393 | draw_rectangle(0.0, 0.0, SCREEN_WIDTH as f32, SCREEN_HEIGHT as f32, 1394 | Color { r: 0.0, g: 0.0, b: 0.0, a: alpha }); 1395 | 1396 | // If fade is complete, wait for linger duration then exit 1397 | if fade_progress >= 1.0 { 1398 | let total_elapsed = elapsed - FADE_DURATION; 1399 | if total_elapsed >= FADE_LINGER_DURATION { 1400 | process::exit(0); 1401 | } 1402 | } 1403 | } 1404 | }, 1405 | Screen::MainMenu => { 1406 | // Update play option enabled status based on cart connection 1407 | play_option_enabled = cart_connected.load(Ordering::Relaxed); 1408 | 1409 | render_main_menu(&ctx, &MAIN_MENU_OPTIONS, main_menu_selection, play_option_enabled, &animation_state, &logo); 1410 | 1411 | // Handle main menu navigation 1412 | if input_state.up { 1413 | if main_menu_selection == 0 { 1414 | main_menu_selection = MAIN_MENU_OPTIONS.len() - 1; 1415 | } else { 1416 | main_menu_selection = (main_menu_selection - 1) % MAIN_MENU_OPTIONS.len(); 1417 | } 1418 | animation_state.trigger_transition(); 1419 | sound_effects.play_cursor_move(); 1420 | } 1421 | if input_state.down { 1422 | main_menu_selection = (main_menu_selection + 1) % MAIN_MENU_OPTIONS.len(); 1423 | animation_state.trigger_transition(); 1424 | sound_effects.play_cursor_move(); 1425 | } 1426 | if input_state.select { 1427 | match main_menu_selection { 1428 | 0 => { 1429 | current_screen = Screen::SaveData; 1430 | input_state.ui_focus = UIFocus::Grid; 1431 | sound_effects.play_select(); 1432 | }, 1433 | 1 => { 1434 | if play_option_enabled { 1435 | sound_effects.play_select(); 1436 | // Create restart session sentinel file and start fade 1437 | let state_dir = Path::new(".local/share/kazeta/state"); 1438 | if state_dir.exists() { 1439 | let sentinel_path = state_dir.join(".RESTART_SESSION_SENTINEL"); 1440 | if let Err(_) = fs::File::create(&sentinel_path) { 1441 | // If we can't create the file, just continue 1442 | // Don't show error to user 1443 | } 1444 | } 1445 | // Start fade to black 1446 | fade_start_time = Some(get_time()); 1447 | current_screen = Screen::FadingOut; 1448 | } else { 1449 | sound_effects.play_reject(); 1450 | animation_state.trigger_play_option_shake(); 1451 | } 1452 | }, 1453 | _ => {} 1454 | } 1455 | } 1456 | }, 1457 | Screen::SaveData => { 1458 | // Check if memories need to be refreshed due to storage media changes 1459 | if let Ok(mut state) = storage_state.lock() { 1460 | if state.needs_memory_refresh { 1461 | if !state.media.is_empty() { 1462 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1463 | } else { 1464 | memories = Vec::new(); 1465 | } 1466 | state.needs_memory_refresh = false; 1467 | dialogs.clear(); 1468 | } 1469 | } 1470 | 1471 | match dialog_state { 1472 | DialogState::None => { 1473 | render_data_view(&ctx, selected_memory, &memories, &icon_cache, &storage_state, &placeholder, scroll_offset, &mut input_state, &mut animation_state, &mut playtime_cache, &mut size_cache); 1474 | 1475 | // Handle back navigation 1476 | if input_state.back { 1477 | current_screen = Screen::MainMenu; 1478 | sound_effects.play_back(); 1479 | } 1480 | 1481 | // Handle storage media switching with tab/bumpers regardless of focus 1482 | if input_state.cycle || input_state.next || input_state.prev { 1483 | if let Ok(mut state) = storage_state.lock() { 1484 | if input_state.cycle { 1485 | if state.media.len() > 1 { 1486 | // Cycle wraps around 1487 | state.selected = (state.selected + 1) % state.media.len(); 1488 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1489 | scroll_offset = 0; 1490 | sound_effects.play_select(); 1491 | } 1492 | } else if input_state.next { 1493 | // Next stops at end 1494 | if state.selected < state.media.len() - 1 { 1495 | state.selected += 1; 1496 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1497 | scroll_offset = 0; 1498 | sound_effects.play_select(); 1499 | } else { 1500 | animation_state.trigger_shake(false); // Shake right arrow when can't go next 1501 | sound_effects.play_reject(); 1502 | } 1503 | } else if input_state.prev { 1504 | // Prev stops at beginning 1505 | if state.selected > 0 { 1506 | state.selected -= 1; 1507 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1508 | scroll_offset = 0; 1509 | sound_effects.play_select(); 1510 | } else { 1511 | animation_state.trigger_shake(true); // Shake left arrow when can't go prev 1512 | sound_effects.play_reject(); 1513 | } 1514 | } 1515 | } 1516 | } 1517 | 1518 | match input_state.ui_focus { 1519 | UIFocus::Grid => { 1520 | if input_state.select { 1521 | let memory_index = get_memory_index(selected_memory, scroll_offset); 1522 | if let Some(_) = memories.get(memory_index) { 1523 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1524 | animation_state.trigger_dialog_transition(grid_pos, dialog_pos); 1525 | dialogs.push(create_main_dialog(&storage_state)); 1526 | dialog_state = DialogState::Opening; 1527 | sound_effects.play_select(); 1528 | } 1529 | } 1530 | if input_state.right && selected_memory < GRID_WIDTH * GRID_HEIGHT - 1 { 1531 | selected_memory += 1; 1532 | animation_state.trigger_transition(); 1533 | sound_effects.play_cursor_move(); 1534 | } 1535 | if input_state.left && selected_memory >= 1 { 1536 | selected_memory -= 1; 1537 | animation_state.trigger_transition(); 1538 | sound_effects.play_cursor_move(); 1539 | } 1540 | if input_state.down { 1541 | if selected_memory < GRID_WIDTH * GRID_HEIGHT - GRID_WIDTH { 1542 | selected_memory += GRID_WIDTH; 1543 | animation_state.trigger_transition(); 1544 | sound_effects.play_cursor_move(); 1545 | } else { 1546 | // Check if there are any saves in the next row 1547 | let next_row_start = get_memory_index(GRID_WIDTH * GRID_HEIGHT, scroll_offset); 1548 | if next_row_start < memories.len() { 1549 | scroll_offset += 1; 1550 | animation_state.trigger_transition(); 1551 | sound_effects.play_cursor_move(); 1552 | } 1553 | } 1554 | } 1555 | if input_state.up { 1556 | if selected_memory >= GRID_WIDTH { 1557 | selected_memory -= GRID_WIDTH; 1558 | animation_state.trigger_transition(); 1559 | sound_effects.play_cursor_move(); 1560 | } else if scroll_offset > 0 { 1561 | scroll_offset -= 1; 1562 | animation_state.trigger_transition(); 1563 | sound_effects.play_cursor_move(); 1564 | } else { 1565 | // Allow moving to storage navigation from leftmost or rightmost column 1566 | if selected_memory % GRID_WIDTH == 0 { 1567 | input_state.ui_focus = UIFocus::StorageLeft; 1568 | animation_state.trigger_transition(); 1569 | sound_effects.play_cursor_move(); 1570 | } else if selected_memory % GRID_WIDTH == GRID_WIDTH - 1 { 1571 | input_state.ui_focus = UIFocus::StorageRight; 1572 | animation_state.trigger_transition(); 1573 | sound_effects.play_cursor_move(); 1574 | } 1575 | } 1576 | } 1577 | }, 1578 | UIFocus::StorageLeft => { 1579 | if input_state.right { 1580 | input_state.ui_focus = UIFocus::StorageRight; 1581 | animation_state.trigger_transition(); 1582 | sound_effects.play_cursor_move(); 1583 | } 1584 | if input_state.down { 1585 | input_state.ui_focus = UIFocus::Grid; 1586 | selected_memory = 0; // Move to leftmost grid position 1587 | animation_state.trigger_transition(); 1588 | sound_effects.play_cursor_move(); 1589 | } 1590 | if input_state.select { 1591 | if let Ok(mut state) = storage_state.lock() { 1592 | if state.selected > 0 { 1593 | state.selected -= 1; 1594 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1595 | scroll_offset = 0; 1596 | sound_effects.play_select(); 1597 | } else { 1598 | animation_state.trigger_shake(true); 1599 | sound_effects.play_reject(); 1600 | } 1601 | } 1602 | } 1603 | }, 1604 | UIFocus::StorageRight => { 1605 | if input_state.left { 1606 | input_state.ui_focus = UIFocus::StorageLeft; 1607 | animation_state.trigger_transition(); 1608 | sound_effects.play_cursor_move(); 1609 | } 1610 | if input_state.down { 1611 | input_state.ui_focus = UIFocus::Grid; 1612 | selected_memory = GRID_WIDTH - 1; // Move to rightmost grid position 1613 | animation_state.trigger_transition(); 1614 | sound_effects.play_cursor_move(); 1615 | } 1616 | if input_state.select { 1617 | if let Ok(mut state) = storage_state.lock() { 1618 | if state.selected < state.media.len() - 1 { 1619 | state.selected += 1; 1620 | memories = load_memories(&state.media[state.selected], &mut icon_cache, &mut icon_queue).await; 1621 | scroll_offset = 0; 1622 | sound_effects.play_select(); 1623 | } else { 1624 | animation_state.trigger_shake(false); 1625 | sound_effects.play_reject(); 1626 | } 1627 | } 1628 | } 1629 | }, 1630 | } 1631 | }, 1632 | DialogState::Opening => { 1633 | // During opening, only render the main view and the transitioning icon 1634 | render_data_view(&ctx, selected_memory, &memories, &icon_cache, &storage_state, &placeholder, scroll_offset, &mut input_state, &mut animation_state, &mut playtime_cache, &mut size_cache); 1635 | // Only render the icon during transition 1636 | let memory_index = get_memory_index(selected_memory, scroll_offset); 1637 | if let Some(mem) = memories.get(memory_index) { 1638 | let icon = match icon_cache.get(&mem.id) { 1639 | Some(icon) => icon, 1640 | None => &placeholder, 1641 | }; 1642 | 1643 | let params = DrawTextureParams { 1644 | dest_size: Some(Vec2 {x: TILE_SIZE, y: TILE_SIZE }), 1645 | source: Some(Rect { x: 0.0, y: 0.0, h: icon.height(), w: icon.width() }), 1646 | rotation: 0.0, 1647 | flip_x: false, 1648 | flip_y: false, 1649 | pivot: None 1650 | }; 1651 | 1652 | let icon_pos = animation_state.get_dialog_transition_pos(); 1653 | draw_texture_ex(&icon, icon_pos.x, icon_pos.y, WHITE, params); 1654 | } 1655 | }, 1656 | DialogState::Open => { 1657 | // When dialog is fully open, only render the dialog 1658 | if let Some(dialog) = dialogs.last_mut() { 1659 | render_dialog(&ctx, dialog, &memories, selected_memory, &icon_cache, ©_op_state, &placeholder, scroll_offset, &animation_state, &mut playtime_cache, &mut size_cache); 1660 | 1661 | let mut selection: i32 = dialog.selection as i32 + dialog.options.len() as i32; 1662 | if input_state.up { 1663 | selection -= 1; 1664 | animation_state.trigger_transition(); 1665 | sound_effects.play_cursor_move(); 1666 | } 1667 | 1668 | if input_state.down { 1669 | selection += 1; 1670 | animation_state.trigger_transition(); 1671 | sound_effects.play_cursor_move(); 1672 | } 1673 | 1674 | let mut cancel = false; 1675 | if input_state.back { 1676 | cancel = true; 1677 | } 1678 | 1679 | let next_selection = selection as usize % dialog.options.len(); 1680 | if next_selection != dialog.selection { 1681 | // Store the new selection to apply after we're done with the immutable borrow 1682 | let new_selection = next_selection; 1683 | dialog.selection = new_selection; 1684 | } else { 1685 | // We need to handle the select input 1686 | if input_state.select { 1687 | let selected_option = &dialog.options[dialog.selection]; 1688 | if !selected_option.disabled { 1689 | action_dialog_id = dialog.id.clone(); 1690 | action_option_value = selected_option.value.clone(); 1691 | 1692 | if selected_option.value == "CANCEL" || selected_option.value == "OK" { 1693 | cancel = true; 1694 | } else { 1695 | sound_effects.play_select(); 1696 | } 1697 | } else { 1698 | animation_state.trigger_dialog_shake(); 1699 | sound_effects.play_reject(); 1700 | } 1701 | } 1702 | } 1703 | 1704 | if cancel { 1705 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1706 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1707 | dialog_state = DialogState::Closing; 1708 | sound_effects.play_back(); 1709 | } 1710 | } 1711 | }, 1712 | DialogState::Closing => { 1713 | // During closing, render both views to show the icon returning 1714 | render_data_view(&ctx, selected_memory, &memories, &icon_cache, &storage_state, &placeholder, scroll_offset, &mut input_state, &mut animation_state, &mut playtime_cache, &mut size_cache); 1715 | // Only render the icon during transition 1716 | let memory_index = get_memory_index(selected_memory, scroll_offset); 1717 | if let Some(mem) = memories.get(memory_index) { 1718 | let icon = match icon_cache.get(&mem.id) { 1719 | Some(icon) => icon, 1720 | None => &placeholder, 1721 | }; 1722 | 1723 | let params = DrawTextureParams { 1724 | dest_size: Some(Vec2 {x: TILE_SIZE, y: TILE_SIZE }), 1725 | source: Some(Rect { x: 0.0, y: 0.0, h: icon.height(), w: icon.width() }), 1726 | rotation: 0.0, 1727 | flip_x: false, 1728 | flip_y: false, 1729 | pivot: None 1730 | }; 1731 | 1732 | let icon_pos = animation_state.get_dialog_transition_pos(); 1733 | draw_texture_ex(&icon, icon_pos.x, icon_pos.y, WHITE, params); 1734 | } 1735 | } 1736 | } 1737 | }, 1738 | } 1739 | 1740 | // Handle dialog actions 1741 | match (action_dialog_id.as_str(), action_option_value.as_str()) { 1742 | ("main", "COPY") => { 1743 | dialogs.push(create_copy_storage_dialog(&storage_state)); 1744 | }, 1745 | ("main", "DELETE") => { 1746 | dialogs.push(create_confirm_delete_dialog()); 1747 | }, 1748 | ("main", "CANCEL") => { 1749 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1750 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1751 | dialog_state = DialogState::Closing; 1752 | sound_effects.play_back(); 1753 | }, 1754 | ("confirm_delete", "DELETE") => { 1755 | if let Ok(mut state) = storage_state.lock() { 1756 | let memory_index = get_memory_index(selected_memory, scroll_offset); 1757 | if let Some(mem) = memories.get(memory_index) { 1758 | if let Err(e) = save::delete_save(&mem.id, &state.media[state.selected].id) { 1759 | dialogs.push(create_error_dialog(format!("ERROR: {}", e))); 1760 | } else { 1761 | state.needs_memory_refresh = true; 1762 | dialog_state = DialogState::None; 1763 | sound_effects.play_back(); 1764 | } 1765 | } 1766 | } 1767 | }, 1768 | ("confirm_delete", "CANCEL") => { 1769 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1770 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1771 | dialog_state = DialogState::Closing; 1772 | sound_effects.play_back(); 1773 | }, 1774 | ("copy_storage_select", target_id) if target_id != "CANCEL" => { 1775 | let memory_index = get_memory_index(selected_memory, scroll_offset); 1776 | let mem = memories[memory_index].clone(); 1777 | let target_id = target_id.to_string(); 1778 | if let Ok(state) = storage_state.lock() { 1779 | let to_media = StorageMedia { id: target_id, free: 0 }; 1780 | 1781 | // Check if save already exists 1782 | if check_save_exists(&mem, &to_media, &mut icon_cache, &mut icon_queue).await { 1783 | dialogs.push(create_save_exists_dialog()); 1784 | } else { 1785 | let thread_state = copy_op_state.clone(); 1786 | let from_media = state.media[state.selected].clone(); 1787 | thread::spawn(move || { 1788 | copy_memory(&mem, &from_media, &to_media, thread_state); 1789 | }); 1790 | } 1791 | } 1792 | }, 1793 | ("copy_storage_select", "CANCEL") => { 1794 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1795 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1796 | dialog_state = DialogState::Closing; 1797 | sound_effects.play_back(); 1798 | }, 1799 | ("save_exists", "OK") => { 1800 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1801 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1802 | dialog_state = DialogState::Closing; 1803 | sound_effects.play_back(); 1804 | }, 1805 | ("error", "OK") => { 1806 | let (grid_pos, dialog_pos) = calculate_icon_transition_positions(selected_memory); 1807 | animation_state.trigger_dialog_transition(dialog_pos, grid_pos); 1808 | dialog_state = DialogState::Closing; 1809 | sound_effects.play_back(); 1810 | }, 1811 | _ => {} 1812 | } 1813 | 1814 | if !icon_queue.is_empty() { 1815 | let (cart_id, icon_path) = icon_queue.remove(0); 1816 | let texture_future = load_texture(&icon_path); 1817 | let texture_result = panic::catch_unwind(|| { 1818 | futures::executor::block_on(texture_future) 1819 | }); 1820 | 1821 | if let Ok(Ok(texture)) = texture_result { 1822 | icon_cache.insert(cart_id.clone(), texture); 1823 | } 1824 | } 1825 | 1826 | // Display any copy operation errors 1827 | if let Ok(mut copy_state) = copy_op_state.lock() { 1828 | if let Some(error_msg) = copy_state.error_message.take() { 1829 | dialogs.push(create_error_dialog(error_msg)); 1830 | dialog_state = DialogState::Opening; 1831 | } 1832 | if copy_state.should_clear_dialogs { 1833 | dialog_state = DialogState::Closing; 1834 | copy_state.should_clear_dialogs = false; 1835 | } 1836 | } 1837 | 1838 | next_frame().await 1839 | } 1840 | } 1841 | --------------------------------------------------------------------------------