diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index aa746f421..9a255419f 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -157,7 +157,7 @@ jobs: ls -l fastfetch - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-android-${{ inputs.abi }} path: ./fastfetch-*.* diff --git a/.github/workflows/build-benchmark.yml b/.github/workflows/build-benchmark.yml index 06160b359..7bf013db9 100644 --- a/.github/workflows/build-benchmark.yml +++ b/.github/workflows/build-benchmark.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: download benchmark data - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: pattern: benchmark-* path: benchmarks diff --git a/.github/workflows/build-dragonfly-amd64.yml b/.github/workflows/build-dragonfly-amd64.yml index 00678c7b0..39315ab7d 100644 --- a/.github/workflows/build-dragonfly-amd64.yml +++ b/.github/workflows/build-dragonfly-amd64.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: dragonflybsd version: '6.4.2' @@ -60,7 +60,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-dragonfly-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-freebsd-amd64.yml b/.github/workflows/build-freebsd-amd64.yml index 7f5d0d3db..4b32e8eed 100644 --- a/.github/workflows/build-freebsd-amd64.yml +++ b/.github/workflows/build-freebsd-amd64.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: freebsd version: '15.1' @@ -61,7 +61,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-freebsd-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-haiku-amd64.yml b/.github/workflows/build-haiku-amd64.yml index 05c6326e2..829aedb34 100644 --- a/.github/workflows/build-haiku-amd64.yml +++ b/.github/workflows/build-haiku-amd64.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: haiku version: 'r1beta6' @@ -29,7 +29,7 @@ jobs: run: uname -a - name: Install dependencies - run: pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua || pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua + run: pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_headers vulkan_loader_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua || pkgman install -y git dbus_devel mesa_devel libelf_devel imagemagick_devel opencl_headers ocl_icd_devel vulkan_headers vulkan_loader_devel zlib_devel chafa_devel cmake llvm22_clang ninja pkgconfig python3.10 lua - name: CMake configuration run: CC=clang CXX=clang++ cmake -GNinja -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} . @@ -53,7 +53,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-haiku-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-linux-armv7l.yml b/.github/workflows/build-linux-armv7l.yml index c319d77ca..ec228b507 100644 --- a/.github/workflows/build-linux-armv7l.yml +++ b/.github/workflows/build-linux-armv7l.yml @@ -41,7 +41,7 @@ jobs: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-linux-armv7l path: ./fastfetch-*.* diff --git a/.github/workflows/build-linux-hosts.yml b/.github/workflows/build-linux-hosts.yml index b4c4761e9..48d396fcd 100644 --- a/.github/workflows/build-linux-hosts.yml +++ b/.github/workflows/build-linux-hosts.yml @@ -52,7 +52,7 @@ jobs: - name: Initialize CodeQL if: inputs.arch == 'amd64' - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: c @@ -64,7 +64,7 @@ jobs: - name: perform CodeQL analysis if: inputs.arch == 'amd64' - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 - name: list features run: ./fastfetch --list-features @@ -123,13 +123,13 @@ jobs: cpack -V - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-linux-${{ inputs.arch }} path: ./fastfetch-*.* - name: upload benchmark data - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: benchmark-linux-${{ inputs.arch }} path: benchmark.json diff --git a/.github/workflows/build-linux-i686.yml b/.github/workflows/build-linux-i686.yml index 51af1c9fc..237380cc1 100644 --- a/.github/workflows/build-linux-i686.yml +++ b/.github/workflows/build-linux-i686.yml @@ -67,7 +67,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-linux-i686 path: ./fastfetch-*.* diff --git a/.github/workflows/build-linux-loong64.yml b/.github/workflows/build-linux-loong64.yml index 088d76930..8c35ee082 100644 --- a/.github/workflows/build-linux-loong64.yml +++ b/.github/workflows/build-linux-loong64.yml @@ -54,7 +54,7 @@ jobs: ' - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-linux-loong64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-linux-vms.yml b/.github/workflows/build-linux-vms.yml index 2bfcf1359..5c7f7a179 100644 --- a/.github/workflows/build-linux-vms.yml +++ b/.github/workflows/build-linux-vms.yml @@ -42,7 +42,7 @@ jobs: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-linux-${{ inputs.arch }} path: ./fastfetch-*.* diff --git a/.github/workflows/build-macos-hosts.yml b/.github/workflows/build-macos-hosts.yml index 3805c615e..fac7f467d 100644 --- a/.github/workflows/build-macos-hosts.yml +++ b/.github/workflows/build-macos-hosts.yml @@ -74,13 +74,13 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-macos-universal path: ./fastfetch-*.* - name: upload benchmark data - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: benchmark-macos-universal path: benchmark.json diff --git a/.github/workflows/build-musl-amd64.yml b/.github/workflows/build-musl-amd64.yml index bb980a198..864ef9ece 100644 --- a/.github/workflows/build-musl-amd64.yml +++ b/.github/workflows/build-musl-amd64.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -44,7 +44,7 @@ jobs: shell: alpine.sh {0} - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-musl-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-netbsd-amd64.yml b/.github/workflows/build-netbsd-amd64.yml index 3a8409d67..49e859a1f 100644 --- a/.github/workflows/build-netbsd-amd64.yml +++ b/.github/workflows/build-netbsd-amd64.yml @@ -18,10 +18,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: netbsd - version: '10.1' + version: '11.0' shell: bash cpu_count: 4 @@ -59,7 +59,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-netbsd-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-omnios-amd64.yml b/.github/workflows/build-omnios-amd64.yml index 1a4deead4..23ee409d4 100644 --- a/.github/workflows/build-omnios-amd64.yml +++ b/.github/workflows/build-omnios-amd64.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: omnios version: 'r151058' @@ -64,7 +64,7 @@ jobs: cpack -V - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-omnios-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-openbsd-amd64.yml b/.github/workflows/build-openbsd-amd64.yml index 529019ade..0a4ea6b01 100644 --- a/.github/workflows/build-openbsd-amd64.yml +++ b/.github/workflows/build-openbsd-amd64.yml @@ -18,10 +18,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Start VM - uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0 + uses: cross-platform-actions/action@82668f1fbd89d9c99487604f822cbf6716f124d3 # v1.7.0 with: operating_system: openbsd - version: '7.9' + version: '8.0' shell: bash cpu_count: 4 @@ -32,10 +32,10 @@ jobs: - name: Update and install dependencies run: | sudo pkg_add -u - sudo pkg_add -r llvm-21.1.8p4 cmake git wayland vulkan-headers vulkan-loader glib2 dconf dbus sqlite3 imagemagick chafa lua-5.4.8p0 # pkg-config is preinstalled + sudo pkg_add -r llvm-22.1.8p10 cmake git wayland vulkan-headers vulkan-loader glib2 dconf dbus sqlite3 imagemagick chafa lua-5.4.8p2 # pkg-config is preinstalled - name: CMake configuration - run: CC=clang-21 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" . + run: CC=clang-22 cmake -DSET_TWEAK=Off -DBUILD_TESTS=On -DENABLE_EMBEDDED_PCIIDS=On -DENABLE_EMBEDDED_AMDGPUIDS=On -DCMAKE_BUILD_TYPE=${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} -DCMAKE_C_FLAGS="-fno-pie" -DCMAKE_EXE_LINKER_FLAGS="-no-pie" . - name: CMake build run: cmake --build . --target package --verbose -j4 @@ -59,7 +59,7 @@ jobs: run: ctest --output-on-failure - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-openbsd-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index c5ca7071a..0c628d2c8 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -22,7 +22,7 @@ jobs: - name: download artifacts if: inputs.ffversion != steps.get_version_release.outputs.release - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - name: create release if: inputs.ffversion != steps.get_version_release.outputs.release diff --git a/.github/workflows/build-solaris-amd64.yml b/.github/workflows/build-solaris-amd64.yml index b0e77d72d..1c7627064 100644 --- a/.github/workflows/build-solaris-amd64.yml +++ b/.github/workflows/build-solaris-amd64.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: run VM - uses: vmactions/solaris-vm@87d3c436511cef802cd1637f86f73b1a97715c8c # v1.4.1 + uses: vmactions/solaris-vm@1c166978f5a2c1f87f1c9d4d8dd85ae3d1eff6f7 # v1.4.2 with: usesh: true envs: 'CMAKE_BUILD_TYPE' @@ -40,7 +40,7 @@ jobs: cpack -V - name: upload artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-solaris-amd64 path: ./fastfetch-*.* diff --git a/.github/workflows/build-windows-hosts.yml b/.github/workflows/build-windows-hosts.yml index e92ac4365..9c1e08283 100644 --- a/.github/workflows/build-windows-hosts.yml +++ b/.github/workflows/build-windows-hosts.yml @@ -106,7 +106,7 @@ jobs: - if: github.event_name == 'push' && github.repository == 'fastfetch-cli/fastfetch' id: upload-unsigned-artifact name: upload artifacts for signing - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-windows-${{ inputs.arch }} path: | @@ -134,14 +134,14 @@ jobs: run: 7z a -t7z -mx9 -bd -y fastfetch-windows-${{ inputs.arch }}.7z LICENSE *.dll fastfetch.exe flashfetch.exe presets - name: upload true artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: fastfetch-windows-${{ inputs.arch }} path: ./fastfetch-windows-${{ inputs.arch }}.* overwrite: true - name: upload benchmark data - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: benchmark-windows-${{ inputs.arch }} path: benchmark.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d12fc3e..e020d1289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +# 2.71.0 + +Features: +* Added a `spin` logo type (`--logo-type spin`), which animates the built-in logo or a text file as a 3D slab that rotates. (Logo) + * This logo type requires `--watch `/`--dynamic-interval ` being set to enable animation. For example: `fastfetch --logo-type spin --watch 1` will animate the logo every second. + * `--logo-spin-speed-per-frame` sets the degrees per drawn frame (default 10); `--logo-spin-speed-per-round` sets the seconds one revolution takes and wins when both are given. Both accept a negative value to turn the other way. + * `--logo-spin-angle` picks the frame to start from, and is the only frame drawn without `--watch`. +* Optimised display detection on GNOME and Cinnamon Wayland sessions. (Display, Linux) + +Bugfixes: +* Don't detect mise and atuin as shells. (#2686, Shell) +* Rejected an empty `separator.string` in the JSON config with an error message. (#2689, Separator) +* Fixed fastfetch blocking forever when the terminal answered a terminal query only partially, for example with just one of the two OSC 10/11 color replies. (Logo / Terminal / TerminalTheme, Unix / Windows) +* Fixed a `$` ending a logo line or the logo itself being parsed as the start of a color code instead of a plain dollar sign. (#2666, Logo) +* Fixed AMD GPU name detection in amdgpu.ids when PCI device ID or revision ID has leading zeros. (GPU, Linux) +* Improved uptime detection on SunOS via kstat. (#2674, Uptime, SunOS) +* Fixed substring formatting returning the remaining string when a negative end index is before the start of the string. (General) +* Fixed a skipped `{?}` / `{/}` conditional in a format string ending at the `{?}` / `{/}` of a conditional nested in it. (General) +* Fixed numbers in format strings that don't fit in 32 bits wrapping around to a valid argument index, constant index, substring index or length, e.g. `{4294967297}` being read as `{1}`, and a signed integer overflow for `{1:-2147483648}`. (General) +* Fixed substring formatting printing the placeholder as is, instead of nothing, when the start index is outside of the string and an end index is given. (General) +* Fixed the CPU micro-architecture level (`march`) being under-reported on Windows on ARM when `IsProcessorFeaturePresent` does not expose a feature the CPU implements. (CPU, Windows) + * It is now decoded from the AArch64 ID registers in the registry, matching the Linux detection. + +Logos: +* Added Tiny Core Linux (#2655) +* Added ArmadaOS (#2649) +* Added AstrOS (#2582) +* Added openSUSE Leap Micro (#2703) + # 2.70.0 Changes: @@ -42,6 +71,7 @@ Bugfixes: * On Windows 10 1809 (build 17763) and later, the whole volume's capacity and free space are now reported by default. Set `disk.useAvailable` to `true` to keep reporting the space that is available to the current user, quota included. Logos: +* Added ArmadaOS logo (#2641) * Added filled Artix logo (#2631) * Renamed PostmarketOS to nura (#2626) diff --git a/CMakeLists.txt b/CMakeLists.txt index 61efe08e2..a27230502 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21.0) # C_STANDARD C17 and C23 project(fastfetch - VERSION 2.70.0 + VERSION 2.71.0 LANGUAGES C DESCRIPTION "Fast neofetch-like system information tool" HOMEPAGE_URL "https://github.com/fastfetch-cli/fastfetch" @@ -564,6 +564,8 @@ set(LIBFASTFETCH_SRC src/detection/zpool/zpool.c src/logo/builtin.c src/logo/logo.c + src/logo/spin/render.c + src/logo/spin/spin.c src/modules/modules.c src/options/display.c src/options/logo.c @@ -623,6 +625,7 @@ if(LINUX) src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c src/detection/displayserver/linux/wayland/wp-color-management-v1-protocol.c @@ -712,6 +715,7 @@ elseif(ANDROID) src/detection/diskio/diskio_linux.c src/detection/displayserver/displayserver_android.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c @@ -800,6 +804,7 @@ elseif(FreeBSD) src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c @@ -898,6 +903,7 @@ elseif(NetBSD) src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c @@ -983,6 +989,7 @@ elseif(OpenBSD) src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c @@ -1243,6 +1250,7 @@ elseif(SunOS) src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/wayland/global-output.c src/detection/displayserver/linux/wayland/kde-output.c src/detection/displayserver/linux/wayland/wl-output-protocol.c @@ -1402,6 +1410,7 @@ elseif(GNU) src/detection/diskio/diskio_nosupport.c src/detection/displayserver/linux/displayserver_linux.c src/detection/displayserver/linux/wayland/wayland.c + src/detection/displayserver/linux/wayland/mutter.c src/detection/displayserver/linux/common.c src/detection/displayserver/linux/drm.c src/detection/displayserver/linux/wmde.c @@ -2361,6 +2370,13 @@ if (BUILD_TESTS) PRIVATE libfastfetch ) + add_executable(fastfetch-test-spin + tests/spin.c + ) + target_link_libraries(fastfetch-test-spin + PRIVATE libfastfetch + ) + add_executable(fastfetch-test-strbuf tests/strbuf.c ) @@ -2483,6 +2499,7 @@ if (BUILD_TESTS) enable_testing() add_test(NAME test-cache COMMAND fastfetch-test-cache) add_test(NAME test-logo COMMAND fastfetch-test-logo) + add_test(NAME test-spin COMMAND fastfetch-test-spin) add_test(NAME test-strbuf COMMAND fastfetch-test-strbuf) add_test(NAME test-list COMMAND fastfetch-test-list) add_test(NAME test-format COMMAND fastfetch-test-format) diff --git a/README.md b/README.md index 160a1975d..d5a236c7d 100644 --- a/README.md +++ b/README.md @@ -213,10 +213,6 @@ Alternatively, you may try using `fastfetch --gpu-driver-specific`, which will m Try `export XAUTHORITY=$HOME/.Xauthority` -### Q: Fastfetch cannot detect my awesome 3rd-party macOS window manager! - -Try `fastfetch --wm-detect-plugin`. See also [#984](https://github.com/fastfetch-cli/fastfetch/issues/984) - ### Q: How can I change the colors of my ASCII logo? Try `fastfetch --logo-color-[1-9] `, where `[1-9]` is the index of color placeholders. diff --git a/doc/help.json b/doc/help.json index 1188434d8..ceb4926d1 100644 --- a/doc/help.json +++ b/doc/help.json @@ -203,10 +203,47 @@ "iterm": "Image file, rendered using the iTerm graphics protocol", "chafa": "Image file, printed as ASCII art using libchafa", "raw": "Image file, printed as raw binary string", + "spin": "Built-in logo (or a text file) drawn as a 3D slab; it turns one step per refresh with `--watch`", "none": "Disable logo printing" } } }, + { + "long": "logo-spin-speed-per-frame", + "desc": "Set how far the spin logo turns per drawn frame, in degrees", + "remark": "Only used by `--logo-type spin`. One revolution takes 360 divided by this value frames, so `--watch` sets the pace; negative turns the other way. 0 means not set, and the default is 10", + "arg": { + "type": "num", + "default": 10 + } + }, + { + "long": "logo-spin-speed-per-round", + "desc": "Set how many seconds one revolution of the spin logo takes", + "remark": "Only used by `--logo-type spin`. Read from the clock, so a slow refresh turns further instead of slowing the logo down; negative turns the other way. 0 means not set, and this wins over `--logo-spin-speed-per-frame` when both are given", + "arg": { + "type": "num", + "default": 0 + } + }, + { + "long": "logo-spin-tilt", + "desc": "Set the camera tilt of the spin logo, in degrees", + "remark": "Only used by `--logo-type spin`", + "arg": { + "type": "num", + "default": 0 + } + }, + { + "long": "logo-spin-angle", + "desc": "Set the rotation of the first spin logo frame, in degrees", + "remark": "Only used by `--logo-type spin`. Without `--watch` the logo is drawn at this angle and stays there. 360 is the same as 0", + "arg": { + "type": "num", + "default": 0 + } + }, { "long": "logo-width", "desc": "Set the width of the logo (in characters) if it is an image", diff --git a/doc/json_schema.json b/doc/json_schema.json index a7c6eca70..8f586889c 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -652,6 +652,10 @@ "const": "raw", "description": "Image file rendered as a raw binary string" }, + { + "const": "spin", + "description": "Built-in logo (or a text file) drawn as a 3D slab, turning one step per `--watch` refresh" + }, { "const": "none", "description": "Disable logo printing" @@ -661,7 +665,49 @@ }, "source": { "type": "string", - "description": "Path to the logo source file" + "description": "Path to the logo source file. For the spin logo: `auto` (the detected OS), any built-in logo name, or a text file in fastfetch logo syntax" + }, + "spin": { + "type": "object", + "additionalProperties": false, + "description": "Options of the spin logo (`logo.type: spin`)", + "properties": { + "speed": { + "type": "object", + "additionalProperties": false, + "description": "How fast the logo turns. 0 means not set for either key; `perRound` wins over `perFrame` when both are given", + "properties": { + "perFrame": { + "type": "number", + "description": "Degrees per drawn frame; a revolution takes 360 / perFrame frames, negative turns the other way. 0 means not set, and 10 is used when neither speed is set", + "minimum": -360, + "maximum": 360, + "default": 0 + }, + "perRound": { + "type": "number", + "description": "Seconds one revolution takes, measured on the clock, negative turns the other way", + "minimum": -3600, + "maximum": 3600, + "default": 0 + } + } + }, + "tilt": { + "type": "number", + "description": "Camera tilt in degrees", + "minimum": -80, + "maximum": 80, + "default": 0 + }, + "angle": { + "type": "number", + "description": "Rotation of the first frame in degrees; 360 is the same as 0. Without `--watch` this is the only frame drawn", + "minimum": 0, + "maximum": 360, + "default": 0 + } + } }, "color": { "type": "object", @@ -4142,7 +4188,8 @@ "string": { "description": "String to print for the separator line", "type": "string", - "default": "-" + "default": "-", + "minLength": 1 }, "outputColor": { "description": "Color of the separator line", diff --git a/src/common/android/applabel.c b/src/common/android/applabel.c index 848c78eb1..853e75895 100644 --- a/src/common/android/applabel.c +++ b/src/common/android/applabel.c @@ -175,27 +175,6 @@ typedef struct FFAndroidLabelPool { return true; } -[[gnu::nonnull(1)]] static void appendCodePoint(FFstrbuf* result, uint32_t code) { - char encoded[4] = {}; - uint32_t length = 0; - if (code < 0x80) { - encoded[length++] = (char) code; - } else if (code < 0x800) { - encoded[length++] = (char) (0xC0 | (code >> 6)); - encoded[length++] = (char) (0x80 | (code & 0x3F)); - } else if (code < 0x10000) { - encoded[length++] = (char) (0xE0 | (code >> 12)); - encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F)); - encoded[length++] = (char) (0x80 | (code & 0x3F)); - } else { - encoded[length++] = (char) (0xF0 | (code >> 18)); - encoded[length++] = (char) (0x80 | ((code >> 12) & 0x3F)); - encoded[length++] = (char) (0x80 | ((code >> 6) & 0x3F)); - encoded[length++] = (char) (0x80 | (code & 0x3F)); - } - ffStrbufAppendNS(result, length, encoded); -} - // One string out of a pool. // // The two lengths in front of a UTF-8 string are a *character* count and a *byte* count, and they @@ -250,7 +229,7 @@ typedef struct FFAndroidLabelPool { i++; } } - appendCodePoint(result, code); + ffStrbufAppendUtf32CodePoint(result, code); } return true; } diff --git a/src/common/impl/format.c b/src/common/impl/format.c index b3b8ab190..2c7f50a49 100644 --- a/src/common/impl/format.c +++ b/src/common/impl/format.c @@ -63,6 +63,13 @@ void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg) { } } +// strtol() to an int32_t. Values that don't fit are clamped instead of wrapping around, +// so that e.g. 4294967297 can't turn into 1. +static int32_t strToInt32(const char* str, char** pEnd) { + long long value = strtoll(str, pEnd, 10); + return value > INT32_MAX ? INT32_MAX : value < INT32_MIN ? INT32_MIN : (int32_t) value; +} + /** * @brief parses a string to a uint32_t * @@ -79,14 +86,14 @@ static uint32_t getArgumentIndex(const char* placeholderValue, uint32_t numArgs, if (firstChar >= '0' && firstChar <= '9') { char* pEnd = nullptr; - uint32_t result = (uint32_t) strtoul(placeholderValue, &pEnd, 10); + unsigned long result = strtoul(placeholderValue, &pEnd, 10); if (result > numArgs) { return UINT32_MAX; } if (*pEnd != '\0') { return UINT32_MAX; } - return result; + return (uint32_t) result; } else if (ffCharIsEnglishAlphabet(firstChar)) { for (uint32_t i = 0; i < numArgs; ++i) { const FFformatarg* arg = &arguments[i]; @@ -343,6 +350,51 @@ static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape return true; } +// Returns the index of the `}` of the `{}` that closes the conditional we are in, or the +// length of the format string if there is none. `i` is the index of the `}` that ends the opening +// placeholder. The part to skip may hold conditionals of its own, which have to be passed over. +// Like in `parseFormatString`, only a conditional with a valid argument opens a block. +static uint32_t findConditionalEnd(const FFstrbuf* formatstr, uint32_t i, char marker, uint32_t numArgs, const FFformatarg* arguments) { + FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate(); + uint32_t depth = 0; + + for (++i; i < formatstr->length; ++i) { + if (formatstr->chars[i] != '{') { + continue; + } + + ++i; + if (i >= formatstr->length) { + break; + } + + // double {{ evaluates to a single { and doesn't start a placeholder + if (formatstr->chars[i] == '{') { + continue; + } + + uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}'); + if (formatstr->chars[i] == marker) { + if (iEnd - i == 1) { + if (depth == 0) { + return iEnd; + } + --depth; + } else { + ffStrbufClear(&placeholderValue); + ffStrbufAppendNS(&placeholderValue, iEnd - i - 1, &formatstr->chars[i + 1]); + uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); + if (index >= 1 && index <= numArgs) { + ++depth; + } + } + } + i = iEnd; + } + + return formatstr->length; +} + static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) { uint32_t argCounter = 0; @@ -440,7 +492,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 } // fastforward to the end of the if without printing the in between - i = ffStrbufNextIndexS(formatstr, i, "{?}") + 2; // 2 is the length of "{?}" - 1 because the loop will increment it again directly after continue + i = findConditionalEnd(formatstr, i, '?', numArgs, arguments); continue; } @@ -463,7 +515,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 } // fastforward to the end of the if without printing the in between - i = ffStrbufNextIndexS(formatstr, i, "{/}") + 2; // 2 is the length of "{/}" - 1 because the loop will increment it again directly after continue + i = findConditionalEnd(formatstr, i, '/', numArgs, arguments); continue; } @@ -480,7 +532,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 // test for constant or env var, if so evaluate it if (firstChar == '$') { char* pend = nullptr; - int32_t indexSigned = (int32_t) strtol(placeholderValue.chars + 1, &pend, 10); + int32_t indexSigned = strToInt32(placeholderValue.chars + 1, &pend); if (pend == placeholderValue.chars + 1) { // treat placeholder as an environment variable char* envValue = getenv(placeholderValue.chars + 1); @@ -539,20 +591,27 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 skipAnsiEscape(&tempString, buffer, &trailingEscape); char* pEnd = nullptr; - int32_t start = (int32_t) strtol(pSep + 1, &pEnd, 10); - if (start < 0) { - start = (int32_t) tempString.length + start; + int32_t start = strToInt32(pSep + 1, &pEnd); + bool hasEnd = *pEnd == ','; + int32_t end = 0; + if (hasEnd) { + end = strToInt32(pEnd + 1, &pEnd); } - if (start >= 0 && (uint32_t) start < tempString.length) { - if (*pEnd == '\0') { - ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]); - } else if (*pEnd == ',') { - int32_t end = (int32_t) strtol(pEnd + 1, &pEnd, 10); - if (!*pEnd) { + + // Check the whole range before using it, so that a valid range that lies + // outside of the string gives an empty result instead of an invalid placeholder + if (!*pEnd) { + if (start < 0) { + start = (int32_t) tempString.length + start; + } + if (start >= 0 && (uint32_t) start < tempString.length) { + if (!hasEnd) { + ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]); + } else { if (end < 0) { end = (int32_t) tempString.length + end; } - if ((uint32_t) end > tempString.length) { + if (end >= 0 && (uint32_t) end > tempString.length) { end = (int32_t) tempString.length; } if (end > start) { @@ -573,7 +632,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 } } else { char* pEnd = nullptr; - int32_t truncLength = (int32_t) strtol(pSep + 1, &pEnd, 10); + int32_t truncLength = strToInt32(pSep + 1, &pEnd); if (*pEnd != '\0') { *pSep = cSep; appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); @@ -583,7 +642,7 @@ static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint3 bool ellipsis = false; if (truncLength < 0) { ellipsis = true; - truncLength = -truncLength; + truncLength = truncLength == INT32_MIN ? INT32_MAX : -truncLength; } FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate(); diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c index 2bd8abac2..ca5b5dca3 100644 --- a/src/common/impl/io_unix.c +++ b/src/common/impl/io_unix.c @@ -149,6 +149,32 @@ void restoreTerm(void) { tcsetattr(ftty, TCSAFLUSH, &oldTerm); } +static const char* waitForTerminalResponse(void) { +#ifndef __APPLE__ + const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS); + if (pollResult <= 0) { + FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s", + pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno)); + return "poll(/dev/tty) timeout or failed"; + } +#else + { + // On macOS, poll(/dev/tty) always returns immediately + // See also https://nathancraddock.com/blog/macos-dev-tty-polling/ + fd_set rd; + FD_ZERO(&rd); + FD_SET(ftty, &rd); + const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }); + if (selectResult <= 0) { + FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s", + selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno)); + return "select(/dev/tty) timeout or failed"; + } + } +#endif + return nullptr; +} + const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { if (ftty < 0) { ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); @@ -174,28 +200,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* ffWriteFDData(ftty, strlen(request), request); // Give the terminal some time to respond -#ifndef __APPLE__ - const int pollResult = poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS); - if (pollResult <= 0) { - FF_DEBUG("poll(/dev/tty) returned %d after %d ms: %s", - pollResult, FF_IO_TERM_RESP_WAIT_MS, pollResult == 0 ? "timed out" : strerror(errno)); - return "poll(/dev/tty) timeout or failed"; + const char* error = waitForTerminalResponse(); + if (error) { + return error; } -#else - { - // On macOS, poll(/dev/tty) always returns immediately - // See also https://nathancraddock.com/blog/macos-dev-tty-polling/ - fd_set rd; - FD_ZERO(&rd); - FD_SET(ftty, &rd); - const int selectResult = select(ftty + 1, &rd, nullptr, nullptr, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }); - if (selectResult <= 0) { - FF_DEBUG("select(/dev/tty) returned %d after %d ms: %s", - selectResult, FF_IO_TERM_RESP_WAIT_MS, selectResult == 0 ? "timed out" : strerror(errno)); - return "select(/dev/tty) timeout or failed"; - } - } -#endif char buffer[1024]; size_t bytesRead = 0; @@ -204,6 +212,13 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* va_start(args, format); while (true) { + // The response may arrive in several chunks; a terminal that sends an incomplete + // response would otherwise block `read` forever + if (bytesRead > 0 && (error = waitForTerminalResponse())) { + va_end(args); + return error; + } + ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1); if (nRead <= 0) { diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index 91f58abce..e787a83d0 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -414,6 +414,35 @@ void ffListFilesRecursively(const char* path, bool pretty) { listFilesRecursively(folder.length, &folder, 0, nullptr, pretty); } +static const char* waitForTerminalResponse(HANDLE hInput) { + while (true) { + NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }); + if (status != STATUS_WAIT_0) { + FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status)); + return "NtWaitForSingleObject() failed or timeout"; + } + + // Ignore all unexpected input events + INPUT_RECORD record; + DWORD len = 0; + if (!PeekConsoleInputW(hInput, &record, 1, &len)) { + return nullptr; + } + + // Key-up events carry no input for `ReadFile`; leaving one in the buffer would make the + // wait return while the next `ReadFile` still blocks + if ( + record.EventType == KEY_EVENT && + record.Event.KeyEvent.bKeyDown && + record.Event.KeyEvent.uChar.UnicodeChar != L'\r' && + record.Event.KeyEvent.uChar.UnicodeChar != L'\n') { + return nullptr; + } + + ReadConsoleInputW(hInput, &record, 1, &len); + } +} + const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE; @@ -440,29 +469,10 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, nullptr); } - while (true) { - NTSTATUS status = NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }); - if (status != STATUS_WAIT_0) { - FF_DEBUG("NtWaitForSingleObject() failed or timeout: %s", ffDebugNtStatus(status)); - SetConsoleMode(hInput, inputMode); - return "NtWaitForSingleObject() failed or timeout"; - } - - // Ignore all unexpected input events - INPUT_RECORD record; - DWORD len = 0; - if (!PeekConsoleInputW(hInput, &record, 1, &len)) { - break; - } - - if ( - record.EventType == KEY_EVENT && - record.Event.KeyEvent.uChar.UnicodeChar != L'\r' && - record.Event.KeyEvent.uChar.UnicodeChar != L'\n') { - break; - } else { - ReadConsoleInputW(hInput, &record, 1, &len); - } + const char* error = waitForTerminalResponse(hInput); + if (error) { + SetConsoleMode(hInput, inputMode); + return error; } va_list args; @@ -472,6 +482,16 @@ const char* ffGetTerminalResponse(const char* request, int nParams, const char* uint32_t bytesRead = 0; while (true) { + // The response may arrive in several chunks; a terminal that sends an incomplete + // response would otherwise block `ReadFile` forever + if (bytesRead > 0 && (error = waitForTerminalResponse(hInput))) { + va_end(args); + if (hasInputMode) { + SetConsoleMode(hInput, inputMode); + } + return error; + } + DWORD bytes = 0; BOOL readOk = ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, nullptr); if (!readOk || bytes == 0) { diff --git a/src/detection/bluetooth/bluetooth_apple.m b/src/detection/bluetooth/bluetooth_apple.m index cef3f9a5f..47bf6eba3 100644 --- a/src/detection/bluetooth/bluetooth_apple.m +++ b/src/detection/bluetooth/bluetooth_apple.m @@ -289,6 +289,7 @@ static FFBluetoothResult* findByName(FFlist* devices, const char* name) { } static const char* detectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSDictionary* classicAddressByIdentifier) { +#ifdef MAC_OS_X_VERSION_10_15 CBManagerAuthorization authorization = CBCentralManager.authorization; if (authorization == CBManagerAuthorizationDenied || authorization == CBManagerAuthorizationRestricted) { return nullptr; // The user said no. `NotDetermined` is left to the manager below, which asks. @@ -377,6 +378,7 @@ static const char* detectLe(FFBluetoothOptions* options, FFlist* devices /* FFBl target->signalQuality = ffRssiToSignalQuality(rssi.intValue); } } +#endif return nullptr; } diff --git a/src/detection/cpu/cpu.c b/src/detection/cpu/cpu.c index 896bbbc40..d5340cce9 100644 --- a/src/detection/cpu/cpu.c +++ b/src/detection/cpu/cpu.c @@ -411,81 +411,246 @@ void ffCPUDetectByCpuid(FFCPUResult* cpu) { #undef HAS_CAP } #elif _WIN32 - #include + #include "common/windows/registry.h" - // Missing from winnt.h of MinGW-w64 - #define PF_ARM_LSE2_AVAILABLE 62 - #define PF_RESERVED_FEATURE 63 - #define PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE 64 - #define PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE 65 - #define PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE 66 - #define PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE 67 - #define PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE 68 - #define PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE 69 - #define PF_ARM_SME_INSTRUCTIONS_AVAILABLE 70 - #define PF_ARM_SME2_INSTRUCTIONS_AVAILABLE 71 - #define PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE 72 - #define PF_ARM_SME2_2_INSTRUCTIONS_AVAILABLE 73 - #define PF_ARM_SME_AES_INSTRUCTIONS_AVAILABLE 74 - #define PF_ARM_SME_SBITPERM_INSTRUCTIONS_AVAILABLE 75 - #define PF_ARM_SME_SF8MM4_INSTRUCTIONS_AVAILABLE 76 - #define PF_ARM_SME_SF8MM8_INSTRUCTIONS_AVAILABLE 77 - #define PF_ARM_SME_SF8DP2_INSTRUCTIONS_AVAILABLE 78 - #define PF_ARM_SME_SF8DP4_INSTRUCTIONS_AVAILABLE 79 - #define PF_ARM_SME_SF8FMA_INSTRUCTIONS_AVAILABLE 80 - #define PF_ARM_SME_F8F32_INSTRUCTIONS_AVAILABLE 81 - #define PF_ARM_SME_F8F16_INSTRUCTIONS_AVAILABLE 82 - #define PF_ARM_SME_F16F16_INSTRUCTIONS_AVAILABLE 83 - #define PF_ARM_SME_B16B16_INSTRUCTIONS_AVAILABLE 84 - #define PF_ARM_SME_F64F64_INSTRUCTIONS_AVAILABLE 85 - #define PF_ARM_SME_I16I64_INSTRUCTIONS_AVAILABLE 86 - #define PF_ARM_SME_LUTv2_INSTRUCTIONS_AVAILABLE 87 - #define PF_ARM_SME_FA64_INSTRUCTIONS_AVAILABLE 88 +// Windows exposes the AArch64 ID registers in the registry, as values named +// "CP xxxx" under HARDWARE\DESCRIPTION\System\CentralProcessor\0. Each value is +// the 64-bit content of one ID register; decode the same fields that the Linux +// HWCAP based detection below is built from. +// +// Where "xxxx" comes from +// ----------------------- +// The four hex digits encode the AArch64 system register name S3_0_C0_CRm_op2 +// (all ID registers live in CRn = C0 and use op0 = 3): +// +// xxxx = 0x4000 + (CRm << 3) + op2 +// +// e.g. ID_AA64PFR0_EL1 = S3_0_C0_C4_0 -> 0x4000 + (4 << 3) + 0 = 0x4020, +// ID_AA64ISAR1_EL1 = S3_0_C0_C6_1 -> 0x4000 + (6 << 3) + 1 = 0x4031. +// The full list is in the ARM Architecture Reference Manual, chapter "AArch64 +// System Register Encoding". Linux has the same encodings in sysreg.h as +// SYS_ID_AA64*_EL1; Windows just prints the encoding instead of the name. +// +// Where the offsets / minimums come from +// -------------------------------------- +// Every field offset below is the LSB of that field in the ARM ARM description +// of the ID register (also SYS_ID_AA64*_EL1_*_SHIFT in Linux sysreg.h). +// Every minimum is the lowest field value that guarantees the corresponding +// instruction/feature (the field value tables in the ARM ARM, mirrored by the +// cpufeature.c checks that set the matching HWCAP/HWCAP2 bit): +// +// value 0b0000 baseline value; usually "not implemented", but for fields +// that describe an always-present form it names that form +// (FP/AdvSIMD: implemented; SVEver: SVE; SMEver: SME) +// value 0b0001 first extension level for most fields; SVEver 1 is SVE2 +// value 0b0010 second level; ATOMIC, LRCPC, DPB, FlagM, PFR1.SME, CSSC +// and SMFR0.SMEver use >= 2 for their newer form +// value 0b1111 reserved "not implemented" marker; a field holding 0xF +// must never be treated as supporting the feature. +// +// armField() therefore requires both "field != 0xF" and +// "field >= minimum". minimum == 0 is used only where the mere presence of an +// implemented field is enough (PFR0.FP and PFR0.AdvSIMD for the ARMv8-A +// baseline). A register can be missing on some Windows builds/CPUs; a missing +// register makes all of its features false and thus lowers the reported +// version instead of reporting a wrong higher one. +// +// Not every field is a 4-bit value: ID_AA64FPFR0_EL1 (ARMv9.5 FP8) only +// defines individual feature bits, so those checks use armBit(). +// +// To add a new instruction/feature in the future +// ---------------------------------------------- +// 1. Find the ID register and field that advertises it in the ARM ARM (and in +// the matching Linux HWCAP/HWCAP2 definition). +// 2. If that register is not read yet, add an FF_ARM_REG_* entry before +// FF_ARM_REG_COUNT and its "CP xxxx" name in armRegisterNames[]; the xxxx is +// computed with the formula above. Registers are read automatically because +// armReadRegisters() loops over the whole table. +// 3. Add a bool using armField(regs, present, FF_ARM_REG_..., offset, minimum) +// or armBit(regs, present, FF_ARM_REG_..., bit). Put the register/field name +// and the Linux HWCAP bit it mirrors in a comment. +// 4. Insert the condition into the correct version's place in the if/else if +// chain below, keeping the order identical to the Linux implementation so +// both platforms report the same march. A newly required feature belongs to +// the version that introduced it; a feature that only raises the baseline +// is an extra `else if` above the older checks. +// 5. Rebuild and check with `fastfetch -s cpu --format json`; compare against +// Linux on the same machine when possible. + +typedef enum FFCPUArmRegister { + FF_ARM_REG_PFR0, + FF_ARM_REG_PFR1, + FF_ARM_REG_ZFR0, + FF_ARM_REG_SMFR0, + FF_ARM_REG_FPFR0, + FF_ARM_REG_ISAR0, + FF_ARM_REG_ISAR1, + FF_ARM_REG_ISAR2, + FF_ARM_REG_ISAR3, + FF_ARM_REG_MMFR1, + FF_ARM_REG_COUNT, +} FFCPUArmRegister; + +static const wchar_t* const armRegisterNames[FF_ARM_REG_COUNT] = { + [FF_ARM_REG_PFR0] = L"CP 4020", // ID_AA64PFR0_EL1 + [FF_ARM_REG_PFR1] = L"CP 4021", // ID_AA64PFR1_EL1 + [FF_ARM_REG_ZFR0] = L"CP 4024", // ID_AA64ZFR0_EL1 + [FF_ARM_REG_SMFR0] = L"CP 4025", // ID_AA64SMFR0_EL1 + [FF_ARM_REG_FPFR0] = L"CP 4027", // ID_AA64FPFR0_EL1 + [FF_ARM_REG_ISAR0] = L"CP 4030", // ID_AA64ISAR0_EL1 + [FF_ARM_REG_ISAR1] = L"CP 4031", // ID_AA64ISAR1_EL1 + [FF_ARM_REG_ISAR2] = L"CP 4032", // ID_AA64ISAR2_EL1 + [FF_ARM_REG_ISAR3] = L"CP 4033", // ID_AA64ISAR3_EL1 + [FF_ARM_REG_MMFR1] = L"CP 4039", // ID_AA64MMFR1_EL1 +}; + +static bool armReadRegisters(uint64_t regs[FF_ARM_REG_COUNT], bool present[FF_ARM_REG_COUNT]) { + FF_AUTO_CLOSE_FD HANDLE hKey = nullptr; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", &hKey, nullptr)) { + return false; + } + + bool anyData = false; + for (uint32_t i = 0; i < FF_ARM_REG_COUNT; ++i) { + uint64_t value = 0; + if (ffRegReadUint64(hKey, armRegisterNames[i], &value, nullptr)) { + regs[i] = value; + present[i] = true; + anyData = true; + } + } + return anyData; +} + +// Same rule as the Linux implementation: a 4-bit field is present when it is +// neither 0xF (not implemented) nor below the value required by the feature. +// `minimum` is the ARM ARM field value that first guarantees the feature; see +// the magic number explanation above. +static bool armField( + const uint64_t regs[FF_ARM_REG_COUNT], + const bool present[FF_ARM_REG_COUNT], + FFCPUArmRegister reg, + uint8_t offset, + uint8_t minimum) { + if (!present[reg]) { + return false; + } + + uint8_t value = (uint8_t) ((regs[reg] >> offset) & 0xF); + return value != 0xF && value >= minimum; +} + +// Single-bit feature fields (currently only ID_AA64FPFR0_EL1). Prefer +// armField() for the normal 4-bit ID fields. +static bool armBit( + const uint64_t regs[FF_ARM_REG_COUNT], + const bool present[FF_ARM_REG_COUNT], + FFCPUArmRegister reg, + uint8_t bit) { + return present[reg] && ((regs[reg] >> bit) & 1) != 0; +} void ffCPUDetectByCpuid(FFCPUResult* cpu) { + uint64_t regs[FF_ARM_REG_COUNT] = {}; + bool present[FF_ARM_REG_COUNT] = {}; + + if (!armReadRegisters(regs, present)) { + return; + } + + // ID_AA64ISAR1_EL1.APA [7:4] describes QARMA5 address authentication, + // while API [11:8] describes the implementation-defined variant. They are + // mutually exclusive but share the same encoding. Normalize API into the + // APA slot so both implementations are detected (Apple Silicon reports + // APA = 0 and API = 2). + uint64_t isar1 = regs[FF_ARM_REG_ISAR1]; + uint8_t apa = (uint8_t) ((isar1 >> 4) & 0xF); + uint8_t api = (uint8_t) ((isar1 >> 8) & 0xF); + if (apa == 0 && api != 0) { + isar1 = (isar1 & ~(UINT64_C(0xF) << 4)) | ((uint64_t) api << 4); + regs[FF_ARM_REG_ISAR1] = isar1; + } + + cpu->march = "unknown"; + // ARMv8-A - bool has_vfp = IsProcessorFeaturePresent(PF_ARM_VFP_32_REGISTERS_AVAILABLE); // Implies basic FP support - bool has_neon = IsProcessorFeaturePresent(PF_ARM_NEON_INSTRUCTIONS_AVAILABLE); // NEON (ASIMD) + bool has_fp = armField(regs, present, FF_ARM_REG_PFR0, 16, 0); // PFR0.FP[19:16] implemented -> HWCAP_FP + bool has_asimd = armField(regs, present, FF_ARM_REG_PFR0, 20, 0); // PFR0.AdvSIMD[23:20] implemented -> HWCAP_ASIMD // ARMv8.1-A - bool has_atomics = IsProcessorFeaturePresent(PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE); // LSE atomics - bool has_crc32 = IsProcessorFeaturePresent(PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE); // CRC32 + bool has_atomics = armField(regs, present, FF_ARM_REG_ISAR0, 20, 2); // ISAR0.ATOMIC[23:20] >= 2 -> HWCAP_ATOMICS + bool has_crc32 = armField(regs, present, FF_ARM_REG_ISAR0, 16, 1); // ISAR0.CRC32[19:16] >= 1 -> HWCAP_CRC32 + bool has_asimdrdm = armField(regs, present, FF_ARM_REG_ISAR0, 28, 1); // ISAR0.RDM[31:28] >= 1 -> HWCAP_ASIMDRDM // ARMv8.2-A - bool has_fp16 = IsProcessorFeaturePresent(PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE); // Half-precision FP + bool has_fphp = armField(regs, present, FF_ARM_REG_PFR0, 16, 1); // PFR0.FP[19:16] >= 1 -> HWCAP_FPHP + bool has_dcpop = armField(regs, present, FF_ARM_REG_ISAR1, 0, 1); // ISAR1.DPB[3:0] >= 1 -> HWCAP_DCPOP // ARMv8.3-A - bool has_lrcpc = IsProcessorFeaturePresent(PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE); // LDAPR/LR with RCPC semantics - bool has_jscvt = IsProcessorFeaturePresent(PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE); // FJCVTZS + bool has_paca = armField(regs, present, FF_ARM_REG_ISAR1, 4, 1) // ISAR1.APA[7:4] >= 1 -> HWCAP_PACA + || armField(regs, present, FF_ARM_REG_ISAR2, 12, 1); // ISAR2.APA3[15:12] >= 1 (newer encoding) + bool has_lrcpc = armField(regs, present, FF_ARM_REG_ISAR1, 20, 1); // ISAR1.LRCPC[23:20] >= 1 -> HWCAP_LRCPC + bool has_fcma = armField(regs, present, FF_ARM_REG_ISAR1, 16, 1); // ISAR1.FCMA[19:16] >= 1 -> HWCAP_FCMA + bool has_jscvt = armField(regs, present, FF_ARM_REG_ISAR1, 12, 1); // ISAR1.JSCVT[15:12] >= 1 -> HWCAP_JSCVT // ARMv8.4-A - // My CPU (Apple M1 Pro in VM) does support LSE2, but Windows doesn't detect it for some reason - bool has_lse2 = IsProcessorFeaturePresent(PF_ARM_LSE2_AVAILABLE); // Large System Extensions version 2, optional from v8.2 - bool has_dp = IsProcessorFeaturePresent(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE); // DotProd, optional from v8.1 (*) + bool has_dit = armField(regs, present, FF_ARM_REG_PFR0, 48, 1); // PFR0.DIT[51:48] >= 1 -> HWCAP_DIT + bool has_flagm = armField(regs, present, FF_ARM_REG_ISAR0, 52, 1); // ISAR0.FlagM[55:52] >= 1 -> HWCAP_FLAGM + bool has_ilrcpc = armField(regs, present, FF_ARM_REG_ISAR1, 20, 2); // ISAR1.LRCPC[23:20] >= 2 -> HWCAP_ILRCPC + + // ARMv8.5-A + bool has_bti = armField(regs, present, FF_ARM_REG_PFR1, 0, 1); // PFR1.BT[3:0] >= 1 -> HWCAP2_BTI + bool has_sb = armField(regs, present, FF_ARM_REG_ISAR1, 36, 1); // ISAR1.SB[39:36] >= 1 -> HWCAP_SB + bool has_dcpodp = armField(regs, present, FF_ARM_REG_ISAR1, 0, 2); // ISAR1.DPB[3:0] >= 2 -> HWCAP2_DCPODP + bool has_flagm2 = armField(regs, present, FF_ARM_REG_ISAR0, 52, 2); // ISAR0.FlagM[55:52] >= 2 -> HWCAP2_FLAGM2 + bool has_frint = armField(regs, present, FF_ARM_REG_ISAR1, 32, 1); // ISAR1.FRINTTS[35:32] >= 1 -> HWCAP2_FRINT // ARMv9.0-A - bool has_sve2 = IsProcessorFeaturePresent(PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE); // SVE2 + bool has_sve2 = armField(regs, present, FF_ARM_REG_ZFR0, 0, 1); // ZFR0.SVEver[3:0] >= 1 (1 = SVE2) -> HWCAP2_SVE2 // ARMv9.1-A // ARMv8.6-A - bool has_bf16 = IsProcessorFeaturePresent(PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE); // BF16, optional from v8.2 - bool has_i8mm = IsProcessorFeaturePresent(PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE); // Int8 matrix multiply, optional from v8.2 + // BF16/I8MM are shared by both version sequences, so the same booleans are + // tested in both branches of the chain below. + bool has_bf16 = armField(regs, present, FF_ARM_REG_ISAR1, 44, 1); // ISAR1.BF16[47:44] >= 1 -> HWCAP2_BF16 + bool has_i8mm = armField(regs, present, FF_ARM_REG_ISAR1, 52, 1); // ISAR1.I8MM[55:52] >= 1 -> HWCAP2_I8MM // ARMv8.7-A - bool has_ebf16 = IsProcessorFeaturePresent(PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE); // Extended BFloat16 behaviors, optional from v8.2 + bool has_afp = armField(regs, present, FF_ARM_REG_MMFR1, 44, 1); // MMFR1.AFP[47:44] >= 1 -> HWCAP2_AFP // ARMv9.2-A - bool has_sme = IsProcessorFeaturePresent(PF_ARM_SME_INSTRUCTIONS_AVAILABLE); // SME + bool has_sme = armField(regs, present, FF_ARM_REG_PFR1, 24, 1); // PFR1.SME[27:24] >= 1 -> HWCAP2_SME // ARMv9.3-A - bool has_sme2 = IsProcessorFeaturePresent(PF_ARM_SME2_INSTRUCTIONS_AVAILABLE); // SME2 + bool has_sme2 = armField(regs, present, FF_ARM_REG_PFR1, 24, 2); // PFR1.SME[27:24] >= 2 -> HWCAP2_SME2 + + // ARMv8.8-A + bool has_mops = armField(regs, present, FF_ARM_REG_ISAR2, 16, 1); // ISAR2.MOPS[19:16] >= 1 -> HWCAP2_MOPS + + // ARMv8.9-A + bool has_cssc = armField(regs, present, FF_ARM_REG_ISAR2, 52, 1); // ISAR2.CSSC[55:52] >= 1 -> HWCAP2_CSSC // ARMv9.4-A - bool has_sme2p1 = IsProcessorFeaturePresent(PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE); // SME2.1 + bool has_sme2p1 = armField(regs, present, FF_ARM_REG_SMFR0, 56, 2); // SMFR0.SMEver[59:56] >= 2 -> HWCAP2_SME2P1 + // ARMv9.5-A + bool has_f8e4m3 = armBit(regs, present, FF_ARM_REG_FPFR0, 1); // FPFR0.F8E4M3[1] -> HWCAP2_F8E4M3 + bool has_f8e5m2 = armBit(regs, present, FF_ARM_REG_FPFR0, 0); // FPFR0.F8E5M2[0] -> HWCAP2_F8E5M2 + + // ARMv9.6-A + bool has_cmpbr = armField(regs, present, FF_ARM_REG_ISAR2, 52, 2); // ISAR2.CSSC[55:52] >= 2 -> HWCAP_CMPBR + bool has_fprcvt = armField(regs, present, FF_ARM_REG_ISAR3, 28, 1); // ISAR3.FPRCVT[31:28] >= 1 -> HWCAP_FPRCVT + + // The version chain (both the ARMv9/ARMv8 split and the order of the + // checks) must stay identical to the Linux implementation above, otherwise + // the same CPU reports a different march depending on the OS. if (has_sve2 || has_sme) { - // ARMv9 family - if (has_sme2p1) { + // ARMv9 + if (has_cmpbr && has_fprcvt) { + cpu->march = "ARMv9.6-A"; + } else if (has_f8e5m2 && has_f8e4m3) { + cpu->march = "ARMv9.5-A"; + } else if (has_sme2p1) { cpu->march = "ARMv9.4-A"; } else if (has_sme2) { cpu->march = "ARMv9.3-A"; @@ -497,20 +662,26 @@ void ffCPUDetectByCpuid(FFCPUResult* cpu) { cpu->march = "ARMv9.0-A"; } } else { - // ARMv8 family - if (has_ebf16) { + // ARMv8 + if (has_cssc) { + cpu->march = "ARMv8.9-A"; + } else if (has_mops) { + cpu->march = "ARMv8.8-A"; + } else if (has_afp) { cpu->march = "ARMv8.7-A"; } else if (has_i8mm && has_bf16) { cpu->march = "ARMv8.6-A"; - } else if (has_dp && has_lse2) { + } else if (has_bti && has_sb && has_dcpodp && has_flagm2 && has_frint) { + cpu->march = "ARMv8.5-A"; + } else if (has_dit && has_flagm && has_ilrcpc) { cpu->march = "ARMv8.4-A"; - } else if (has_lrcpc && has_jscvt) { + } else if (has_paca && has_lrcpc && has_fcma && has_jscvt) { cpu->march = "ARMv8.3-A"; - } else if (has_fp16) { + } else if (has_fphp && has_dcpop) { cpu->march = "ARMv8.2-A"; - } else if (has_atomics && has_crc32) { + } else if (has_atomics && has_crc32 && has_asimdrdm) { cpu->march = "ARMv8.1-A"; - } else if (has_neon && has_vfp) { + } else if (has_asimd && has_fp) { cpu->march = "ARMv8-A"; } } diff --git a/src/detection/displayserver/linux/wayland/mutter.c b/src/detection/displayserver/linux/wayland/mutter.c new file mode 100644 index 000000000..7a37bfc6f --- /dev/null +++ b/src/detection/displayserver/linux/wayland/mutter.c @@ -0,0 +1,426 @@ +#include "wayland.h" + +#if defined(FF_HAVE_WAYLAND) && defined(FF_HAVE_DBUS) && defined(__linux__) + + #include "common/arrutil.h" + #include "common/dbus.h" + #include "common/debug.h" + #include "common/edidHelper.h" + #include "common/strutil.h" + + #include + +// Read the display configuration of GNOME / Mutter and Cinnamon / Muffin, including the raw EDID +// data, instead of guessing the DRM connector by matching the wl_output name (#...). +// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.gnome.Mutter.DisplayConfig.xml + + // MetaMonitorColorMode + // https://gitlab.gnome.org/GNOME/mutter/-/blob/main/src/backends/meta-monitor-manager-private.h + #define FF_MUTTER_COLOR_MODE_BT2100 1 + +// Muffin (Cinnamon) exposes the very same interface as Mutter (GNOME), only the names differ +typedef struct FFMutterDisplayConfig { + const char* busName; + const char* objectPath; + const char* interface; +} FFMutterDisplayConfig; + +static const FFMutterDisplayConfig mutterDisplayConfigs[] = { + { + .busName = "org.gnome.Mutter.DisplayConfig", + .objectPath = "/org/gnome/Mutter/DisplayConfig", + .interface = "org.gnome.Mutter.DisplayConfig", + }, + { + .busName = "org.cinnamon.Muffin.DisplayConfig", + .objectPath = "/org/cinnamon/Muffin/DisplayConfig", + .interface = "org.cinnamon.Muffin.DisplayConfig", + }, +}; + +static inline DBusMessage* mutterCall(FFDBusData* dbus, const FFMutterDisplayConfig* config, const char* method) { + return ffDBusGetMethodReply(dbus, config->busName, config->objectPath, config->interface, method, nullptr, nullptr); +} + +// Returns the first display configuration interface that responds, or nullptr +static const FFMutterDisplayConfig* mutterFindConfig(FFDBusData* dbus) { + for (uint32_t i = 0; i < ARRAY_SIZE(mutterDisplayConfigs); ++i) { + DBusMessage* reply = mutterCall(dbus, &mutterDisplayConfigs[i], "GetCurrentState"); + if (reply != nullptr) { + dbus->lib->ffdbus_message_unref(reply); + return &mutterDisplayConfigs[i]; + } + } + + return nullptr; +} + +static FFDisplayResult* mutterFindDisplay(FFDisplayServerResult* result, const char* connector) { + if (connector == nullptr || *connector == '\0') { + return nullptr; + } + + const uint64_t id = ffWaylandGenerateIdFromName(connector); + FF_LIST_FOR_EACH (FFDisplayResult, display, result->displays) { + if (display->id == id) { + display->platformApi = "wayland-mutter"; + return display; + } + } + + return nullptr; +} + +// Point `value` to the variant stored under `key` in the `a{sv}` dictionary `dict` +static bool mutterDictGet(FFDBusData* dbus, DBusMessageIter* dict, const char* key, DBusMessageIter* value) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(dict) != DBUS_TYPE_ARRAY) { + return false; + } + + DBusMessageIter entries; + dbus->lib->ffdbus_message_iter_recurse(dict, &entries); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&entries) == DBUS_TYPE_DICT_ENTRY) { + DBusMessageIter entry; + dbus->lib->ffdbus_message_iter_recurse(&entries, &entry); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_STRING) { + const char* entryKey = nullptr; + dbus->lib->ffdbus_message_iter_get_basic(&entry, &entryKey); + if (entryKey && ffStrEquals(entryKey, key) && dbus->lib->ffdbus_message_iter_next(&entry)) { + *value = entry; + return true; + } + } + + if (!dbus->lib->ffdbus_message_iter_next(&entries)) { + break; + } + } + + return false; +} + +// Whether the `au` array contained in the variant `variant` contains `needle` +static bool mutterVariantContainsUint32(FFDBusData* dbus, DBusMessageIter* variant, uint32_t needle) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(variant) != DBUS_TYPE_VARIANT) { + return false; + } + + DBusMessageIter array; + dbus->lib->ffdbus_message_iter_recurse(variant, &array); + if (dbus->lib->ffdbus_message_iter_get_arg_type(&array) != DBUS_TYPE_ARRAY) { + return false; + } + + DBusMessageIter items; + dbus->lib->ffdbus_message_iter_recurse(&array, &items); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&items) == DBUS_TYPE_UINT32) { + uint32_t item = 0; + dbus->lib->ffdbus_message_iter_get_basic(&items, &item); + if (item == needle) { + return true; + } + if (!dbus->lib->ffdbus_message_iter_next(&items)) { + break; + } + } + + return false; +} + +// Copy the `ay` byte array contained in the variant `variant` into `data` +static bool mutterVariantGetBytes(FFDBusData* dbus, DBusMessageIter* variant, uint8_t* data, uint32_t capacity, uint32_t* length) { + uint32_t count = 0; + + if (dbus->lib->ffdbus_message_iter_get_arg_type(variant) == DBUS_TYPE_VARIANT) { + DBusMessageIter array; + dbus->lib->ffdbus_message_iter_recurse(variant, &array); + if (dbus->lib->ffdbus_message_iter_get_arg_type(&array) == DBUS_TYPE_ARRAY) { + DBusMessageIter items; + dbus->lib->ffdbus_message_iter_recurse(&array, &items); + while (dbus->lib->ffdbus_message_iter_get_arg_type(&items) == DBUS_TYPE_BYTE) { + uint8_t byte = 0; + dbus->lib->ffdbus_message_iter_get_basic(&items, &byte); + if (count < capacity) { + data[count] = byte; + } + ++count; + if (!dbus->lib->ffdbus_message_iter_next(&items)) { + break; + } + } + } + } + + *length = count; + return count > 0 && count <= capacity; +} + +static void mutterApplyEdid(FFDisplayResult* display, const uint8_t* edid, uint32_t length) { + FF_STRBUF_AUTO_DESTROY edidName = ffStrbufCreate(); + if (ffEdidGetName(edid, &edidName) && edidName.length > 0) { + ffStrbufSet(&display->name, &edidName); + } + + if (display->serial.length == 0) { + ffEdidGetSerial(edid, &display->serial); + } + + if (display->manufactureYear == 0) { + ffEdidGetManufactureDate(edid, &display->manufactureYear, &display->manufactureWeek); + } + + if (display->hdrStatus == FF_DISPLAY_HDR_STATUS_UNKNOWN) { + display->hdrStatus = ffEdidGetHdrCompatible(edid, length) + ? FF_DISPLAY_HDR_STATUS_SUPPORTED + : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + + if (display->preferredWidth == 0) { + uint32_t width = 0, height = 0; + double refreshRate = 0; + ffEdidGetPreferredResolutionAndRefreshRate(edid, &width, &height, &refreshRate); + display->preferredWidth = width; + display->preferredHeight = height; + display->preferredRefreshRate = refreshRate; + } +} + +static void mutterApplyMonitorProperties(FFDBusData* dbus, DBusMessageIter* properties, FFDisplayResult* display) { + DBusMessageIter value; + + // "is-builtin" is authoritative, unlike ffdsGetDisplayType() which guesses from the connector name + if (mutterDictGet(dbus, properties, "is-builtin", &value)) { + bool builtin = false; + if (ffDBusGetBool(dbus, &value, &builtin)) { + display->type = builtin ? FF_DISPLAY_TYPE_BUILTIN : FF_DISPLAY_TYPE_EXTERNAL; + } + } + + bool hdrEnabled = false; + if (mutterDictGet(dbus, properties, "color-mode", &value)) { + uint64_t colorMode = 0; + hdrEnabled = ffDBusGetUint(dbus, &value, &colorMode) && colorMode == FF_MUTTER_COLOR_MODE_BT2100; + } + + bool hdrSupported = false; + if (mutterDictGet(dbus, properties, "supported-color-modes", &value)) { + hdrSupported = mutterVariantContainsUint32(dbus, &value, FF_MUTTER_COLOR_MODE_BT2100); + } + + if (hdrEnabled) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } else if (hdrSupported && display->hdrStatus == FF_DISPLAY_HDR_STATUS_UNKNOWN) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } +} + +static void mutterParseCurrentState(FFDBusData* dbus, DBusMessage* reply, FFDisplayServerResult* result) { + DBusMessageIter root; + if (!dbus->lib->ffdbus_message_iter_init(reply, &root)) { + return; + } + + // (u serial, a(monitors) monitors, a(logical_monitors) logical_monitors, a{sv} properties) + if (!dbus->lib->ffdbus_message_iter_next(&root)) { // monitors + return; + } + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&root) == DBUS_TYPE_ARRAY) { + DBusMessageIter monitors; + dbus->lib->ffdbus_message_iter_recurse(&root, &monitors); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&monitors) == DBUS_TYPE_STRUCT) { + DBusMessageIter monitor; + dbus->lib->ffdbus_message_iter_recurse(&monitors, &monitor); + + // ((ssss) monitor_specs, a(siiddada{sv}) modes, a{sv} properties) + if (dbus->lib->ffdbus_message_iter_get_arg_type(&monitor) == DBUS_TYPE_STRUCT) { + DBusMessageIter specs; + dbus->lib->ffdbus_message_iter_recurse(&monitor, &specs); + + const char* connector = nullptr; + if (dbus->lib->ffdbus_message_iter_get_arg_type(&specs) == DBUS_TYPE_STRING) { + dbus->lib->ffdbus_message_iter_get_basic(&specs, &connector); + } + + FFDisplayResult* display = mutterFindDisplay(result, connector); + if (display) { + // (connector, vendor, product, serial) + DBusMessageIter serialSpec = specs; + if (display->serial.length == 0 && + dbus->lib->ffdbus_message_iter_next(&serialSpec) && // vendor + dbus->lib->ffdbus_message_iter_next(&serialSpec) && // product + dbus->lib->ffdbus_message_iter_next(&serialSpec) && // serial + dbus->lib->ffdbus_message_iter_get_arg_type(&serialSpec) == DBUS_TYPE_STRING) { + const char* serial = nullptr; + dbus->lib->ffdbus_message_iter_get_basic(&serialSpec, &serial); + if (serial && !ffStrEquals(serial, "unknown")) { + ffStrbufSetS(&display->serial, serial); + } + } + + // Skip `modes`, then read `properties` + DBusMessageIter properties = monitor; + if (dbus->lib->ffdbus_message_iter_next(&properties) && + dbus->lib->ffdbus_message_iter_next(&properties) && + dbus->lib->ffdbus_message_iter_get_arg_type(&properties) == DBUS_TYPE_ARRAY) { + mutterApplyMonitorProperties(dbus, &properties, display); + } + } + } + + dbus->lib->ffdbus_message_iter_next(&monitors); + } + } + + // logical_monitors -> primary flag + if (!dbus->lib->ffdbus_message_iter_next(&root) || + dbus->lib->ffdbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) { + return; + } + + DBusMessageIter logicalMonitors; + dbus->lib->ffdbus_message_iter_recurse(&root, &logicalMonitors); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitors) == DBUS_TYPE_STRUCT) { + DBusMessageIter logicalMonitor; + dbus->lib->ffdbus_message_iter_recurse(&logicalMonitors, &logicalMonitor); + + // (i x, i y, d scale, u transform, b primary, a(ssss) monitors, a{sv} properties) + int index = 0; + bool primary = false; + + do { + const int argType = dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitor); + + if (index == 4 && argType == DBUS_TYPE_BOOLEAN) { + dbus_bool_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(&logicalMonitor, &value); + primary = value != 0; + } else if (index == 5 && primary && argType == DBUS_TYPE_ARRAY) { + DBusMessageIter logicalMonitorMonitors; + dbus->lib->ffdbus_message_iter_recurse(&logicalMonitor, &logicalMonitorMonitors); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&logicalMonitorMonitors) == DBUS_TYPE_STRUCT) { + DBusMessageIter specs; + dbus->lib->ffdbus_message_iter_recurse(&logicalMonitorMonitors, &specs); + if (dbus->lib->ffdbus_message_iter_get_arg_type(&specs) == DBUS_TYPE_STRING) { + const char* connector = nullptr; + dbus->lib->ffdbus_message_iter_get_basic(&specs, &connector); + FFDisplayResult* display = mutterFindDisplay(result, connector); + if (display) { + display->primary = true; + } + } + dbus->lib->ffdbus_message_iter_next(&logicalMonitorMonitors); + } + } + + ++index; + } while (dbus->lib->ffdbus_message_iter_next(&logicalMonitor)); + + dbus->lib->ffdbus_message_iter_next(&logicalMonitors); + } +} + +static void mutterParseResources(FFDBusData* dbus, DBusMessage* reply, FFDisplayServerResult* result) { + DBusMessageIter root; + if (!dbus->lib->ffdbus_message_iter_init(reply, &root)) { + return; + } + + // (u serial, a(crtcs) crtcs, a(outputs) outputs, a(modes) modes, i max_screen_width, i max_screen_height) + DBusMessageIter outputs; + if (!dbus->lib->ffdbus_message_iter_next(&root) || // crtcs + !dbus->lib->ffdbus_message_iter_next(&root) || // outputs + dbus->lib->ffdbus_message_iter_get_arg_type(&root) != DBUS_TYPE_ARRAY) { + return; + } + dbus->lib->ffdbus_message_iter_recurse(&root, &outputs); + + while (dbus->lib->ffdbus_message_iter_get_arg_type(&outputs) == DBUS_TYPE_STRUCT) { + DBusMessageIter output; + dbus->lib->ffdbus_message_iter_recurse(&outputs, &output); + + // (u id, x output_id, i crtc_index, au crtcs, s name, au modes, au clones, a{sv} properties) + FFDisplayResult* display = nullptr; + int index = 0; + + do { + const int argType = dbus->lib->ffdbus_message_iter_get_arg_type(&output); + + if (index == 4 && argType == DBUS_TYPE_STRING) { + const char* connector = nullptr; + dbus->lib->ffdbus_message_iter_get_basic(&output, &connector); + display = mutterFindDisplay(result, connector); + } else if (index == 7 && display != nullptr && argType == DBUS_TYPE_ARRAY) { + DBusMessageIter value; + if (mutterDictGet(dbus, &output, "edid", &value)) { + uint8_t edid[1024]; + uint32_t length = 0; + if (mutterVariantGetBytes(dbus, &value, edid, ARRAY_SIZE(edid), &length) && ffEdidIsValid(edid, length)) { + mutterApplyEdid(display, edid, length); + } + } + } + + ++index; + } while (dbus->lib->ffdbus_message_iter_next(&output)); + + dbus->lib->ffdbus_message_iter_next(&outputs); + } +} + +// Whether org.gnome.Mutter.DisplayConfig (GNOME) or org.cinnamon.Muffin.DisplayConfig (Cinnamon) +// is reachable. Used to decide if the wl_output name based DRM connector matching +// (matchDrmConnector) can be skipped. +bool ffdsMutterAvailable(void) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != nullptr) { + return false; + } + + return mutterFindConfig(&dbus) != nullptr; +} + +// Enrich the displays detected from wl_output with the authoritative data reported by Mutter / Muffin. +// Returns true when the D-Bus connection is up and DisplayConfig could be queried. The caller treats +// that as authoritative and skips monitors.xml, even if the reply names no primary monitor. +bool ffdsConnectMutter(FFDisplayServerResult* result) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + const char* error = ffDBusLoadData(DBUS_BUS_SESSION, &dbus); + if (error) { + FF_DEBUG("ffDBusLoadData() failed: %s", error); + return false; + } + + const FFMutterDisplayConfig* config = mutterFindConfig(&dbus); + if (config == nullptr) { + FF_DEBUG("No Mutter / Muffin DisplayConfig interface found"); + return false; + } + + DBusMessage* reply = mutterCall(&dbus, config, "GetCurrentState"); + if (reply == nullptr) { + FF_DEBUG("GetCurrentState() failed"); + return false; + } + mutterParseCurrentState(&dbus, reply, result); + dbus.lib->ffdbus_message_unref(reply); + + // GetCurrentState() does not expose the EDID data, so fall back to the legacy GetResources() + reply = mutterCall(&dbus, config, "GetResources"); + if (reply != nullptr) { + mutterParseResources(&dbus, reply, result); + dbus.lib->ffdbus_message_unref(reply); + } else { + FF_DEBUG("GetResources() failed"); + } + + return true; +} + +#endif diff --git a/src/detection/displayserver/linux/wayland/wayland.c b/src/detection/displayserver/linux/wayland/wayland.c index 29c1d46e9..ff29dc2c0 100644 --- a/src/detection/displayserver/linux/wayland/wayland.c +++ b/src/detection/displayserver/linux/wayland/wayland.c @@ -157,7 +157,7 @@ void ffWaylandOutputNameListener(void* data, [[maybe_unused]] void* output, cons display->type = ffdsGetDisplayType(name); #if __linux__ - if (!display->edidName.length) { + if (!display->edidName.length && !display->parent->useMutter) { matchDrmConnector(name, display); } #endif @@ -282,6 +282,14 @@ const char* ffdsConnectWayland(FFDisplayServerResult* result) { waylandDetectWM(ffwl_display_get_fd(data.display), result); + #if defined(FF_HAVE_DBUS) && defined(__linux__) + // gnome-shell (Mutter) and cinnamon (Muffin) expose the monitor configuration via D-Bus, + // including the raw EDID. Prefer it over matching the wl_output name against the DRM connectors. + data.useMutter = + (ffStrbufEqualS(&result->wmProcessName, "gnome-shell") || ffStrbufEqualS(&result->wmProcessName, "cinnamon")) && + ffdsMutterAvailable(); + #endif + struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, nullptr); if (registry == nullptr) { ffwl_display_disconnect(data.display); @@ -309,7 +317,18 @@ const char* ffdsConnectWayland(FFDisplayServerResult* result) { data.ffwl_proxy_destroy(registry); ffwl_display_disconnect(data.display); - { + bool displayConfigRead = false; + #if defined(FF_HAVE_DBUS) && defined(__linux__) + if (data.useMutter) { + // A working D-Bus connection with a reachable DisplayConfig is authoritative. + // Don't second-guess it with monitors.xml even when the reply names no primary + // monitor (e.g. connector names that happen to match no wl_output). + displayConfigRead = ffdsConnectMutter(result); + } + #endif + + // Fallback for compositors that do not expose the monitor configuration, or when it could not be read + if (!displayConfigRead) { const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell") ? "monitors.xml" : ffStrbufEqualS(&result->wmProcessName, "cinnamon") diff --git a/src/detection/displayserver/linux/wayland/wayland.h b/src/detection/displayserver/linux/wayland/wayland.h index e225eb881..6a34d0833 100644 --- a/src/detection/displayserver/linux/wayland/wayland.h +++ b/src/detection/displayserver/linux/wayland/wayland.h @@ -37,6 +37,7 @@ typedef struct WaylandData { WaylandProtocolType protocolType; struct wl_proxy* zxdgOutputManager; struct wl_proxy* wpColorManager; + bool useMutter; // gnome-shell: read the display configuration from org.gnome.Mutter.DisplayConfig instead of guessing it } WaylandData; typedef struct WaylandDisplay { @@ -92,4 +93,9 @@ const char* ffWaylandHandleKdeOutputOrder(WaylandData* wldata, struct wl_registr const char* ffWaylandHandleZxdgOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); const char* ffWaylandHandleWpColor(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); + #if defined(FF_HAVE_DBUS) && defined(__linux__) +bool ffdsMutterAvailable(void); +bool ffdsConnectMutter(FFDisplayServerResult* result); + #endif + #endif diff --git a/src/detection/gamepad/gamepad_apple.m b/src/detection/gamepad/gamepad_apple.m index 44f8baa72..6f713a8bb 100644 --- a/src/detection/gamepad/gamepad_apple.m +++ b/src/detection/gamepad/gamepad_apple.m @@ -165,6 +165,7 @@ typedef struct FFFindGamepadContext { // The GameController framework's own devices, and how many of them there were. `set` is the IOKit // side, used only to borrow the serial -- see `findHIDDeviceForController()`. static uint32_t findGameControllerDevices(FFlist* devices, NSSet* set) { +#ifdef MAC_OS_VERSION_11_0 if (@available(macOS 11.0, *)) { // See the note at the top: the list stays empty until the run loop has been pumped once, and // the timeout only bounds the case where the framework has nothing to report at all. The @@ -222,6 +223,8 @@ static uint32_t findGameControllerDevices(FFlist* devices, NSSet* set) { } return count; } +#endif + return 0; } @@ -234,6 +237,7 @@ static uint32_t findGameControllerDevices(FFlist* devices, NSSet* set) { // Apple's whitelist, fails this and skips the wait entirely instead of sitting through a timeout for // a list that will never contain it. static bool anyDeviceIsClaimedByGameController(NSSet* set) { +#ifdef MAC_OS_VERSION_11_0 if (@available(macOS 11.0, *)) { for (id value in set) { if ([GCController supportsHIDDevice: (__bridge IOHIDDeviceRef) value]) { @@ -241,12 +245,15 @@ static bool anyDeviceIsClaimedByGameController(NSSet* set) { } } } +#endif + return false; } static void enumSet(IOHIDDeviceRef value, FFFindGamepadContext* ctx) { // A device the framework owns is already in the list under its own name; see the note at the // top for why this is conditional on the framework having contributed anything at all. +#ifdef MAC_OS_VERSION_11_0 if (ctx->gameControllerCount > 0) { if (@available(macOS 11.0, *)) { if ([GCController supportsHIDDevice:value]) { @@ -254,6 +261,7 @@ static void enumSet(IOHIDDeviceRef value, FFFindGamepadContext* ctx) { } } } +#endif FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *ctx->devices); ffStrbufInit(&device->serial); diff --git a/src/detection/gpu/gpu_pci.c b/src/detection/gpu/gpu_pci.c index 2aa98745c..02f82d6a4 100644 --- a/src/detection/gpu/gpu_pci.c +++ b/src/detection/gpu/gpu_pci.c @@ -272,7 +272,7 @@ static bool loadAmdGpuIdsInc(uint16_t deviceId, uint8_t revision, FFGPUResult* g static void parseAmdGpuIdsFile(uint16_t deviceId, uint8_t revision, FFGPUResult* gpu) { char query[32]; - snprintf(query, ARRAY_SIZE(query), "%X,\t%X,", (unsigned) deviceId, (unsigned) revision); + snprintf(query, ARRAY_SIZE(query), "%04X,\t%02X,", (unsigned) deviceId, (unsigned) revision); #ifdef FF_CUSTOM_AMDGPU_IDS_PATH FF_DEBUG("Searching AMD GPU IDs at custom path '%s' for device=0x%04x revision=0x%02x", FF_STR(FF_CUSTOM_AMDGPU_IDS_PATH), deviceId, revision); ffParsePropFile(FF_STR(FF_CUSTOM_AMDGPU_IDS_PATH), query, &gpu->name); diff --git a/src/detection/terminalshell/terminalshell_linux.c b/src/detection/terminalshell/terminalshell_linux.c index e5ff41442..9fa06f454 100644 --- a/src/detection/terminalshell/terminalshell_linux.c +++ b/src/detection/terminalshell/terminalshell_linux.c @@ -59,6 +59,9 @@ static pid_t getShellInfo(FFShellResult* result, pid_t pid) { ffStrbufEqualS(&result->processName, "valgrind") || ffStrbufEqualS(&result->processName, "fastfetch") || // #994 ffStrbufEqualS(&result->processName, "flashfetch") || + ffStrbufEqualS(&result->processName, "mise") || + ffStrbufEqualS(&result->processName, "mise-shim") || + ffStrbufEqualS(&result->processName, "atuin") || ffStrbufEqualS(&result->processName, "proot") || ffStrbufEqualS(&result->processName, "script") || #ifdef __linux__ diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c index b241721ff..eb5d59ffc 100644 --- a/src/detection/terminalshell/terminalshell_windows.c +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -32,7 +32,7 @@ static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) { if ( !gui && (ffStrbufIgnCaseEqualS(&result->prettyName, "sudo") || ffStrbufIgnCaseEqualS(&result->prettyName, "su") || ffStrbufIgnCaseEqualS(&result->prettyName, "gdb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb-dap") || ffStrbufIgnCaseEqualS(&result->prettyName, "python") || // python on windows generates shim executables ffStrbufIgnCaseEqualS(&result->prettyName, "fastfetch") || // scoop warps the real binaries with a "shim" exe - ffStrbufIgnCaseEqualS(&result->prettyName, "flashfetch") || ffStrbufContainIgnCaseS(&result->prettyName, "debug") || ffStrbufContainIgnCaseS(&result->prettyName, "time") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "ConEmuC") // https://github.com/fastfetch-cli/fastfetch/issues/488#issuecomment-1619982014 + ffStrbufIgnCaseEqualS(&result->prettyName, "flashfetch") || ffStrbufIgnCaseEqualS(&result->prettyName, "mise") || ffStrbufIgnCaseEqualS(&result->prettyName, "mise-shim") || ffStrbufContainIgnCaseS(&result->prettyName, "debug") || ffStrbufContainIgnCaseS(&result->prettyName, "time") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "ConEmuC") // https://github.com/fastfetch-cli/fastfetch/issues/488#issuecomment-1619982014 )) { ffStrbufClear(&result->processName); ffStrbufClear(&result->prettyName); diff --git a/src/detection/uptime/uptime_sunos.c b/src/detection/uptime/uptime_sunos.c index 1e6aa4fd2..b76b33889 100644 --- a/src/detection/uptime/uptime_sunos.c +++ b/src/detection/uptime/uptime_sunos.c @@ -1,19 +1,37 @@ #include "uptime.h" +#include "common/debug.h" #include "common/time.h" -#include +#include + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} const char* ffDetectUptime(FFUptimeResult* result) { - struct utmpx* ut; - - setutxent(); - while (nullptr != (ut = getutxent())) { - if (ut->ut_type == BOOT_TIME) { - result->bootTime = (uint64_t) ut->ut_tv.tv_sec * 1000 + (uint64_t) ut->ut_tv.tv_usec / 1000000; - result->uptime = ffTimeGetNow() - result->bootTime; - break; - } + [[gnu::cleanup(kstatFreeWrap)]] kstat_ctl_t* kc = kstat_open(); + if (!kc) { + FF_DEBUG("kstat_open() failed"); + return "kstat_open() failed"; } - endutxent(); + + kstat_t* ks = kstat_lookup(kc, "unix", 0, "system_misc"); + if (!ks || kstat_read(kc, ks, nullptr) < 0) { + FF_DEBUG("kstat_lookup(unix:0:system_misc) failed"); + return "kstat_lookup(unix:0:system_misc) failed"; + } + + kstat_named_t* bootTime = (kstat_named_t*) kstat_data_lookup(ks, "boot_time"); + if (!bootTime) { + FF_DEBUG("kstat_data_lookup(boot_time) failed"); + return "kstat_data_lookup(boot_time) failed"; + } + + result->bootTime = (uint64_t) bootTime->value.ui32 * 1000; + result->uptime = ffTimeGetNow() - result->bootTime; + return nullptr; } diff --git a/src/detection/wifi/wifi_apple.m b/src/detection/wifi/wifi_apple.m index 658a43d31..8d91dad21 100644 --- a/src/detection/wifi/wifi_apple.m +++ b/src/detection/wifi/wifi_apple.m @@ -52,15 +52,15 @@ const char* ffDetectWifi(FFlist* result) { if (inf.ssid) { // https://developer.apple.com/forums/thread/732431 ffStrbufAppendS(&item->conn.ssid, inf.ssid.UTF8String); - } else if (networkProfile.ssid) { - ffStrbufSetStatic(&item->conn.ssid, inf.configuration.networkProfiles.firstObject.ssid.UTF8String); + } else if (networkProfile.ssid) { // Dead in macOS 27.2 + ffStrbufSetStatic(&item->conn.ssid, networkProfile.ssid.UTF8String); } else { - ffStrbufSetStatic(&item->conn.ssid, ""); // https://developer.apple.com/forums/thread/732431 + ffStrbufSetStatic(&item->conn.ssid, ""); } if (inf.bssid) { ffStrbufAppendS(&item->conn.bssid, inf.bssid.UTF8String); - } else if (networkProfile.bssidList) { + } else if (networkProfile.bssidList) { // Dead in macOS 27.2 ffStrbufSetStatic(&item->conn.bssid, [networkProfile.bssidList.firstObject[@"BSSID"] UTF8String]); } else { ffStrbufSetStatic(&item->conn.bssid, ""); diff --git a/src/fastfetch.c b/src/fastfetch.c index 4591301b9..7c3c45ce9 100644 --- a/src/fastfetch.c +++ b/src/fastfetch.c @@ -2,6 +2,7 @@ #include "common/ffdata.h" #include "detection/version/version.h" #include "logo/logo.h" +#include "logo/spin/spin.h" #include "common/FFcache.h" #include "common/commandoption.h" #include "common/genconfig.h" @@ -754,6 +755,12 @@ static void parseConfigFiles(FFdata* data) { } } +// `-30`, `-.5`: a negative number is the value of the previous option, not the +// name of the next one (`--logo-spin-tilt -30`). +static bool isNegativeNumber(const char* str) { + return str[0] == '-' && (isdigit((unsigned char) str[1]) || (str[1] == '.' && isdigit((unsigned char) str[2]))); +} + static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(FFdata* data, char* key, char* value)) { for (int i = 1; i < argc; i++) { const char* key = argv[i]; @@ -768,6 +775,7 @@ static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(F if (i == argc - 1 || (argv[i + 1][0] == '-' && argv[i + 1][1] != '\0' && // `-` is used as an alias for `/dev/stdin` !ffStrEqualsIgnCase(argv[i], "--separator-string") // Separator string can start with a - + && !isNegativeNumber(argv[i + 1]) // A negative number is a value )) { parser(data, argv[i], nullptr); } else { @@ -817,6 +825,12 @@ static void run(FFdata* data) { fputs("\e[H", stdout); // Move cursor to the top left corner to overwrite the previous output instance.state.keysHeight = 0; // Reset keysHeight so `ffLogoPrintRemaining` will recalculate it ffCacheInvalidateAll(); // Drop the detection results cached for the previous round, so that this round re-detects them + // `--logo-type spin`: one rotation step per refresh, so the logo turns in sync with + // the modules instead of owning a timer of its own. A still logo, or any other logo + // type, costs a single `false` here. + if (ffSpinTick()) { + ffLogoPrint(); + } } else { break; } diff --git a/src/fastfetch.h b/src/fastfetch.h index a8d34869f..6e3e0bd71 100644 --- a/src/fastfetch.h +++ b/src/fastfetch.h @@ -27,6 +27,17 @@ typedef struct FFLogoLineCacheState { uint32_t rightOffset; } FFLogoLineCacheState; +// `--logo-type spin`: kept here instead of in a file-static block so that the +// state lives with the rest of the run and starts clean on every invocation. +typedef struct FFLogoSpinState { + float degrees; // rotation of the frame drawn last + double lastDrawMs; // when that frame was drawn + bool started; // `logo.spin.angle` has been applied + bool printed; // a frame was drawn, so `ffSpinTick` has work to do + uint32_t printedWidth; + uint32_t printedHeight; +} FFLogoSpinState; + typedef struct FFstate { uint32_t logoWidth; uint32_t logoHeight; @@ -35,6 +46,7 @@ typedef struct FFstate { uint32_t dynamicInterval; FFPlatform platform; FFLogoLineCacheState logoLineCache; + FFLogoSpinState logoSpin; } FFstate; typedef struct FFinstance { diff --git a/src/logo/ascii/a.inc b/src/logo/ascii/a.inc index c20136860..adf6de03d 100644 --- a/src/logo/ascii/a.inc +++ b/src/logo/ascii/a.inc @@ -615,6 +615,17 @@ static const FFlogo A[] = { .colorTitle = FF_COLOR_FG_DEFAULT, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_ARMADA + // ArmadaOS + { + .names = { "Armada" }, + .lines = FASTFETCH_DATATEXT_LOGO_ARMADA, + .colors = { + FF_COLOR_FG_DEFAULT, + FF_COLOR_FG_BLUE, + }, + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_ARMBIAN // Armbian { @@ -845,6 +856,25 @@ static const FFlogo A[] = { .colorTitle = FF_COLOR_FG_DEFAULT, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_ASTROS + // AstrOS + { + .names = { "AstrOS" }, + .lines = FASTFETCH_DATATEXT_LOGO_ASTROS, + .colors = { + FF_COLOR_FG_RGB "54;19;170", + FF_COLOR_FG_RGB "73;32;182", + FF_COLOR_FG_RGB "91;46;193", + FF_COLOR_FG_RGB "110;59;205", + FF_COLOR_FG_RGB "129;73;216", + FF_COLOR_FG_RGB "148;86;228", + FF_COLOR_FG_RGB "166;100;239", + FF_COLOR_FG_RGB "185;113;251", + }, + .colorKeys = FF_COLOR_FG_RGB "148;86;228", + .colorTitle = FF_COLOR_FG_RGB "185;113;251", + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_JANUSLINUX // Ataraxia { diff --git a/src/logo/ascii/a/armada.txt b/src/logo/ascii/a/armada.txt new file mode 100644 index 000000000..2eee32843 --- /dev/null +++ b/src/logo/ascii/a/armada.txt @@ -0,0 +1,17 @@ + # + #\ + /## + ### + #### + ##### #\ + ###### ## + ####### ##\ + ######## |### + ########/ ####\ + /######### ####| + ##########/ /##### + ############ #####| + #############/ ######/ + ##############/ ####### + #######^^^^^^^^/ /######/ +.#^'' `'^#' diff --git a/src/logo/ascii/a/astros.txt b/src/logo/ascii/a/astros.txt new file mode 100644 index 000000000..01f235cf4 --- /dev/null +++ b/src/logo/ascii/a/astros.txt @@ -0,0 +1,20 @@ + $7▁▃▅▅▅▅▃$8▁ + $7▗████████▖ + $6▟$7██████████▙ + $6▟███████$7█████▙ + $6▗████████████████$7▖ + $5▗██$6████████████████▖ + $5▟█████████$6███████████▙ + $5▟████████▛▔▔▔▔▜██$6██████▙ ▝▀▀▀▇▇▅▃ + $4▗▇███$5█████▘ ▝████████$6█▖ ▜██▙ + $4▗█████████▘ $5▝█████████▖ $6▟███ + $4▄█████████▘ ▐▌ $5▝█████████▙ ▟███$6▌ + $3▟████$4████▛ ▁▂▃▅██▅▃▂▂ ▜█$5███████▋▁▄████▘ + $3▗▟████████▛ $4▔▀██▀▔▔ ▜████▛▀▂$5▅████▀ + $3▗█████████▀ ▝▘ $4▀▀▂▃▆████▛▀ + $2▂ ▃██████$3███▘ ▁▂$4▄▅▆█████▀▀▂▅█▙ + $2▟█████████▘▄▄▄$3▄▄▄▄▄▄▅▅▅▆▇▇██████▀▀▀▂$4▄▆█████▙ + $1▟$2████████▛▘▝▀▀▀▀▀▀▀▀▀▀$3▀▀▀▀▀▀▔▔▔ ▝███████$4██▙▖ +$1████████$2█▛ $3▜█████████ +$1▜███████▛ $3▜███████▛ + $1▔▀▀▀▀▀▔ $2▔▀$3▀▀▀▀▔ diff --git a/src/logo/ascii/o.inc b/src/logo/ascii/o.inc index 54b9282f0..dcb200360 100644 --- a/src/logo/ascii/o.inc +++ b/src/logo/ascii/o.inc @@ -223,6 +223,18 @@ static const FFlogo O[] = { .colorTitle = FF_COLOR_FG_GREEN, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_OPENSUSE_LEAP_MICRO + // OpenSuseLeapMicro + { + .names = { "opensuse-leap-micro" }, + .lines = FASTFETCH_DATATEXT_LOGO_OPENSUSE_LEAP_MICRO, + .colors = { + FF_COLOR_FG_GREEN, + }, + .colorKeys = FF_COLOR_FG_GREEN, + .colorTitle = FF_COLOR_FG_GREEN, + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_OPENSUSE_LEAP_OLD // OpenSuseLeapOld { diff --git a/src/logo/ascii/o/opensuse_leap_micro.txt b/src/logo/ascii/o/opensuse_leap_micro.txt new file mode 100644 index 000000000..639c74d4a --- /dev/null +++ b/src/logo/ascii/o/opensuse_leap_micro.txt @@ -0,0 +1,19 @@ + +=+ + +=====+ + +==== ====+ + +==== ====+ + +==== ====+ + +==== ======= ====+ + +==== +===+++===+ ====+ +========== === === ========== ++++++++==== ===+ +=== ====+++++++ + +==== +=======+ ====+ + == +==== +++ ====+ == + ==== +==== ====+ ==== + ==== +==== ====+ ==== + ==== +=======+ ==== + ==== +===+ ==== + ==== ==== + ====+==== + ===== + = \ No newline at end of file diff --git a/src/logo/ascii/t.inc b/src/logo/ascii/t.inc index 3adf4784a..d13b34d79 100644 --- a/src/logo/ascii/t.inc +++ b/src/logo/ascii/t.inc @@ -85,6 +85,17 @@ static const FFlogo T[] = { }, }, #endif + #ifdef FASTFETCH_DATATEXT_LOGO_TINYCORE + // TinyCore + { + .names = { "TinyCore" }, + .lines = FASTFETCH_DATATEXT_LOGO_TINYCORE, + .colors = { + FF_COLOR_FG_BLUE, + FF_COLOR_FG_LIGHT_WHITE, + }, + }, + #endif #ifdef FASTFETCH_DATATEXT_LOGO_TORIZONCORE // Torizon OS { diff --git a/src/logo/ascii/t/tinycore.txt b/src/logo/ascii/t/tinycore.txt new file mode 100644 index 000000000..0d39c63ec --- /dev/null +++ b/src/logo/ascii/t/tinycore.txt @@ -0,0 +1,10 @@ +$1 .-~~~~~~~-. +$1 .' $2tiny$1 '. +$1 / / \ +$1 ; / ; +$1 | / | +$1 | / | +$1 ; / $2micro$1 ; +$1 \ / / +$1 '. .' +$1 '-.......-' diff --git a/src/logo/logo.c b/src/logo/logo.c index 9a580e48b..b95ac5fdc 100644 --- a/src/logo/logo.c +++ b/src/logo/logo.c @@ -8,6 +8,7 @@ #include "detection/os/os.h" #include "detection/terminalshell/terminalshell.h" #include "detection/terminalsize/terminalsize.h" +#include "logo/spin/spin.h" #include #include @@ -143,10 +144,13 @@ static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bo if (doColorReplacement && *data == '$') { ++data; - if (*data == '$' || *data == '\0') { + // `$$` is an escaped dollar; a `$` ending a line or the logo is a plain one + if (*data == '$' || *data == '\0' || *data == '\n' || *data == '\r') { ffStrbufAppendC(&line, '$'); ++lineWidth; - ++data; + if (*data == '$') { + ++data; + } continue; } @@ -321,7 +325,7 @@ void ffLogoPrintChars(const char* data, bool doColorReplacement) { logoLineCacheClear(cache); } -static void logoApplyColors(const FFlogo* logo, bool replacement) { +void ffLogoApplyColors(const FFlogo* logo, bool replacement) { if (instance.config.display.colorTitle.length == 0) { ffStrbufSetStatic(&instance.config.display.colorTitle, logo->colorTitle ?: logo->colors[0]); } @@ -436,14 +440,14 @@ static const FFlogo* logoGetBuiltinDetected(FFLogoSize size) { } static void logoPrintStruct(const FFlogo* logo) { - logoApplyColors(logo, true); + ffLogoApplyColors(logo, true); ffLogoPrintChars(logo->lines, true); } static void logoPrintNone(void) { if (!instance.config.display.pipe) { - logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false); + ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false); } instance.state.logoHeight = 0; instance.state.logoWidth = 0; @@ -481,7 +485,7 @@ static bool logoPrintData(bool doColorReplacement, FFstrbuf* source) { return false; } - logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement); + ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement); ffLogoPrintChars(source->chars, doColorReplacement); return true; } @@ -545,7 +549,7 @@ static bool logoPrintFileIfExists(bool doColorReplacement, bool raw) { return false; } - logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement); + ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), doColorReplacement); if (raw) { return ffLogoPrintCharsRaw(content.chars, content.length, instance.config.display.showErrors); } @@ -560,7 +564,7 @@ static bool logoPrintImageIfExists(FFLogoType logo, bool printError) { return false; } - logoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false); + ffLogoApplyColors(logoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), false); return true; } #endif @@ -651,6 +655,11 @@ void ffLogoPrint(void) { return; } + if (options->type == FF_LOGO_TYPE_SPIN) { + ffSpinLogoPrint(); + return; + } + // If the source is not set, we can directly print the detected logo. if (options->source.length == 0) { ffLogoPrintDetected(options->type == FF_LOGO_TYPE_SMALL ? FF_LOGO_SIZE_SMALL : FF_LOGO_SIZE_NORMAL); diff --git a/src/logo/logo.h b/src/logo/logo.h index c6422f978..3ce53ae6c 100644 --- a/src/logo/logo.h +++ b/src/logo/logo.h @@ -38,6 +38,8 @@ void ffLogoBuiltinListAutocompletion(void); void ffLogoPrintDetected(FFLogoSize size); const FFlogo* ffLogoGetBuiltinForName(const FFstrbuf* name, FFLogoSize size); const FFlogo* ffLogoGetBuiltinDetected(FFLogoSize size); +// Fills the `logo.color.N` slots that the user did not set from `logo`. +void ffLogoApplyColors(const FFlogo* logo, bool replacement); // builtin.c extern const FFlogo* ffLogoBuiltins[]; diff --git a/src/logo/spin/render.c b/src/logo/spin/render.c new file mode 100644 index 000000000..255b6d980 --- /dev/null +++ b/src/logo/spin/render.c @@ -0,0 +1,506 @@ +#include "logo/spin/render.h" + +#include +#include +#include + +// Terminal cells are about twice as tall as wide. World units are cell +// heights, so one column is half a unit wide. +#define FF_SPIN_CELL_ASPECT 0.5f +// Camera distance in bounding-sphere radii. Smaller means stronger perspective. +#define FF_SPIN_CAMERA_RADII 4.0f + +// Shading for the slab's edges, dark to bright. The faces keep the logo's glyphs. +static const uint32_t FFSpinWallRamp[] = { 0x2591, 0x2592, 0x2593, 0x2588 }; // ░▒▓█ +static const float FFSpinLight[3] = { -0.48f, 0.6f, 0.64f }; // unit length, camera space + +typedef struct SpinGeometry { + float hx, hy, hz; // half extents of the slab + float camera; // camera distance from the slab center + float focal; // maps the front face 1:1 onto cells at yaw 0 +} SpinGeometry; + +static SpinGeometry spinGeometry(const FFSpinArt* art) { + SpinGeometry g; + g.hx = (float) art->width * FF_SPIN_CELL_ASPECT * 0.5f; + g.hy = (float) art->height * 0.5f; + g.hz = fmaxf(0.3f, fmaxf(g.hx, g.hy) * 0.055f); + float radius = sqrtf(g.hx * g.hx + g.hy * g.hy + g.hz * g.hz); + g.camera = radius * FF_SPIN_CAMERA_RADII; + g.focal = g.camera - g.hz; + return g; +} + +static uint32_t utf8Decode(const char** cursor) { + const unsigned char* s = (const unsigned char*) *cursor; + uint32_t length, glyph; + if (s[0] < 0x80) { + *cursor += 1; + return s[0]; + } + if ((s[0] & 0xE0) == 0xC0) { + length = 2; + glyph = s[0] & 0x1F; + } else if ((s[0] & 0xF0) == 0xE0) { + length = 3; + glyph = s[0] & 0x0F; + } else if ((s[0] & 0xF8) == 0xF0) { + length = 4; + glyph = s[0] & 0x07; + } else { + *cursor += 1; + return '?'; + } + for (uint32_t i = 1; i < length; ++i) { + if ((s[i] & 0xC0) != 0x80) { + *cursor += i; + return '?'; + } + glyph = (glyph << 6) | (s[i] & 0x3F); + } + *cursor += length; + return glyph; +} + +static bool glyphIsBlank(uint32_t glyph) { + return glyph <= ' ' || glyph == 0x7F || glyph == 0xA0 || glyph == 0x2800; +} + +static uint8_t paletteAdd(FFSpinArt* art, const char* params, uint32_t length) { + if (length == 0 || (length == 1 && params[0] == '0')) { + return 0; + } + for (uint32_t i = FF_SPIN_PALETTE_LOGO_SLOTS; i < art->paletteCount; ++i) { + if (art->palette[i].length == length && memcmp(art->palette[i].chars, params, length) == 0) { + return (uint8_t) i; + } + } + if (art->paletteCount >= FF_SPIN_PALETTE_MAX) { + return 0; + } + ffStrbufSetNS(&art->palette[art->paletteCount], length, params); + return (uint8_t) art->paletteCount++; +} + +// Walks fastfetch logo text ($1..$9 colors, `$$`, tabs, SGR escapes). With +// `fill` false it only measures the grid. +static void artWalk(FFSpinArt* art, const char* text, bool fill, uint32_t* outWidth, uint32_t* outHeight) { + uint32_t row = 0, col = 0, width = 0; + uint8_t color = 1; // fastfetch starts every logo in $1 + const char* p = text; + while (*p) { + if (*p == '\r') { + ++p; + continue; + } + if (*p == '\n') { + if (col > width) width = col; + ++row; + col = 0; + ++p; + continue; + } + if (*p == '\t') { + col += 4; + ++p; + continue; + } + if (*p == '\e') { + if (p[1] != '[') { + ++p; + continue; + } + const char* params = p + 2; + const char* end = params; + while ((*end >= '0' && *end <= '9') || *end == ';') { + ++end; + } + if (*end == 'm' && fill) { + color = paletteAdd(art, params, (uint32_t) (end - params)); + } + p = *end ? end + 1 : end; + continue; + } + if (*p == '$') { + if (p[1] >= '1' && p[1] <= '9') { + color = (uint8_t) (p[1] - '0'); + p += 2; + continue; + } + if (p[1] == '$') { + ++p; // `$$` is a literal dollar sign + } + } + uint32_t glyph = utf8Decode(&p); + if (fill && !glyphIsBlank(glyph)) { + size_t index = (size_t) row * art->width + col; + art->glyphs[index] = glyph; + art->colors[index] = color; + } + ++col; + } + if (col > width) width = col; + if (col > 0) ++row; + *outWidth = width; + *outHeight = row; +} + +// Drops blank rows and columns around the art, so it spins around its center. +static bool artCrop(FFSpinArt* art) { + uint32_t minX = UINT32_MAX, minY = UINT32_MAX, maxX = 0, maxY = 0; + for (uint32_t y = 0; y < art->height; ++y) { + for (uint32_t x = 0; x < art->width; ++x) { + if (art->glyphs[(size_t) y * art->width + x] == 0) continue; + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + if (minX == UINT32_MAX) { + return false; + } + uint32_t width = maxX - minX + 1, height = maxY - minY + 1; + if (width == art->width && height == art->height) { + return true; + } + uint32_t* glyphs = (uint32_t*) malloc((size_t) width * height * sizeof(uint32_t)); + uint8_t* colors = (uint8_t*) malloc((size_t) width * height); + if (!glyphs || !colors) { + free(glyphs); + free(colors); + return false; + } + for (uint32_t y = 0; y < height; ++y) { + size_t from = (size_t) (minY + y) * art->width + minX; + memcpy(glyphs + (size_t) y * width, art->glyphs + from, width * sizeof(uint32_t)); + memcpy(colors + (size_t) y * width, art->colors + from, width); + } + free(art->glyphs); + free(art->colors); + art->glyphs = glyphs; + art->colors = colors; + art->width = width; + art->height = height; + return true; +} + +bool ffSpinArtParse(FFSpinArt* art, const char* text, const FFstrbuf* colors, uint32_t colorCount) { + memset(art, 0, sizeof(*art)); + art->paletteCount = FF_SPIN_PALETTE_LOGO_SLOTS; + for (uint32_t i = 0; colors && i < colorCount && i + 1 < FF_SPIN_PALETTE_LOGO_SLOTS; ++i) { + ffStrbufSet(&art->palette[i + 1], &colors[i]); + } + + uint32_t width, height; + artWalk(art, text, false, &width, &height); + if (width == 0 || height == 0) { + return false; + } + art->glyphs = (uint32_t*) calloc((size_t) width * height, sizeof(uint32_t)); + art->colors = (uint8_t*) calloc((size_t) width * height, 1); + if (!art->glyphs || !art->colors) { + return false; + } + art->width = width; + art->height = height; + artWalk(art, text, true, &width, &height); + return artCrop(art); +} + +void ffSpinArtDestroy(FFSpinArt* art) { + free(art->glyphs); + free(art->colors); + for (uint32_t i = 0; i < FF_SPIN_PALETTE_MAX; ++i) { + ffStrbufDestroy(&art->palette[i]); + } + memset(art, 0, sizeof(*art)); +} + +bool ffSpinFrameInit(FFSpinFrame* frame, uint32_t width, uint32_t height) { + memset(frame, 0, sizeof(*frame)); + if (width == 0 || height == 0) { + return false; + } + frame->glyphs = (uint32_t*) calloc((size_t) width * height, sizeof(uint32_t)); + frame->colors = (uint8_t*) calloc((size_t) width * height, 1); + if (!frame->glyphs || !frame->colors) { + ffSpinFrameDestroy(frame); + return false; + } + frame->width = width; + frame->height = height; + return true; +} + +void ffSpinFrameDestroy(FFSpinFrame* frame) { + free(frame->glyphs); + free(frame->colors); + memset(frame, 0, sizeof(*frame)); +} + +static void frameClear(FFSpinFrame* frame) { + memset(frame->glyphs, 0, (size_t) frame->width * frame->height * sizeof(uint32_t)); + memset(frame->colors, 0, (size_t) frame->width * frame->height); +} + +void ffSpinRotationMatrix(float yaw, float tilt, float out[3][3], float outInverse[3][3]) { + float cy = cosf(yaw), sy = sinf(yaw); + float cx = cosf(tilt), sx = sinf(tilt); + // Rx(tilt) * Ry(yaw): spin around the logo's vertical axis, then lean it. + const float m[3][3] = { + { cy, 0, sy }, + { sx * sy, cx, -sx * cy }, + { -cx * sy, sx, cx * cy }, + }; + for (uint32_t i = 0; i < 3; ++i) { + for (uint32_t j = 0; j < 3; ++j) { + out[i][j] = m[i][j]; + outInverse[j][i] = m[i][j]; + } + } +} + +bool ffSpinRotationIsOrthonormal(const float m[3][3], float epsilon) { + for (uint32_t i = 0; i < 3; ++i) { + for (uint32_t j = 0; j < 3; ++j) { + float dot = m[0][i] * m[0][j] + m[1][i] * m[1][j] + m[2][i] * m[2][j]; + if (fabsf(dot - (i == j ? 1.0f : 0.0f)) > epsilon) { + return false; + } + } + } + float det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) + - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + return fabsf(det - 1.0f) <= epsilon; +} + +uint32_t ffSpinMirrorGlyph(uint32_t glyph) { + static const uint32_t pairs[][2] = { + { '/', '\\' }, { '(', ')' }, { '[', ']' }, { '{', '}' }, { '<', '>' }, + { 'b', 'd' }, { 'p', 'q' }, + { 0x258C, 0x2590 }, { 0x2598, 0x259D }, { 0x2596, 0x2597 }, { 0x259B, 0x259C }, + { 0x2599, 0x259F }, { 0x259A, 0x259E }, { 0x25E2, 0x25E3 }, { 0x25E4, 0x25E5 }, + { 0x2571, 0x2572 }, { 0x250C, 0x2510 }, { 0x2514, 0x2518 }, { 0x251C, 0x2524 }, + { 0x256D, 0x256E }, { 0x2570, 0x256F }, { 0x2554, 0x2557 }, { 0x255A, 0x255D }, + }; + for (uint32_t i = 0; i < sizeof(pairs) / sizeof(pairs[0]); ++i) { + if (glyph == pairs[i][0]) return pairs[i][1]; + if (glyph == pairs[i][1]) return pairs[i][0]; + } + if (glyph >= 0x2800 && glyph <= 0x28FF) { + // Braille: swap the left dot column (1,2,3,7) with the right one (4,5,6,8). + uint32_t dots = glyph - 0x2800; + return 0x2800 + (((dots & 0x07) << 3) | ((dots & 0x38) >> 3) | ((dots & 0x40) << 1) | ((dots & 0x80) >> 1)); + } + return glyph; +} + +void ffSpinFrameSize(const FFSpinArt* art, float tilt, uint32_t* width, uint32_t* height) { + SpinGeometry g = spinGeometry(art); + float maxX = (float) art->width * 0.5f, maxY = (float) art->height * 0.5f; + for (uint32_t step = 0; step < 96; ++step) { + float rot[3][3], inv[3][3]; + ffSpinRotationMatrix((float) step * 2.0f * (float) M_PI / 96.0f, tilt, rot, inv); + for (uint32_t corner = 0; corner < 8; ++corner) { + float p[3] = { corner & 1 ? g.hx : -g.hx, corner & 2 ? g.hy : -g.hy, corner & 4 ? g.hz : -g.hz }; + float c[3]; + for (uint32_t i = 0; i < 3; ++i) { + c[i] = rot[i][0] * p[0] + rot[i][1] * p[1] + rot[i][2] * p[2]; + } + float scale = g.focal / (g.camera - c[2]); + maxX = fmaxf(maxX, fabsf(c[0]) * scale / FF_SPIN_CELL_ASPECT); + maxY = fmaxf(maxY, fabsf(c[1]) * scale); + } + } + // A cell is drawn when the ray through its center hits, and the outermost + // cell centers sit half a cell inside the frame edge. + float marginX = ceilf(maxX - (float) art->width * 0.5f - 0.5f); + float marginY = ceilf(maxY - (float) art->height * 0.5f - 0.5f); + *width = art->width + 2 * (marginX > 0 ? (uint32_t) marginX : 0); + *height = art->height + 2 * (marginY > 0 ? (uint32_t) marginY : 0); +} + +void ffSpinFramePutArt(FFSpinFrame* frame, const FFSpinArt* art) { + frameClear(frame); + if (frame->width < art->width || frame->height < art->height) { + return; + } + uint32_t left = (frame->width - art->width) / 2; + uint32_t top = (frame->height - art->height) / 2; + for (uint32_t y = 0; y < art->height; ++y) { + size_t to = (size_t) (top + y) * frame->width + left; + size_t from = (size_t) y * art->width; + memcpy(frame->glyphs + to, art->glyphs + from, art->width * sizeof(uint32_t)); + memcpy(frame->colors + to, art->colors + from, art->width); + } +} + +typedef enum SpinHit { + SPIN_HIT_NONE, + SPIN_HIT_FRONT, + SPIN_HIT_BACK, + SPIN_HIT_WALL, +} SpinHit; + +// Casts one ray (object space) through the extruded art. On a hit, `cell` is +// the art cell and `normal` the object-space normal of the surface. +static SpinHit castRay(const FFSpinArt* art, const SpinGeometry* g, const float o[3], const float d[3], uint32_t* cell, float normal[3]) { + const float lo[3] = { -g->hx, -g->hy, -g->hz }; + const float hi[3] = { g->hx, g->hy, g->hz }; + float tNear = 0.0f, tFar = INFINITY; + int axis = -1; + for (int a = 0; a < 3; ++a) { + if (fabsf(d[a]) < 1e-12f) { + if (o[a] < lo[a] || o[a] > hi[a]) return SPIN_HIT_NONE; + continue; + } + float t1 = (lo[a] - o[a]) / d[a]; + float t2 = (hi[a] - o[a]) / d[a]; + if (t1 > t2) { + float t = t1; + t1 = t2; + t2 = t; + } + if (t1 > tNear) { + tNear = t1; + axis = a; + } + if (t2 < tFar) tFar = t2; + if (tNear > tFar) return SPIN_HIT_NONE; + } + if (axis < 0) return SPIN_HIT_NONE; + + // Grid coordinates: x in columns from the left, y in rows from the top. + const int32_t w = (int32_t) art->width, h = (int32_t) art->height; + float gx = (o[0] + d[0] * tNear + g->hx) / FF_SPIN_CELL_ASPECT; + float gy = g->hy - (o[1] + d[1] * tNear); + float dgx = d[0] / FF_SPIN_CELL_ASPECT; + float dgy = -d[1]; + int32_t cx = (int32_t) floorf(gx), cy = (int32_t) floorf(gy); + if (cx < 0) cx = 0; + if (cx >= w) cx = w - 1; + if (cy < 0) cy = 0; + if (cy >= h) cy = h - 1; + + normal[0] = normal[1] = normal[2] = 0.0f; + if (art->glyphs[(size_t) cy * art->width + (uint32_t) cx]) { + *cell = (uint32_t) cy * art->width + (uint32_t) cx; + normal[axis] = d[axis] > 0 ? -1.0f : 1.0f; + if (axis == 2) return d[2] < 0 ? SPIN_HIT_FRONT : SPIN_HIT_BACK; + return SPIN_HIT_WALL; + } + + // Walk the grid cell by cell (Amanatides-Woo) until the ray leaves the slab. + int32_t stepX = dgx > 0 ? 1 : -1, stepY = dgy > 0 ? 1 : -1; + float tMaxX = dgx != 0 ? tNear + ((float) (dgx > 0 ? cx + 1 : cx) - gx) / dgx : INFINITY; + float tMaxY = dgy != 0 ? tNear + ((float) (dgy > 0 ? cy + 1 : cy) - gy) / dgy : INFINITY; + float tDeltaX = dgx != 0 ? fabsf(1.0f / dgx) : INFINITY; + float tDeltaY = dgy != 0 ? fabsf(1.0f / dgy) : INFINITY; + while (true) { + bool alongX = tMaxX < tMaxY; + if ((alongX ? tMaxX : tMaxY) > tFar) return SPIN_HIT_NONE; + if (alongX) { + cx += stepX; + tMaxX += tDeltaX; + if (cx < 0 || cx >= w) return SPIN_HIT_NONE; + } else { + cy += stepY; + tMaxY += tDeltaY; + if (cy < 0 || cy >= h) return SPIN_HIT_NONE; + } + if (art->glyphs[(size_t) cy * art->width + (uint32_t) cx]) { + *cell = (uint32_t) cy * art->width + (uint32_t) cx; + if (alongX) { + normal[0] = (float) -stepX; + } else { + normal[1] = (float) stepY; // grid rows grow downward + } + return SPIN_HIT_WALL; + } + } +} + +void ffSpinRenderArt(const FFSpinArt* art, float yaw, float tilt, FFSpinFrame* frame) { + frameClear(frame); + SpinGeometry g = spinGeometry(art); + float rot[3][3], inv[3][3]; + ffSpinRotationMatrix(yaw, tilt, rot, inv); + const float origin[3] = { inv[0][2] * g.camera, inv[1][2] * g.camera, inv[2][2] * g.camera }; + const uint32_t rampLast = (uint32_t) (sizeof(FFSpinWallRamp) / sizeof(FFSpinWallRamp[0])) - 1; + + for (uint32_t y = 0; y < frame->height; ++y) { + float v = (float) frame->height * 0.5f - ((float) y + 0.5f); + for (uint32_t x = 0; x < frame->width; ++x) { + float u = ((float) x + 0.5f - (float) frame->width * 0.5f) * FF_SPIN_CELL_ASPECT; + float d[3]; + for (uint32_t i = 0; i < 3; ++i) { + d[i] = inv[i][0] * u + inv[i][1] * v - inv[i][2] * g.focal; + } + uint32_t cell = 0; + float n[3]; + SpinHit hit = castRay(art, &g, origin, d, &cell, n); + if (hit == SPIN_HIT_NONE) continue; + + size_t index = (size_t) y * frame->width + x; + frame->colors[index] = art->colors[cell]; + if (hit == SPIN_HIT_FRONT) { + frame->glyphs[index] = art->glyphs[cell]; + } else if (hit == SPIN_HIT_BACK) { + frame->glyphs[index] = ffSpinMirrorGlyph(art->glyphs[cell]); + } else { + float diffuse = 0.0f; + for (uint32_t i = 0; i < 3; ++i) { + diffuse += (rot[i][0] * n[0] + rot[i][1] * n[1] + rot[i][2] * n[2]) * FFSpinLight[i]; + } + float luminance = 0.15f + 0.85f * fmaxf(diffuse, 0.0f); + frame->glyphs[index] = FFSpinWallRamp[(uint32_t) (luminance * (float) rampLast + 0.5f)]; + } + } + } +} + +static void appendSgr(FFstrbuf* out, const FFSpinArt* art, uint8_t color, bool bold) { + ffStrbufAppendS(out, bold ? "\e[0;1" : "\e[0"); + if (color < art->paletteCount && art->palette[color].length > 0) { + ffStrbufAppendC(out, ';'); + ffStrbufAppend(out, &art->palette[color]); + } + ffStrbufAppendC(out, 'm'); +} + +void ffSpinAppendCells(FFstrbuf* out, const FFSpinFrame* frame, const FFSpinArt* art, uint32_t index, uint32_t count, bool color, bool bold) { + int32_t active = -1; + for (uint32_t i = index; i < index + count; ++i) { + uint32_t glyph = frame->glyphs[i]; + if (glyph == 0) { + ffStrbufAppendC(out, ' '); + continue; + } + if (color && (int32_t) frame->colors[i] != active) { + active = frame->colors[i]; + appendSgr(out, art, frame->colors[i], bold); + } + ffStrbufAppendUtf32CodePoint(out, glyph); + } + if (active >= 0) { + ffStrbufAppendS(out, "\e[0m"); + } +} + +void ffSpinFrameToAnsi(const FFSpinFrame* frame, const FFSpinArt* art, bool color, bool bold, bool trimRight, FFstrbuf* out) { + for (uint32_t y = 0; y < frame->height; ++y) { + uint32_t length = frame->width; + if (trimRight) { + // Empty cells at the end of a line are not part of the art. Printing + // them would pad every line to the frame width and make the output + // differ from the other text logo types. + while (length > 0 && frame->glyphs[(size_t) y * frame->width + length - 1] == 0) { + --length; + } + } + ffSpinAppendCells(out, frame, art, y * frame->width, length, color, bold); + ffStrbufAppendC(out, '\n'); + } +} diff --git a/src/logo/spin/render.h b/src/logo/spin/render.h new file mode 100644 index 000000000..3beddd578 --- /dev/null +++ b/src/logo/spin/render.h @@ -0,0 +1,54 @@ +#pragma once + +#include "common/FFstrbuf.h" + +#include +#include + +#define FF_SPIN_PALETTE_MAX 32 +// Palette slots 1..9 hold the logo's $1..$9 colors; raw SGR sequences found in +// custom logo files get the slots after that. Slot 0 is the terminal default. +#define FF_SPIN_PALETTE_LOGO_SLOTS 10 + +// A logo's ASCII art as a grid of cells. The renderer extrudes it into a slab. +typedef struct FFSpinArt { + uint32_t width; + uint32_t height; + uint32_t* glyphs; // Unicode code point per cell, 0 = empty + uint8_t* colors; // palette index per cell + uint32_t paletteCount; + FFstrbuf palette[FF_SPIN_PALETTE_MAX]; // SGR parameters, e.g. "1;34" +} FFSpinArt; + +typedef struct FFSpinFrame { + uint32_t width; + uint32_t height; + uint32_t* glyphs; + uint8_t* colors; +} FFSpinFrame; + +// `colors` holds the logo's $1..$9 colors (may be nullptr). Returns false for +// empty art or when memory runs out. +bool ffSpinArtParse(FFSpinArt* art, const char* text, const FFstrbuf* colors, uint32_t colorCount); +void ffSpinArtDestroy(FFSpinArt* art); + +bool ffSpinFrameInit(FFSpinFrame* frame, uint32_t width, uint32_t height); +void ffSpinFrameDestroy(FFSpinFrame* frame); + +// Frame size that keeps the art inside the frame at every yaw for this tilt. +// The art is centered; the extra space is the same on both sides. +void ffSpinFrameSize(const FFSpinArt* art, float tilt, uint32_t* width, uint32_t* height); +// Copies the art face-on into the center of the frame, exactly as authored. +void ffSpinFramePutArt(FFSpinFrame* frame, const FFSpinArt* art); +void ffSpinRenderArt(const FFSpinArt* art, float yaw, float tilt, FFSpinFrame* frame); + +void ffSpinRotationMatrix(float yaw, float tilt, float out[3][3], float outInverse[3][3]); +bool ffSpinRotationIsOrthonormal(const float matrix[3][3], float epsilon); +uint32_t ffSpinMirrorGlyph(uint32_t glyph); + +// Appends `count` cells starting at `index` with colors, ending in a reset. +void ffSpinAppendCells(FFstrbuf* out, const FFSpinFrame* frame, const FFSpinArt* art, uint32_t index, uint32_t count, bool color, bool bold); +// One line per frame row, each ending in '\n'. `trimRight` drops the empty cells at the end of a +// row, which is what the other text logo types print; a frame that is redrawn as it turns must +// keep its full width instead, or the information column would move with the angle. +void ffSpinFrameToAnsi(const FFSpinFrame* frame, const FFSpinArt* art, bool color, bool bold, bool trimRight, FFstrbuf* out); diff --git a/src/logo/spin/spin.c b/src/logo/spin/spin.c new file mode 100644 index 000000000..a77b467b1 --- /dev/null +++ b/src/logo/spin/spin.c @@ -0,0 +1,167 @@ +#include "logo/spin/spin.h" + +#include "common/io.h" +#include "common/strutil.h" +#include "common/time.h" +#include "logo/logo.h" +#include "logo/spin/render.h" + +#include +#include +#include + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif + +// Degrees per frame when neither speed option is set. +#define SPIN_DEFAULT_DEGREES_PER_FRAME 10.0f + +// One step of the turn. `logo.spin.speedPerRound` wins over +// `logo.spin.speedPerFrame` when both are set; 0 means unset for either. +static float spinStepDegrees(void) { + const FFOptionsLogoSpin* spin = &instance.config.logo.spin; + if (spin->speedPerRound != 0.0f) { + // Seconds per revolution: follow the clock instead of counting frames, + // so the turn keeps its pace even when a refresh takes longer than the + // interval asked for. + FFLogoSpinState* state = &instance.state.logoSpin; + const double now = ffTimeGetTick(); + const double elapsedMs = now - state->lastDrawMs; + state->lastDrawMs = now; + return (float) (360.0 / (double) spin->speedPerRound * elapsedMs / 1000.0); + } + if (spin->speedPerFrame != 0.0f) { + return spin->speedPerFrame; + } + return SPIN_DEFAULT_DEGREES_PER_FRAME; +} + +static bool spinIsAuto(const FFstrbuf* source) { + return source->length == 0 || ffStrbufIgnCaseEqualS(source, "auto"); +} + +static const FFlogo* spinBuiltin(const FFstrbuf* source, FFLogoSize size) { + return spinIsAuto(source) ? ffLogoGetBuiltinDetected(size) : ffLogoGetBuiltinForName(source, size); +} + +static bool spinLoadFile(const FFstrbuf* source, FFstrbuf* text) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + if (ffPathExpandEnv(source->chars, &path) && ffAppendFileBuffer(path.chars, text)) { + return text->length > 0; + } + return ffAppendFileBuffer(source->chars, text) && text->length > 0; +} + +void ffSpinLogoPrint(void) { + FFOptionsLogo* options = &instance.config.logo; + FFLogoSpinState* state = &instance.state.logoSpin; + instance.state.logoWidth = 0; + instance.state.logoHeight = 0; + + if (!state->started) { + // The first frame starts where `logo.spin.angle` says, and every + // `--watch` refresh turns it by one step. + state->started = true; + state->degrees = fmodf(options->spin.angle, 360.0f); + if (state->degrees < 0.0f) { + state->degrees += 360.0f; + } + } + const float spinAngle = state->degrees * (float) M_PI / 180.0f; + + // The source is the detected OS (`auto`), a builtin logo name, or a file + // in fastfetch's logo syntax, like the other text logo types. + FF_STRBUF_AUTO_DESTROY fileText = ffStrbufCreate(); + const FFlogo* logo = spinBuiltin(&options->source, FF_LOGO_SIZE_NORMAL); + if (!logo) { + logo = spinBuiltin(&options->source, FF_LOGO_SIZE_UNKNOWN); + } + if (!logo && !spinLoadFile(&options->source, &fileText)) { + if (instance.config.display.showErrors) { + fprintf(stderr, "Logo (spin): no builtin logo or readable file named %s\n", options->source.chars); + } + } + + // `logo.color.N` on top of the logo's own colors; a file logo borrows the + // detected logo's palette, as the other text logo types do. + ffLogoApplyColors(logo ? logo : ffLogoGetBuiltinDetected(FF_LOGO_SIZE_NORMAL), true); + + FFSpinArt art = {}; + if (!ffSpinArtParse(&art, logo ? logo->lines : fileText.chars, options->colors, FASTFETCH_LOGO_MAX_COLORS)) { + ffSpinArtDestroy(&art); // parse may have allocated before it gave up + return; + } + + const float tilt = options->spin.tilt * (float) M_PI / 180.0f; + uint32_t width = art.width; + uint32_t height = art.height; + // With `--watch` the frame has to stay the same size from the first draw on, + // or the information column would move when the logo starts turning. + const bool animated = instance.state.dynamicInterval > 0; + if (animated || spinAngle != 0.0f || tilt != 0.0f) { + // A turned slab sticks out of the art's box, so the frame has to make + // room for its widest angle or the logo would be clipped. + ffSpinFrameSize(&art, tilt, &width, &height); + } + + FFSpinFrame frame = {}; + if (!ffSpinFrameInit(&frame, width, height)) { + ffSpinArtDestroy(&art); + return; + } + + if (spinAngle == 0.0f && tilt == 0.0f) { + ffSpinFramePutArt(&frame, &art); // face on: exactly the original art + } else { + ffSpinRenderArt(&art, spinAngle, tilt, &frame); + } + + // Hand the frame to the upstream logo pipeline: it pads, positions and + // measures the art, and its line cache is what puts the modules next to + // the logo. `false`: the frame already carries its own SGR sequences. + FF_STRBUF_AUTO_DESTROY ansi = ffStrbufCreateA(width * height * 4); + ffSpinFrameToAnsi(&frame, &art, !instance.config.display.pipe, instance.config.display.brightColor, !animated, &ansi); + // The serialiser ends every row with a newline; the last one would add an + // empty line to the logo and change the output from the other logo types. + if (ansi.length > 0 && ansi.chars[ansi.length - 1] == '\n') { + ansi.chars[--ansi.length] = '\0'; + } + + // A resize changes the frame size, and the cells outside the new frame + // would keep showing the previous one. + if (state->printed && (width != state->printedWidth || height != state->printedHeight)) { + fputs("\e[2J\e[H", stdout); + } + // `ffLogoPrintChars` writes the art with write(2) for `--logo-position top`, + // while the caller's cursor moves go through stdio; flush so the art lands + // where the cursor was just moved to. + fflush(stdout); + ffLogoPrintChars(ansi.chars, false); + + ffSpinFrameDestroy(&frame); + ffSpinArtDestroy(&art); + + state->printed = true; + state->printedWidth = width; + state->printedHeight = height; + state->lastDrawMs = ffTimeGetTick(); // start measuring the next step from now +} + +bool ffSpinTick(void) { + FFLogoSpinState* state = &instance.state.logoSpin; + if (!state->printed) { + return false; + } + + const float step = spinStepDegrees(); + if (step == 0.0f) { + return false; // a logo that does not turn does not have to be redrawn + } + + // The angle is wrapped instead of growing, or a long `--watch` session + // would lose precision in the sine and cosine. + float degrees = state->degrees + step; + state->degrees = degrees - floorf(degrees / 360.0f) * 360.0f; + return true; +} diff --git a/src/logo/spin/spin.h b/src/logo/spin/spin.h new file mode 100644 index 000000000..1b758d3f1 --- /dev/null +++ b/src/logo/spin/spin.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +// The spin logo: the builtin logo (or a text file in logo syntax) drawn as an +// extruded slab. It has no timer and reads no input of its own; the `--watch` +// loop calls `ffSpinTick` once per refresh, which is what makes it turn. + +// Draws the current frame. Called from `ffLogoPrint`. +void ffSpinLogoPrint(void); + +// Advances the rotation by one `--watch` refresh and returns true when the logo +// has to be drawn again, so a still logo or any other logo type costs a single +// comparison per refresh. The step is `logo.spin.speedPerFrame` degrees, or +// whatever `logo.spin.speedPerRound` makes of the time since the last frame. +bool ffSpinTick(void); diff --git a/src/modules/separator/separator.c b/src/modules/separator/separator.c index c8ac4b337..ed29da8f2 100644 --- a/src/modules/separator/separator.c +++ b/src/modules/separator/separator.c @@ -80,6 +80,14 @@ void ffParseSeparatorJsonObject(FFSeparatorOptions* options, yyjson_val* module) } if (unsafe_yyjson_equals_str(key, "string")) { + if (!yyjson_is_str(val)) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Separator), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property 'string' must be a string"); + continue; + } + if (unsafe_yyjson_get_len(val) == 0) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Separator), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property 'string' must not be empty"); + continue; + } ffStrbufSetJsonVal(&options->string, val); continue; } diff --git a/src/modules/uptime/uptime.c b/src/modules/uptime/uptime.c index 3a68c4696..2afa44995 100644 --- a/src/modules/uptime/uptime.c +++ b/src/modules/uptime/uptime.c @@ -60,7 +60,7 @@ void ffGenerateUptimeJsonConfig(FFUptimeOptions* options, yyjson_mut_doc* doc, y } bool ffGenerateUptimeJsonResult([[maybe_unused]] FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { - FFUptimeResult result; + FFUptimeResult result = {}; const char* error = ffDetectUptime(&result); if (error) { diff --git a/src/options/logo.c b/src/options/logo.c index bc667c7e0..ca6e881d7 100644 --- a/src/options/logo.c +++ b/src/options/logo.c @@ -23,6 +23,22 @@ static bool logoTypeIsImage(FFLogoType type) { } #endif +// The numeric `--logo-spin-*` options: a plain number with a range, so a typo is +// rejected instead of producing a nonsensical frame. +static float logoParseFloat(const char* key, const char* value, float min, float max) { + if (value == nullptr) { + fprintf(stderr, "Error: usage: %s \n", key); + exit(477); + } + char* end = nullptr; + double parsed = strtod(value, &end); + if (end == value || *end != '\0' || parsed < min || parsed > max) { + fprintf(stderr, "Error: %s must be a number between %g and %g\n", key, min, max); + exit(477); + } + return (float) parsed; +} + // --sixel, --kitty, --kitty-direct, --kitty-icat and --iterm. They keep a branch of their own in // every build so that the error a build without image logos gives names the flag that was used, // rather than it being rejected as an unknown option. --chafa is not among them: it is gated on @@ -70,6 +86,10 @@ void ffOptionsInitLogo(FFOptionsLogo* options) { options->cache = FF_LOGO_CACHE_ON; options->position = FF_LOGO_POSITION_LEFT; options->animationFrame = FF_LOGO_ANIMATION_FRAME_FIRST; + options->spin.speedPerFrame = 0.0f; // 0: not set, see `ffSpinTick` + options->spin.speedPerRound = 0.0f; // 0: not set, see `ffSpinTick` + options->spin.tilt = 0.0f; + options->spin.angle = 0.0f; #if FF_HAVE_CHAFA options->chafaFgOnly = false; @@ -118,6 +138,7 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons { "iterm", FF_LOGO_TYPE_IMAGE_ITERM }, { "chafa", FF_LOGO_TYPE_IMAGE_CHAFA }, { "raw", FF_LOGO_TYPE_IMAGE_RAW }, + { "spin", FF_LOGO_TYPE_SPIN }, { "none", FF_LOGO_TYPE_NONE }, {}, }); @@ -184,6 +205,14 @@ bool ffOptionsParseLogoCommandLine(FFOptionsLogo* options, const char* key, cons }); } else if (ffStrEqualsIgnCase(subKey, "animation-frame")) { options->animationFrame = ffOptionParseInt32(key, value); + } else if (ffStrEqualsIgnCase(subKey, "spin-speed-per-frame")) { + options->spin.speedPerFrame = logoParseFloat(key, value, -360.0f, 360.0f); + } else if (ffStrEqualsIgnCase(subKey, "spin-speed-per-round")) { + options->spin.speedPerRound = logoParseFloat(key, value, -3600.0f, 3600.0f); + } else if (ffStrEqualsIgnCase(subKey, "spin-tilt")) { + options->spin.tilt = logoParseFloat(key, value, -80.0f, 80.0f); + } else if (ffStrEqualsIgnCase(subKey, "spin-angle")) { + options->spin.angle = logoParseFloat(key, value, 0.0f, 360.0f); } else { return false; } @@ -323,6 +352,7 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo { "iterm", FF_LOGO_TYPE_IMAGE_ITERM }, { "chafa", FF_LOGO_TYPE_IMAGE_CHAFA }, { "raw", FF_LOGO_TYPE_IMAGE_RAW }, + { "spin", FF_LOGO_TYPE_SPIN }, { "none", FF_LOGO_TYPE_NONE }, {}, }); @@ -436,6 +466,50 @@ const char* ffOptionsParseLogoJsonConfig(FFOptionsLogo* options, yyjson_val* roo } options->animationFrame = (int32_t) yyjson_get_sint(val); continue; + } else if (unsafe_yyjson_equals_str(key, "spin")) { + if (!yyjson_is_obj(val)) { + return "Property 'logo.spin' must be an object"; + } + + yyjson_val *spinKey, *spinVal; + size_t spinIdx, spinMax; + yyjson_obj_foreach (val, spinIdx, spinMax, spinKey, spinVal) { + if (unsafe_yyjson_equals_str(spinKey, "speed")) { + if (!yyjson_is_obj(spinVal)) { + return "Property 'logo.spin.speed' must be an object"; + } + yyjson_val *speedKey, *speedVal; + size_t speedIdx, speedMax; + yyjson_obj_foreach (spinVal, speedIdx, speedMax, speedKey, speedVal) { + if (unsafe_yyjson_equals_str(speedKey, "perFrame")) { + if (!yyjson_is_num(speedVal) || yyjson_get_num(speedVal) < -360 || yyjson_get_num(speedVal) > 360) { + return "Property 'logo.spin.speed.perFrame' must be a number between -360 and 360"; + } + options->spin.speedPerFrame = (float) yyjson_get_num(speedVal); + } else if (unsafe_yyjson_equals_str(speedKey, "perRound")) { + if (!yyjson_is_num(speedVal) || yyjson_get_num(speedVal) < -3600 || yyjson_get_num(speedVal) > 3600) { + return "Property 'logo.spin.speed.perRound' must be a number between -3600 and 3600"; + } + options->spin.speedPerRound = (float) yyjson_get_num(speedVal); + } else { + return "Unknown logo.spin.speed key"; + } + } + } else if (unsafe_yyjson_equals_str(spinKey, "tilt")) { + if (!yyjson_is_num(spinVal) || yyjson_get_num(spinVal) < -80 || yyjson_get_num(spinVal) > 80) { + return "Property 'logo.spin.tilt' must be a number between -80 and 80"; + } + options->spin.tilt = (float) yyjson_get_num(spinVal); + } else if (unsafe_yyjson_equals_str(spinKey, "angle")) { + if (!yyjson_is_num(spinVal) || yyjson_get_num(spinVal) < 0 || yyjson_get_num(spinVal) > 360) { + return "Property 'logo.spin.angle' must be a number between 0 and 360"; + } + options->spin.angle = (float) yyjson_get_num(spinVal); + } else { + return "Unknown logo.spin key"; + } + } + continue; } else if (unsafe_yyjson_equals_str(key, "chafa")) { #if FF_HAVE_CHAFA if (!yyjson_is_obj(val)) { @@ -568,6 +642,9 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { case FF_LOGO_TYPE_IMAGE_RAW: yyjson_mut_obj_add_str(doc, obj, "type", "raw"); break; + case FF_LOGO_TYPE_SPIN: + yyjson_mut_obj_add_str(doc, obj, "type", "spin"); + break; default: yyjson_mut_obj_add_str(doc, obj, "type", "auto"); break; @@ -575,6 +652,17 @@ void ffOptionsGenerateLogoJsonConfig(FFdata* data, FFOptionsLogo* options) { yyjson_mut_obj_add_str(doc, obj, "source", options->source.chars); + if (options->type == FF_LOGO_TYPE_SPIN) { + yyjson_mut_val* spin = yyjson_mut_obj(doc); + yyjson_mut_val* speed = yyjson_mut_obj(doc); + yyjson_mut_obj_add_real(doc, speed, "perFrame", options->spin.speedPerFrame); + yyjson_mut_obj_add_real(doc, speed, "perRound", options->spin.speedPerRound); + yyjson_mut_obj_add_val(doc, spin, "speed", speed); + yyjson_mut_obj_add_real(doc, spin, "tilt", options->spin.tilt); + yyjson_mut_obj_add_real(doc, spin, "angle", options->spin.angle); + yyjson_mut_obj_add_val(doc, obj, "spin", spin); + } + { yyjson_mut_val* color = yyjson_mut_obj(doc); for (int i = 0; i < FASTFETCH_LOGO_MAX_COLORS; i++) { diff --git a/src/options/logo.h b/src/options/logo.h index 984efd894..aec544803 100644 --- a/src/options/logo.h +++ b/src/options/logo.h @@ -21,9 +21,17 @@ typedef enum FFLogoType: uint8_t { FF_LOGO_TYPE_IMAGE_ITERM, // image file, printed as iterm graphics protocol FF_LOGO_TYPE_IMAGE_CHAFA, // image file, printed as ascii art using libchafa FF_LOGO_TYPE_IMAGE_RAW, // image file, printed as raw binary string + FF_LOGO_TYPE_SPIN, // builtin logo (or a text file) drawn as a 3D slab, one turn step per `--watch` refresh FF_LOGO_TYPE_NONE, // `--logo none`, but still applies colors to the system information output (unless `--pipe` is set) } FFLogoType; +typedef struct FFOptionsLogoSpin { + float speedPerFrame; // degrees the logo turns per drawn frame; negative turns the other way, 0: not set + float speedPerRound; // seconds one revolution takes; negative turns the other way, 0: not set (wins over speedPerFrame) + float tilt; // camera tilt in degrees + float angle; // rotation of the first frame in degrees; drawn as is without `--watch` +} FFOptionsLogoSpin; + typedef enum FFLogoPosition: uint8_t { FF_LOGO_POSITION_LEFT, FF_LOGO_POSITION_TOP, @@ -47,6 +55,7 @@ typedef struct FFOptionsLogo { FFstrbuf source; FFLogoType type; FFLogoPosition position; + FFOptionsLogoSpin spin; FFstrbuf colors[FASTFETCH_LOGO_MAX_COLORS]; uint32_t width; uint32_t height; diff --git a/tests/format.c b/tests/format.c index 374fd48df..4e3e27bcb 100644 --- a/tests/format.c +++ b/tests/format.c @@ -17,6 +17,19 @@ static void verify(const char* format, const char* arg, const char* expected, in #define VERIFY(format, argument, expected) verify((format), (argument), (expected), __LINE__) +static void verify2(const char* format, const char* arg1, const char* arg2, const char* expected, int lineNo) { + FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY formatter = ffStrbufCreateStatic(format); + const FFformatarg arguments[] = { { .type = FF_ARG_TYPE_STRING, arg1 }, { .type = FF_ARG_TYPE_STRING, arg2 } }; + ffParseFormatString(&result, &formatter, 2, arguments); + if (!ffStrbufEqualS(&result, expected)) { + fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, format, expected, result.chars); + exit(1); + } +} + +#define VERIFY2(format, argument1, argument2, expected) verify2((format), (argument1), (argument2), (expected), __LINE__) + int main(void) { instance.config.display.pipe = true; @@ -96,9 +109,14 @@ int main(void) { VERIFY("output({1~10,10})", "12345 67890", "output()"); VERIFY("output({1~10,5})", "12345 67890", "output()"); VERIFY("output({1~3,-3})", "12345 67890", "output(45 67)"); + VERIFY("output({1~0,-12})", "12345 67890", "output()"); VERIFY("output({1~-5,-3})", "12345 67890", "output(67)"); VERIFY("output({1~-0,10})", "12345 67890", "output(12345 6789)"); VERIFY("output({1~-3,-5})", "12345 67890", "output()"); + VERIFY("output({1~11,12})", "12345 67890", "output()"); // Same as {1~11} + VERIFY("output({1~20,30})", "12345 67890", "output()"); // Same as {1~20} + VERIFY("output({1~-50,3})", "12345 67890", "output()"); // Same as {1~-50} + VERIFY("output({1~-50,-1})", "12345 67890", "output()"); // Same as {1~-50} VERIFY("output({1~})", "12345 67890", "output(12345 67890)"); // Same as {1~0} VERIFY("output({1~-0})", "12345 67890", "output(12345 67890)"); // Same as {1~0} VERIFY("output({1~,-1})", "12345 67890", "output(12345 6789)"); // Same as {1~0,-1} @@ -122,6 +140,28 @@ int main(void) { VERIFY("output({?1}OK{?}{/1}NOT OK{/})", "", "output(NOT OK)"); } + { + // a skipped conditional may hold conditionals of its own + VERIFY2("{?1}A{?2}B{?}C{?}D", "x", "x", "ABCD"); + VERIFY2("{?1}A{?2}B{?}C{?}D", "x", "", "ACD"); + VERIFY2("{?1}A{?2}B{?}C{?}D", "", "x", "D"); + VERIFY2("{?1}A{?2}B{?}C{?}D", "", "", "D"); + VERIFY2("{/1}A{/2}B{/}C{/}D", "", "", "ABCD"); + VERIFY2("{/1}A{/2}B{/}C{/}D", "", "x", "ACD"); + VERIFY2("{/1}A{/2}B{/}C{/}D", "x", "", "D"); + VERIFY2("{/1}A{/2}B{/}C{/}D", "x", "x", "D"); + VERIFY2("{?1}A{?1}B{?1}C{?}D{?}E{?}F", "", "", "F"); + VERIFY2("{?1}{/2}A{/}B{?}C", "", "", "C"); + + // a conditional with an invalid argument does not open a block + VERIFY2("{?1}A{?3}B{?}C", "", "x", "C"); + VERIFY2("{?1}A{?3}B{?}C{?}D", "", "x", "C{?}D"); + + // an escaped brace does not start a placeholder + VERIFY2("{?1}{{?}}A{?}B", "", "", "B"); + VERIFY2("{?1}{{?2}A{?}B", "", "x", "B"); + } + { VERIFY("output({1:3})", "\e[1m12345", "output(\e[1m123)"); VERIFY("output({1:-3})", "\e[1m12345", "output(\e[1m123…)"); @@ -164,6 +204,22 @@ int main(void) { VERIFY("output({1~4})", "\e[?25l12345", "output(\e[?25l5)"); } + { + // numbers that don't fit in 32 bits must not wrap around to a valid value + VERIFY("output({4294967296})", "x", "output({4294967296})"); + VERIFY("output({4294967297})", "x", "output({4294967297})"); + VERIFY("output({99999999999999999999})", "x", "output({99999999999999999999})"); + VERIFY("output({1~4294967297})", "12345 67890", "output()"); + VERIFY("output({1~-4294967297})", "12345 67890", "output()"); + VERIFY("output({1~0,4294967296})", "12345 67890", "output(12345 67890)"); + VERIFY("output({1:4294967297})", "12345 67890", "output(12345 67890)"); + VERIFY("output({1:-4294967296})", "12345 67890", "output(12345 67890)"); + + // the length is negated for the ellipsis, which overflows for the smallest int32 + VERIFY("output({1:-2147483648})", "12345 67890", "output(12345 67890)"); + VERIFY("output({1:-2147483647})", "12345 67890", "output(12345 67890)"); + } + #ifndef _WIN32 // Windows doesn't have setenv { ffListInit(&instance.config.display.constants); @@ -180,6 +236,10 @@ int main(void) { VERIFY("output({$FF_INVAL})", "", "output({$FF_INVAL})"); VERIFY("output({$9}{$0}${-9})", "", "output({$9}{$0}${-9})"); VERIFY("output({$1NO})", "", "output({$1NO})"); + + // numbers that don't fit in 32 bits must not wrap around to a valid index + VERIFY("output({$4294967297})", "", "output({$4294967297})"); + VERIFY("output({$-4294967297})", "", "output({$-4294967297})"); ffListDestroy(&instance.config.display.constants); } #endif diff --git a/tests/logo.c b/tests/logo.c index d100cd7df..e9a7901eb 100644 --- a/tests/logo.c +++ b/tests/logo.c @@ -1,5 +1,6 @@ #include "logo/logo.h" #include "common/textModifier.h" +#include "fastfetch.h" #include #include @@ -55,6 +56,15 @@ int main(void) { VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_TOP), "top") == 0); VERIFY(strcmp(ffLogoPositionToString(FF_LOGO_POSITION_RIGHT), "right") == 0); + // A `$` ending a line or the logo is a plain dollar: it keeps the line break and the terminator. + instance.config.display.pipe = true; + ffLogoPrintChars("ab$\ncd", true); + VERIFY(instance.state.logoHeight == 1 && instance.state.logoWidth == 3); + ffLogoPrintChars("ab$", true); + VERIFY(instance.state.logoHeight == 0 && instance.state.logoWidth == 3); + ffLogoPrintChars("a$1b$$c", true); + VERIFY(instance.state.logoWidth == 4); + puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET); return 0; } diff --git a/tests/spin.c b/tests/spin.c new file mode 100644 index 000000000..7e9cab56b --- /dev/null +++ b/tests/spin.c @@ -0,0 +1,224 @@ +#include "fastfetch.h" +#include "logo/logo.h" +#include "logo/spin/render.h" + +#include +#include +#include +#include + +#ifndef M_PI + #define M_PI 3.14159265358979323846 +#endif + +// Unlike assert(), this also checks in release builds. +#define VERIFY(expression) \ + do { \ + if (!(expression)) { \ + fprintf(stderr, "spin test failed [%d]: %s\n", __LINE__, #expression); \ + exit(1); \ + } \ + } while (0) + +static bool isWallGlyph(uint32_t glyph) { + return glyph >= 0x2591 && glyph <= 0x2593 ? true : glyph == 0x2588; // ░▒▓█ +} + +static void testRotation(void) { + const float angles[] = { 0.0f, 0.25f, (float) M_PI / 2.0f, (float) M_PI, 5.0f }; + const float tilts[] = { 0.0f, 0.2f, -0.4f, 1.0f }; + for (uint32_t i = 0; i < ARRAY_SIZE(angles); ++i) { + for (uint32_t j = 0; j < ARRAY_SIZE(tilts); ++j) { + float rotation[3][3], inverse[3][3]; + ffSpinRotationMatrix(angles[i], tilts[j], rotation, inverse); + VERIFY(ffSpinRotationIsOrthonormal(rotation, 1e-4f)); + for (uint32_t r = 0; r < 3; ++r) { + for (uint32_t c = 0; c < 3; ++c) { + VERIFY(fabsf(rotation[r][c] - inverse[c][r]) < 1e-5f); + } + } + } + } +} + +static void testParse(void) { + FFstrbuf colors[2]; + ffStrbufInitStatic(&colors[0], "31"); + ffStrbufInitStatic(&colors[1], "1;34"); + + FFSpinArt art; + // Leading blank rows and columns are cropped; `$$` is a literal dollar. + VERIFY(ffSpinArtParse(&art, "\n $1ab\n $2c$$\xe2\x96\x88\n", colors, 2)); + VERIFY(art.width == 3 && art.height == 2); + VERIFY(art.glyphs[0] == 'a' && art.colors[0] == 1); + VERIFY(art.glyphs[1] == 'b' && art.colors[1] == 1); + VERIFY(art.glyphs[2] == 0); + VERIFY(art.glyphs[3] == 'c' && art.colors[3] == 2); + VERIFY(art.glyphs[4] == '$'); + VERIFY(art.glyphs[5] == 0x2588); + VERIFY(ffStrbufEqualS(&art.palette[2], "1;34")); + ffSpinArtDestroy(&art); + + // Raw SGR escapes in custom logo files get their own palette slots. + VERIFY(ffSpinArtParse(&art, "\e[38;5;208mx\e[0my", nullptr, 0)); + VERIFY(art.width == 2 && art.height == 1); + VERIFY(art.colors[0] >= FF_SPIN_PALETTE_LOGO_SLOTS); + VERIFY(ffStrbufEqualS(&art.palette[art.colors[0]], "38;5;208")); + VERIFY(art.colors[1] == 0); + ffSpinArtDestroy(&art); + + VERIFY(!ffSpinArtParse(&art, " \n \t \n", nullptr, 0)); + ffSpinArtDestroy(&art); + VERIFY(!ffSpinArtParse(&art, "", nullptr, 0)); + ffSpinArtDestroy(&art); +} + +static void testMirror(void) { + VERIFY(ffSpinMirrorGlyph('/') == '\\'); + VERIFY(ffSpinMirrorGlyph('\\') == '/'); + VERIFY(ffSpinMirrorGlyph('(') == ')'); + VERIFY(ffSpinMirrorGlyph('o') == 'o'); + VERIFY(ffSpinMirrorGlyph(0x2801) == 0x2808); // braille dot 1 -> dot 4 + VERIFY(ffSpinMirrorGlyph(0x2840) == 0x2880); // braille dot 7 -> dot 8 + for (uint32_t glyph = 0x2800; glyph <= 0x28FF; ++glyph) { + VERIFY(ffSpinMirrorGlyph(ffSpinMirrorGlyph(glyph)) == glyph); + } +} + +static const char testLogo[] = + "$1 /\\\n" + "$1 / \\\n" + "$1 /\\ \\\n" + "$2 / __ \\\n" + "$2/__/ \\__\\"; + +static void testRenderFaces(void) { + FFstrbuf colors[2]; + ffStrbufInitStatic(&colors[0], "36"); + ffStrbufInitStatic(&colors[1], "34"); + FFSpinArt art; + VERIFY(ffSpinArtParse(&art, testLogo, colors, 2)); + + uint32_t width, height; + ffSpinFrameSize(&art, 0.0f, &width, &height); + VERIFY(width >= art.width && height >= art.height); + VERIFY((width - art.width) % 2 == 0 && (height - art.height) % 2 == 0); + uint32_t left = (width - art.width) / 2, top = (height - art.height) / 2; + + FFSpinFrame frame; + VERIFY(ffSpinFrameInit(&frame, width, height)); + + // Face-on, every cell of the art shows up unchanged at its place. + ffSpinRenderArt(&art, 0.0f, 0.0f, &frame); + for (uint32_t y = 0; y < art.height; ++y) { + for (uint32_t x = 0; x < art.width; ++x) { + uint32_t glyph = art.glyphs[y * art.width + x]; + if (!glyph) continue; + size_t index = (size_t) (top + y) * width + left + x; + VERIFY(frame.glyphs[index] == glyph); + VERIFY(frame.colors[index] == art.colors[y * art.width + x]); + } + } + + // Turned around, the back face shows the art mirrored, glyphs included. + ffSpinRenderArt(&art, (float) M_PI, 0.0f, &frame); + for (uint32_t y = 0; y < art.height; ++y) { + for (uint32_t x = 0; x < art.width; ++x) { + uint32_t glyph = art.glyphs[y * art.width + x]; + if (!glyph) continue; + size_t index = (size_t) (top + y) * width + left + (art.width - 1 - x); + VERIFY(frame.glyphs[index] == ffSpinMirrorGlyph(glyph)); + } + } + + // Edge-on, only a thin band of edge shading is left. + ffSpinRenderArt(&art, (float) M_PI / 2.0f, 0.0f, &frame); + uint32_t usedColumns = 0; + for (uint32_t x = 0; x < width; ++x) { + bool used = false; + for (uint32_t y = 0; y < height; ++y) { + uint32_t glyph = frame.glyphs[y * width + x]; + if (glyph) { + used = true; + VERIFY(isWallGlyph(glyph)); + } + } + usedColumns += used; + } + VERIFY(usedColumns > 0 && usedColumns <= 4); + + ffSpinFrameDestroy(&frame); + ffSpinArtDestroy(&art); +} + +// Renders into a frame one cell larger on every side than ffSpinFrameSize +// asks for; the extra ring must stay empty at every angle. +static void verifyFits(const FFSpinArt* art, float tilt) { + uint32_t width, height; + ffSpinFrameSize(art, tilt, &width, &height); + FFSpinFrame frame; + VERIFY(ffSpinFrameInit(&frame, width + 2, height + 2)); + for (uint32_t step = 0; step < 48; ++step) { + ffSpinRenderArt(art, (float) step * 2.0f * (float) M_PI / 48.0f, tilt, &frame); + for (uint32_t x = 0; x < frame.width; ++x) { + VERIFY(frame.glyphs[x] == 0); + VERIFY(frame.glyphs[(size_t) (frame.height - 1) * frame.width + x] == 0); + } + for (uint32_t y = 0; y < frame.height; ++y) { + VERIFY(frame.glyphs[(size_t) y * frame.width] == 0); + VERIFY(frame.glyphs[(size_t) y * frame.width + frame.width - 1] == 0); + } + } + ffSpinFrameDestroy(&frame); +} + +static void testBuiltinLogos(void) { + uint32_t count = 0; + for (uint32_t letter = 0; letter < 26; ++letter) { + for (const FFlogo* logo = ffLogoBuiltins[letter]; *logo->names; ++logo) { + FFSpinArt art; + VERIFY(ffSpinArtParse(&art, logo->lines, nullptr, 0)); + VERIFY(art.width > 0 && art.height > 0); + verifyFits(&art, 0.0f); + if (count % 16 == 0) { + verifyFits(&art, 0.4f); + } + ffSpinArtDestroy(&art); + ++count; + } + } + VERIFY(count > 400); +} + +static void testAnsiOutput(void) { + FFstrbuf colors[1]; + ffStrbufInitStatic(&colors[0], "35"); + FFSpinArt art; + VERIFY(ffSpinArtParse(&art, "a\xe2\x96\x88 b", colors, 1)); + FFSpinFrame frame; + VERIFY(ffSpinFrameInit(&frame, art.width, art.height)); + ffSpinFramePutArt(&frame, &art); + + FF_STRBUF_AUTO_DESTROY plain = ffStrbufCreate(); + ffSpinFrameToAnsi(&frame, &art, false, false, true, &plain); + VERIFY(ffStrbufEqualS(&plain, "a\xe2\x96\x88 b\n")); + + FF_STRBUF_AUTO_DESTROY colored = ffStrbufCreate(); + ffSpinFrameToAnsi(&frame, &art, true, true, true, &colored); + VERIFY(strstr(colored.chars, "\e[0;1;35ma") != nullptr); + VERIFY(strstr(colored.chars, "\e[0m\n") != nullptr); + + ffSpinFrameDestroy(&frame); + ffSpinArtDestroy(&art); +} + +int main(void) { + testRotation(); + testParse(); + testMirror(); + testRenderFaces(); + testBuiltinLogos(); + testAnsiOutput(); + puts("spin tests passed"); + return 0; +}