Compare commits

...

52 Commits

Author SHA1 Message Date
Carter Li 0c3b852bf7 Merge pull request #2613 from fastfetch-cli/dev
Release v2.69.0
2026-09-25 18:46:15 +08:00
李通洲 866c5e89c8 Global: fixes several issues found by AI 2026-09-25 17:28:44 +08:00
Carter Li a1edf7316c GPU: adds more debug log 2026-09-25 12:27:58 +08:00
Carter Li c3164d15e1 TerminalShell: hardens version detection code 2026-09-25 11:34:13 +08:00
Carter Li a2eda0090a Shell (Linux): supports more ksh variants 2026-09-25 11:33:46 +08:00
Carter Li d8797ae314 Shell (Windows): fixes possible buffer overflow 2026-09-25 11:32:56 +08:00
Carter Li 93175aea1d TerminalShell: clears version buffer if detection failed 2026-09-25 10:58:27 +08:00
Carter Li 537635cd9c Logo: adds option logo.position: auto 2026-09-25 09:51:34 +08:00
Carter Li c68bedd9bf CMake: adds a missing test 2026-09-25 08:46:41 +08:00
Carter Li 80a931a87f CPU (Linux): fixes Snapdragon X2 series model detection
Fixes #2611
2026-09-25 08:46:13 +08:00
李通洲 d223b04ad8 Media (Android): adds support 2026-09-25 01:55:23 +08:00
李通洲 0e04ebdf63 Wallpaper (Android): adds support 2026-09-24 23:42:27 +08:00
李通洲 bba45429ae Global: fixes issues found by AI 2026-09-24 21:27:51 +08:00
Carter Li 00ccd8be12 Doc: simplifies changelog 2026-09-24 17:44:48 +08:00
Carter Li 42e4fe3085 General: adds general.preload.lua option 2026-09-24 15:55:23 +08:00
Carter Li fb5622215b Common (Format): fixes lua error message trimming 2026-09-24 15:32:15 +08:00
Carter Li ccf1dcfa6d CMake: disables quickjs by default 2026-09-24 14:17:55 +08:00
Carter Li 0c6a9d6e98 CI: fixes build 2026-09-24 14:11:05 +08:00
Carter Li b6b5dabd54 Packages (Windows): disables winget again 2026-09-24 14:09:41 +08:00
Carter Li fdbaaea962 CMake (macOS): uses lazy_framework if applicate
It's said deprecated. Let's see if it still works.
2026-09-24 13:52:29 +08:00
Carter Li add2339c9c chore: removes doc/wiki
Github doesn't support it
2026-09-24 13:51:28 +08:00
Carter Li e1e2de0929 Gamepad (macOS): adds battery level detection support 2026-09-24 11:15:28 +08:00
Carter Li 59f0f533e7 Doc: removes outdated info [ci skip] 2026-09-24 10:11:59 +08:00
李通洲 28f4f6464b Common (Windows): simplifies winrt helper 2026-09-24 10:09:02 +08:00
李通洲 982313063d Gamepad (Windows): fixes battery level detection 2026-09-24 00:02:27 +08:00
Carter Li 1e7386788f Logo (Sixel): adds modification notice
for 3a9595e121
2026-09-23 23:00:00 +08:00
minnnjuuu 3a9595e121 Logo (Sixel): fixes allocator leak on output allocation failure (#2610) 2026-09-23 22:50:22 +08:00
Carter Li f8df95d146 Common (Lua): don't segfault when formating a null string
Fixes #2608
2026-09-23 22:29:39 +08:00
Carter Li 592ef68d60 Bluetooth (Linux): adds comments about LE device support of BlueZ 2026-09-23 18:04:46 +08:00
Carter Li 0bf6c8df74 Doc: adds wiki to docs as a submodule 2026-09-23 18:04:08 +08:00
Carter Li be15a0942c Chore: fixes building 2026-09-23 15:18:56 +08:00
Carter Li 4a97cc6825 Bluetooth: adds option showType 2026-09-23 15:11:06 +08:00
Carter Li a78d2a7737 Bluetooth (Windows / macOS): adds Bluetooth LE device detection 2026-09-23 14:02:08 +08:00
Carter Li 3930759007 Common (Windows): adds a WinRT helper 2026-09-23 13:33:35 +08:00
李通洲 e21530551d Common (Percent): adds ffRssiToSignalQuality 2026-09-23 13:33:35 +08:00
Carter Li c26bace3c2 Global: fixes a lot bugs found by AI 2026-09-22 22:09:26 +08:00
Carter Li fe49a9624e Host (macOS): adds new models 2026-09-22 09:47:45 +08:00
李通洲 7f3209181e Global: adds general cache layer 2026-09-22 00:45:40 +08:00
李通洲 2f74d6be4d Networking (Linux): fixes possible memleaks 2026-09-21 20:13:56 +08:00
李通洲 256c6a4257 PublicIP / Weather: fixes possible can only be used once due to internal limitations error 2026-09-21 20:08:11 +08:00
Carter Li aaa9153f5a CI (Windows): fixes build
Partially reverts f26795826c
2026-09-21 14:55:12 +08:00
dependabot[bot] f26795826c CI: Bump the github-actions group with 5 updates (#2598)
Bumps the github-actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [cross-platform-actions/action](https://github.com/cross-platform-actions/action) | `1.5.0` | `1.6.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.37.9` | `4.38.0` |
| [vmactions/solaris-vm](https://github.com/vmactions/solaris-vm) | `1.3.9` | `1.4.0` |
| [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) | `2.3` | `3.0` |


Updates `cross-platform-actions/action` from 1.5.0 to 1.6.0
- [Release notes](https://github.com/cross-platform-actions/action/releases)
- [Changelog](https://github.com/cross-platform-actions/action/blob/master/changelog.md)
- [Commits](https://github.com/cross-platform-actions/action/compare/faa0c6197e94aacf1c5956460152c8380d3560a5...e0b9770014ba65d5e0815f15b74031c3a635f641)

Updates `github/codeql-action/init` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `github/codeql-action/analyze` from 4.37.9 to 4.38.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/cdf488f595d80d6e07e03d4674febd5ab45fa938...b96794f015dfd88f77b49b1c93e0fa7110f94c63)

Updates `vmactions/solaris-vm` from 1.3.9 to 1.4.0
- [Release notes](https://github.com/vmactions/solaris-vm/releases)
- [Commits](https://github.com/vmactions/solaris-vm/compare/96d8d976f9e67d82ec6c7e8ce9c1060731f9e21c...a11f407d3720a3717673d4194f19ee63b284977e)

Updates `signpath/github-action-submit-signing-request` from 2.3 to 3.0
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/c92b958760219087e01f8d67a1669ed57afe2627...f6d04783b4569d051e0c80105fe66e82819d0092)

---
updated-dependencies:
- dependency-name: cross-platform-actions/action
  dependency-version: 1.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: vmactions/solaris-vm
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '3.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-21 01:37:15 -05:00
Carter Li 18b31ca875 Doc: fixes json_schema 2026-09-21 14:29:10 +08:00
Carter Li 17b98007b4 Logo (Image): improves compatibility for iTerm when printing gif logo 2026-09-21 14:26:29 +08:00
Carter Li 1023388d60 Doc: adds missing command flags 2026-09-21 14:22:14 +08:00
Carter Li 6842ae0757 Common (Format): fixes module format strings mishandling a value that carries more than one leading ANSI escape sequence 2026-09-21 14:22:14 +08:00
Carter Li b726ee5a85 Chore: fixes a comment [ci skip] 2026-09-21 14:22:14 +08:00
Piotr Kubaj fd08fdb9df Global (FreeBSD): fixes build when libc defines memrchr as a macro (#2599)
FreeBSD 16.0-CURRENT's <string.h> now provides C23-style
qualifier-preserving wrappers for the string search functions.  Besides
the prototype, `memrchr` (like `memchr`, `strchr`, `strstr`, `memmem`,
...) is also defined as a function-like macro built on `_Generic`:

    #define memrchr(b, c, n) __qualsel((b), \
        (const void *)(memrchr)((b), (c), (n)), (memrchr)((b), (c), (n)))

`common/memrchr.h` redeclares the function unconditionally.  When
<string.h> has already been included, that declaration is expanded by
the macro and every translation unit fails with:

    src/common/memrchr.h:11:7: error: expected identifier or '('
       11 | void* memrchr(const void* s, int c, size_t n);

A libc that defines `memrchr` as a macro has necessarily declared the
function it wraps, so skip our declaration in that case.  Nothing
changes on platforms where `memrchr` is not a macro, and the fallback
implementation is still selected by `check_function_exists`.
2026-09-21 01:21:57 -05:00
Natsu 203ccb31fa Packages (Linux): detects rum overlay packages (#2597) 2026-09-21 00:30:25 -05:00
Carter Li 3503facd68 Doc: update changelog [ci skip] 2026-09-21 09:43:09 +08:00
Carter Li b2d48a1908 JsonSchema: updates moduleFormat 2026-09-21 09:25:35 +08:00
Carter Li b803d1ac82 Logo (Image): tidy 2026-09-21 09:25:35 +08:00
183 changed files with 6492 additions and 1431 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
check-filenames =
builtin = clear,rare,usage,informal
skip = */.git,*/cmake-build-*,*/.idea,*/completions,*/presets,*/screenshots,*/tests,*/3rdparty,*/logo/ascii,./src/detection/gpu/asahi_drm.h,./src/detection/cpu/cpu_x86.c,./src/detection/gpu/gpu_bsddrm.c,./src/detection/displayserver/linux/wayland/*-protocol.*,./build,./build-*,./build_*,./src/common/impl/genconfig.c
ignore-words-list = iterm,compiletime,unknwn,pengwin,siduction,master,slave,sur,doas,conexant,ags,bu,renderD,SME,Carmel,kinda,symbole,connexion,comando,processus,lokale,adres,titel,som,environnement,Sistem,Lokal,Lokal,Sistem,Paket,Hava,statics
ignore-words-list = iterm,compiletime,unknwn,pengwin,siduction,master,slave,sur,doas,conexant,ags,bu,renderD,SME,Carmel,kinda,symbole,connexion,comando,processus,lokale,adres,titel,som,environnement,Sistem,Lokal,Lokal,Sistem,Paket,Hava,statics,IIterator,whitelist
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: dragonflybsd
version: '6.4.2'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: freebsd
version: '15.1'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: haiku
version: 'r1beta6'
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
- name: Initialize CodeQL
if: inputs.arch == 'amd64'
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: c
@@ -64,7 +64,7 @@ jobs:
- name: perform CodeQL analysis
if: inputs.arch == 'amd64'
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
- name: list features
run: ./fastfetch --list-features
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: netbsd
version: '10.1'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: omnios
version: 'r151058'
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Start VM
uses: cross-platform-actions/action@faa0c6197e94aacf1c5956460152c8380d3560a5 # v1.5.0
uses: cross-platform-actions/action@e0b9770014ba65d5e0815f15b74031c3a635f641 # v1.6.0
with:
operating_system: openbsd
version: '7.9'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: run VM
uses: vmactions/solaris-vm@96d8d976f9e67d82ec6c7e8ce9c1060731f9e21c # v1.3.9
uses: vmactions/solaris-vm@a11f407d3720a3717673d4194f19ee63b284977e # v1.4.0
with:
usesh: true
envs: 'CMAKE_BUILD_TYPE'
@@ -116,6 +116,7 @@ jobs:
- if: github.event_name == 'push' && github.repository == 'fastfetch-cli/fastfetch'
name: submit signing request
# https://github.com/SignPath/github-action-submit-signing-request/issues/18
uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+1
View File
@@ -16,3 +16,4 @@ fastfetch.kdev4
/*.zh.md
/.work*
/doc/*.md
/fastfetch.wiki
+55 -82
View File
@@ -1,92 +1,65 @@
# 2.69.0
Changes:
* ImageMagick is no longer used for image logos on Windows, macOS and Android, and has been replaced by the platform image frameworks (WIC on Windows, ImageIO on macOS, AImageDecoder on Android). (Logo)
* An image logo needs Android 11 (API 30), the release that introduced the platform image decoder, and is reported as an error on Android 10 and older.
* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available, and is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible.
* The `--logo-recache` option has been replaced by `--logo-cache <bool|regen>`, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo)
* `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss; `false` ignores the image logo cache completely, reading nothing from it and writing nothing to it; `regen` does what `--logo-recache true` used to do. `logo.cache` accepts a boolean, or the string `"regen"`.
* The `waitTime` option of `DiskIO` and `NetIO` now defaults to `250` ms instead of `500`. (DiskIO / NetIO)
* The byte counters are maintained by the kernel as I/O happens, so a shorter sampling window still yields an accurate rate, and both modules now finish about 250 ms sooner. The two modules wait concurrently, so enabling both does not cost twice the wait time.
* The `waitTime` option of `Top` now defaults to `250` ms instead of `500`. (Top)
* The operating system accounts process CPU time in fixed steps, so a window longer than roughly 150 ms does not visibly improve the reported percentages. The module now finishes about 250 ms sooner.
* ImageMagick 6 is deprecated and will be removed in a future release. It remains available for older Debian and Ubuntu releases; upgrade to ImageMagick 7 when possible.
* The experimental QuickJS format scripting is deprecated, disabled by default, and planned for removal in the next release. Re-enable it with `cmake -DENABLE_QUICKJS=ON`, or migrate scripts to Lua. (General)
* Replaced `--logo-recache` and `logo.recache` with `--logo-cache` and `logo.cache`. Use `true` to reuse or create valid cache entries, `false` to disable caching, or `regen` to force regeneration. (Logo)
* Modules selected with `--structure` now honor their options from the JSON config.
Features:
* Improved image logo support
* Backend rewritten
* Added a native image decoding backend on Windows (WIC), macOS (ImageIO) and Android (AImageDecoder).
* Added an embedded libsixel encoder, used to produce sixel output on Windows, macOS and Android. It is reported by `fastfetch --list-features` as "Embedded sixel".
* Enabled chafa image output on Windows, macOS and Android independently of ImageMagick.
* As a result, `fastfetch --sixel X:\path\to\image` now works out of the box on Windows Terminal.
* Image logo cache entries are now validated against the modification time of the source image, so editing an image logo in place invalidates its cached rendering. (Logo)
* Cache entries written by older versions are not reused, as they carry no modification time.
* Image logos can now be animated when the terminal and the image protocol support it. (Logo)
* `--logo-animation-frame <0>` (`logo.animationFrame: 0` in the JSON config) plays a GIF or APNG. Only the `kitty` image protocol can play an animation; the frames are decoded and composed by fastfetch, so no external program is involved.
* `--logo-animation-frame <N>` renders the Nth frame as a still image, and negative values count back from the end, so `-1` is the last frame. This works for the `sixel`, `kitty` and `chafa` logo types. Note that negative values can only be given in the JSON config, as the command line parser reads a leading `-` as another option.
* The default is `1`, which renders a still image, so nothing changes for anyone who does not opt in. The frames come from the platform image framework (WIC on Windows, ImageIO on macOS, AImageDecoder on Android, ImageMagick 7 on Linux); a single-frame GIF falls back to a still image.
* On Android an animation needs Android 12 (API 31), and the repeat count is not available there, so an animation is reported as looping forever. A build with no image decoder, or one built with ImageMagick 6, reports an error instead of silently showing a still image.
* A terminal that supports the kitty graphics protocol but not its animation frames, such as Konsole, shows the first frame.
* Added the CMake option `ENABLE_IMAGE_LOGO`, which defaults to `ON`. Configure with `-DENABLE_IMAGE_LOGO=OFF` to build fastfetch without any image logo support. (Logo)
* Image logos are the only consumer of ImageMagick, chafa, and the embedded libsixel encoder, so none of the three is searched for at configure time, and no image decoding sources are compiled in. This is intended only to reduce binary size on embedded systems (such as OpenWrt).
* The `sixel`, `kitty`, `kitty-direct`, `kitty-icat`, `iterm` and `chafa` logo types are rejected with an error, both on the command line and in the JSON config, and the `auto` logo type never tries an image.
* `--logo-type raw` keeps working: it passes a pre-rendered byte stream through unchanged and needs no decoder, so a logo can still be displayed by converting the image externally.
* Added CPU name and frequency detection support on SPARC. (CPU, Linux)
* Added package detection support for CRUX. (Packages, Linux)
* Exposed in custom format as `{crux}`.
* Improved Android ROM detection (DE, Android)
* Added support for HarmonyOS, HarmonyOS NEXT, Flyme, JOYUI, SmartisanOS, realme UI, HydrogenOS, ZUI, ZUXOS, MyOS, NebulaAIOS, ObricUI, MiFavor, LineageOS, PixelExperience, EUI and 360 UI.
* Added support for MagicUI 3.x, which stores a bare version number instead of a `MagicUI_x.y.z` string.
* Added Samsung OneUI support (#2541)
* It is read from system properties, so no particular Android version is required. This is mostly untested due to lack of available devices running these ROMs. Please report any issues you encounter.
* Improved Camera detection on Android (Camera, Android)
* The camera list is now read from the camera2 NDK instead of `termux-api CameraInfo`, so the Termux:API app is no longer required and no subprocess is spawned.
* This needs Android 7.0 (API 24), which is where the camera2 NDK was introduced.
* Improved Battery detection on Android (Battery, Android)
* The charge level and charging state are now read from the battery properties service over `/dev/binder` instead of `termux-api BatteryStatus`, so the Termux:API app is no longer required and no subprocess is spawned.
* The service interface changed in Android 10, and the right request is picked at run time, so every Android release is covered.
* Battery temperature, cycle count, manufacturer, model name, serial number and manufacture date need the `BATTERY_STATS` permission, which an app cannot obtain, so an app no longer reports them.
* Running as `adb shell` or as root, `dumpsys battery` is used, which also reports the battery temperature and technology, and reports a battery that has reached the critical level as such. An app can read none of the three.
* Improved Display detection on Android (Display, Android)
* The displays are now read from the display service instead of a vendor property that only some Xiaomi devices set. The preferred mode, the physical size, the rotation, the manufacture date and the display id are now reported as well.
* This needs Android 13 (API 33). On Android 12 and older only that vendor property is available to an app, and a device that does not set it reports no display.
* Running as `adb shell` or as root, `dumpsys display` is used as well, which is what covers Android 12 and older.
* The refresh rate is now the rate of the active display mode, and the HDR capability is read from the display itself, for every display rather than only for the built-in one.
* Improved WiFi detection on Android (Wifi, Android)
* The connection details are now read from the WiFi service over `/dev/binder` instead of `termux-api WifiConnectionInfo`, so no subprocess is spawned.
* The Termux:API app is still required, though. It is what carries the WiFi permission that the Termux app does not request itself, so uninstalling it turns this module off.
* A location permission is required as well, and Termux:API is what carries that one too. The WiFi service withholds the SSID and the BSSID from an app that does not hold one, and hands both over once it is granted; until then the signal, the rates, the frequency, the Wi-Fi standard and the security type are reported as usual, and the two names are reported as `<redacted>`. The grant has to be for all the time rather than only while the app is in use, as the service withholds the two names from an app that is not in the foreground either -- which is the state a run over `ssh` is in.
* The interface name and its state, the connection state, the Wi-Fi standard and the security type are now reported as well.
* The security type is reported from Android 12 on. The reply Android 11 sends ends before that field, so the `security` field stays empty there.
* This needs Android 11 (API 30), the release that moved the Wi-Fi framework into an APEX. There is no fallback, so Android 10 and older report an error instead.
* WiFi detection on Android is experimental. The reply the WiFi service hands an app drifts between Android releases and between vendors, and what it contains also depends on the permissions the calling app holds, so a device can report less than its connection has. Please report anything that looks wrong or missing.
* Improved COSMIC detection (DE / WM, Linux)
* The version is now read from the `COSMIC_VERSION` environment variable when it is set.
* Improved accuracy and performance of process name detection in the Top module. (Top, macOS)
* Improved Packages detection on Windows (Packages, Windows)
* `winget` is now detected by default, instead of requiring a build-time opt-in, and the source agreements are accepted non-interactively on a machine that has never accepted them. Opt out by setting `packages.disabled` to `["winget"]`, or by configuring with `cmake -DPACKAGES_DISABLE_WINGET=ON`.
* The count reports the packages the `winget` source knows about, which is not limited to the packages `winget` itself installed. Microsoft Store apps are no longer counted.
* `winget list` takes about 1.5 seconds, so its result is cached. The cache is refreshed when a program is installed or removed, not on a timer, so only the first run after such a change is slow.
* Improved Wallpaper detection on macOS Sonoma and later (#2559, Wallpaper, macOS)
* The image path is now also extracted from the `Configuration` field of the wallpaper plist, and the `NSWorkspace` fallback is used only as a last resort.
* Removed the `kvm` dependency on OpenBSD by using `sysctl` directly. (General, OpenBSD)
* Modules that were selected on the command line via `--structure` / `-s` now honor module options configured in the JSON config. (CommandOption)
* Improved reliability of fastfetch's built-in HTTP client. (PublicIP, Weather)
* It now supports custom ports and can properly handle chunked transfer encoding. It is designed for minimal resource usage and fast performance, and does not support full HTTP features like HTTPS; the `Command` module with `curl` can be used for those.
* Added Umbriel wayland compositor version detection (WM, Linux)
* Improved the player name detection on Windows to show the name Windows shows for it. (Player, Windows)
* An unpackaged player such as Chrome is now reported as `Google Chrome` instead of `Chrome`.
* Added `logo.position: "auto"` and `--logo-position auto` to place wide logos above module output when the terminal has less than 32 columns available for text. (Logo)
* The position can also be selected in interactive `--gen-config`.
* For now, the default position is `left` for compatibility with previous releases. This may change to `auto` in future releases.
* Improved Bluetooth detection support:
* Added Bluetooth Low Energy detection on Windows and macOS.
* Report device type (LE or classic) and signal quality, if available.
* Added Bluetooth Core 6.0–6.3 version reporting. (BluetoothRadio)
* Improved image log support:
* Image logos on Windows, macOS, and Android now use native image processing libraries instead of ImageMagick.
* For package managers: ImageMagick dependencies can be removed on macOS, Windows and Android as they are no longer used for image logos.
* Windows Terminal supports sixel logos out of the box
* Changes to a source image now correctly invalidate its cache. (Logo)
* Added GIF and APNG support to `--kitty` image protocol. Set `logo.animationFrame` to `0` to play animations in compatible kitty terminals; positive or negative values select a still frame. Android animation requires Android 12 (API 31); terminals without kitty graphics protocol support report an error and fall back to the built-in logo instead. (Logo)
* Added cmake option `-DENABLE_IMAGE_LOGO=<BOOL>` (default `ON`) to disable image logos and reduce binary size. `raw` logos remain available. (Logo)
* Improved Linux support:
* Added RakuOS rum overlay package counting (`{rum}`) and CRUX package detection (`{crux}`). (Packages)
* Added CPU name and frequency detection on SPARC. (CPU)
* Improved COSMIC version detection and added Umbriel version detection. (DE / WM)
* Improved Android support:
* Expanded ROM detection to HarmonyOS, Flyme, OneUI, LineageOS, and many other ROMs. (DE)
* Improved camera detection; Android 7 (API 24) or later is required. (Camera)
* Battery level and charging state are now available to apps; detailed battery information requires root or ADB. (Battery)
* Added display mode, physical size, rotation, and HDR information. App-based detection requires Android 13 (API 33); root or ADB can also detect displays on older versions. (Display)
* Added Wi-Fi interface, connection, standard, and security information. Android 11 (API 30) or later is required. Termux:API is needed for the Wi-Fi permission, and location permission is needed to reveal SSID and BSSID; without location permission the two names appear as `<redacted>`. (Wifi)
* Added Wallpaper detection, which effectively reports `/data/system/users/0/wallpaper`. (Wallpaper)
* Added Media detection, root/ADB only. (Media / Player)
* Improved macOS support:
* Improved process-name detection in Top. (Top)
* Improved wallpaper detection on macOS Sonoma and later. (#2559, Wallpaper)
* Improved Windows support:
* `winget` now counts packages from configured sources, excludes Microsoft Store apps, and caches results. It remains disabled by default. (Packages)
* Player names now match the names shown by Windows (for example, Chrome is reported as Google Chrome). (Player)
* Added battery-level detection for more controllers on Windows and macOS. (Gamepad)
* Improved macOS controller name detection (macOS, Gamepad)
* macOS controller names now match the system; configs that filter by name may need updating.
* Requires macOS 11 or later.
* Improved the built-in HTTP client used by PublicIP and Weather; it now supports custom ports and chunked responses. (PublicIP / Weather)
* Reduced the default `waitTime` for DiskIO, NetIO, and Top from 500 ms to 250 ms. (DiskIO / NetIO / Top)
* Added `general.preload.lua` for loading shared Lua helpers before modules run. It requires a Lua-enabled build and starts the Lua interpreter whenever configured. (General)
* Lua format scripting is now considered stable.
Bugfixes:
* Fixed Base64 encoding producing incorrect output for some inputs. (General)
* Fixed image logos not working when ImageMagick is built without a quantum depth suffix in its library name, as is the case on FreeBSD. (Logo, FreeBSD)
* Fixed TerminalFont detection on Windows ignoring Windows Terminal JSON fragment files. (#2573, TerminalFont, Windows)
* Fixed 64-bit values being truncated by `strtoul` on platforms where `unsigned long` is 32-bit. (Swap / PhysicalDisk / PhysicalMemory / GPU)
* Fixed read-only SQLite databases failing with `SQLITE_READONLY` when the database directory is not writable. (Packages)
* This fixes PKG package count detection on FreeBSD.
* Fixed `{#keys}` and `{#title}` in module format strings not honoring the `brightColor` display option. (Format)
* Fixed `paddingTop` and `paddingLeft` being ignored by the `kitty-icat` image logo type. (Logo)
* Fixed issues when running on big-endian platforms.
* Some internal cleanups and optimizations.
* Fixed Snapdragon X2 series model detection on Linux. (#2611, CPU / Linux)
* Fixed image logo caching, padding, and positioning issues, including an iTerm display bug. (Logo)
* Fixed several Windows issues, including Windows Terminal font detection, redirected `--gen-config` output, and gamepad battery reporting for DualShock 4 and Switch controllers. (#2573, TerminalFont; Gamepad; General)
* Fixed formatting and display issues, including ANSI-aware truncation, date/time formatting, and bright key/title colors. (Format / DateTime / Display)
* Fixed `--dynamic-interval` retries and stale results across multiple rounds. (PublicIP / Weather / Display / Monitor / WM / DE / Media / Player / Shell / Terminal)
* Fixed invalid numeric and color options that could cause hangs or excessive output. (Separator / CPUUsage / NetIO / DiskIO / LoadAvg / Colors)
* Fixed JSON output and config validation across several modules, including GPU, Btrfs, Gamepad, Packages, and Custom. (General)
* Fixed GPU memory and device-type reporting, GNOME Classic and Trinity version detection, and default-route family names. (GPU / DE / LocalIP)
* Fixed module output and filtering issues affecting editor failures, sound status, command selection, and ignored keyboard/gamepad devices. (Editor / Sound / Command / Keyboard / Gamepad)
* Fixed package counting with read-only databases and corrected other package detection and JSON output issues. (Packages)
* Fixed Base64 encoding, big-endian handling, and 64-bit value truncation on 32-bit platforms. (General)
Logos:
* Added ALT Atomic
+115 -23
View File
@@ -98,12 +98,16 @@ cmake_dependent_option(ENABLE_ELF "Enable libelf" ON "LINUX OR ANDROID OR Dragon
cmake_dependent_option(ENABLE_THREADS "Enable multithreading" ON "Threads_FOUND" OFF)
option(ENABLE_IMAGE_LOGO "Enable image logos (sixel / kitty / iTerm / chafa)" ON)
if(ENABLE_IMAGE_LOGO)
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "WIN32 OR APPLE OR ANDROID" OFF)
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
endif()
# The four below are declared outside the guard on purpose. `cmake_dependent_option` is a macro: when
# the condition is false it is never expanded, so nothing rewrites the value already sitting in
# CMakeCache.txt, and `cmake -DENABLE_IMAGE_LOGO=OFF .` in an existing build directory would leave
# ENABLE_SIXEL=ON behind -- sixel and chafa would still be compiled and linked while image.c, the
# only file that uses them, is not. The condition does the disabling instead: it is the fourth
# argument's job, and it keeps the option visible and settable in cmake-gui either way.
cmake_dependent_option(ENABLE_IMAGEMAGICK7 "Enable imagemagick 7" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR Haiku OR GNU" OFF)
cmake_dependent_option(ENABLE_IMAGEMAGICK6 "Enable imagemagick 6" ON "ENABLE_IMAGE_LOGO;LINUX OR FreeBSD OR OpenBSD OR NetBSD OR SunOS OR GNU" OFF)
cmake_dependent_option(ENABLE_SIXEL "Enable sixel logo output via the embedded libsixel encoder (Windows, macOS and Android; replaces ImageMagick's SIXEL coder)" ON "ENABLE_IMAGE_LOGO;WIN32 OR APPLE OR ANDROID" OFF)
cmake_dependent_option(ENABLE_CHAFA "Enable chafa" ON "ENABLE_IMAGE_LOGO;ENABLE_IMAGEMAGICK6 OR ENABLE_IMAGEMAGICK7 OR WIN32 OR APPLE OR ANDROID" OFF)
option(ENABLE_ZLIB "Enable zlib" ON)
option(ENABLE_SYSTEM_YYJSON "Use system provided (instead of fastfetch embedded) yyjson library" OFF)
@@ -119,7 +123,7 @@ option(ENABLE_EMBEDDED_PCIIDS "Embed pci.ids into fastfetch, requires `python`"
option(ENABLE_EMBEDDED_AMDGPUIDS "Embed amdgpu.ids into fastfetch, requires `python`" OFF)
option(ENABLE_WORDEXP "Enable using of wordexp(3) if available, instead of glob(3)" ON)
option(ENABLE_LUA "Enable lua scripting for format strings" ON)
option(ENABLE_QUICKJS "Enable quickjs scripting for format strings" ON)
option(ENABLE_QUICKJS "Enable quickjs scripting for format strings" OFF)
option(ENABLE_LIBZFS "Enable libzfs" ON)
option(ENABLE_WCWIDTH "Detect wide-width characters with wcwidth when calculating logo and separator width. Disable for a smaller and slightly faster build, but logos containing CJK/emoji may be measured too narrow and render incorrectly. Such usecases are uncommon." ON)
set(DEFAULT_STRUCTURE "Title:Separator:OS:Host:Kernel:Uptime:Packages:Shell:Display:DE:WM:WMTheme:Theme:Icons:Font:Cursor:Terminal:TerminalFont:CPU:GPU:Memory:Swap:Disk:LocalIp:Battery:PowerAdapter:Locale:Break:Colors" CACHE STRING "Default module structure for detection")
@@ -135,15 +139,30 @@ endif()
# other leftovers, and makes `CONFIGURE_DEPENDS` react to added or removed sources.
file(GLOB FF_MODULE_SRCS CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/src/modules" "${CMAKE_CURRENT_SOURCE_DIR}/src/modules/*/*.c")
set(FF_MODULE_DIRS "")
set(FF_MODULE_SRC_DIRS "")
foreach(FF_MODULE_SRC ${FF_MODULE_SRCS})
get_filename_component(FF_MODULE_DIR "${FF_MODULE_SRC}" DIRECTORY)
get_filename_component(FF_MODULE_NAME "${FF_MODULE_SRC}" NAME_WE)
if("${FF_MODULE_DIR}" STREQUAL "${FF_MODULE_NAME}")
list(APPEND FF_MODULE_DIRS "${FF_MODULE_DIR}")
endif()
list(APPEND FF_MODULE_SRC_DIRS "${FF_MODULE_DIR}")
endforeach()
unset(FF_MODULE_SRCS)
# A directory of sources whose name no source carries registers no module at all, and nothing else
# in the build says so -- the module simply never appears. Reported rather than left to be noticed.
# Directories are compared rather than sources, so that a module which keeps helper sources next to
# its own `<module>.c` does not warn about every one of them.
list(REMOVE_DUPLICATES FF_MODULE_SRC_DIRS)
foreach(FF_MODULE_DIR ${FF_MODULE_SRC_DIRS})
if(NOT FF_MODULE_DIR IN_LIST FF_MODULE_DIRS)
message(WARNING "src/modules/${FF_MODULE_DIR}/ contains C sources but no ${FF_MODULE_DIR}.c, "
"so no module is registered for it. A module directory must hold a source file named after it.")
endif()
endforeach()
unset(FF_MODULE_SRC_DIRS)
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
@@ -161,7 +180,11 @@ string(REGEX MATCHALL "FF_PACKAGES_FLAG_[A-Z]+_BIT" PACKAGE_MANAGERS "${PACKAGE_
string(REGEX REPLACE "FF_PACKAGES_FLAG_([A-Z]+)_BIT" "\\1" PACKAGE_MANAGERS "${PACKAGE_MANAGERS}")
list(SORT PACKAGE_MANAGERS)
foreach(package_manager ${PACKAGE_MANAGERS})
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
if(package_manager STREQUAL "WINGET")
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" ON)
else()
option(PACKAGES_DISABLE_${package_manager} "Disable ${package_manager} package manager detection by default" OFF)
endif()
endforeach()
option(PACKAGES_REMOVE_DISABLED "Remove disabled package managers from the build entirely, instead of just skipping them at runtime" OFF)
@@ -462,6 +485,7 @@ set(LIBFASTFETCH_SRC
src/common/impl/time.c
src/common/impl/edidHelper.c
src/common/impl/base64.c
src/common/impl/FFcache.c
src/common/impl/FFlist.c
src/common/impl/FFstrbuf.c
src/common/impl/path.c
@@ -515,6 +539,11 @@ if(ENABLE_IMAGE_LOGO)
src/logo/image/im7.c
src/logo/image/sixel.c
)
if(ANDROID)
list(APPEND LIBFASTFETCH_SRC
src/logo/image/aid.c
)
endif()
endif()
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
@@ -613,6 +642,7 @@ elseif(ANDROID)
list(APPEND LIBFASTFETCH_SRC
src/common/android/binder.c
src/common/android/dex.c
src/common/android/package.c
src/common/impl/dbus.c
src/common/impl/io_unix.c
src/common/impl/netif_linux.c
@@ -666,6 +696,7 @@ elseif(ANDROID)
src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c
src/detection/gamepad/gamepad_nosupport.c
src/detection/media/media_android.c
src/detection/media/media_linux.c
src/detection/memory/memory_linux.c
src/detection/mouse/mouse_nosupport.c
@@ -687,13 +718,13 @@ elseif(ANDROID)
src/detection/tpm/tpm_nosupport.c
src/detection/uptime/uptime_linux.c
src/detection/users/users_linux.c
src/detection/wallpaper/wallpaper_android.c
src/detection/wallpaper/wallpaper_linux.c
src/detection/wifi/wifi_android.c
src/detection/wm/wm_linux.c
src/detection/de/de_linux.c
src/detection/wmtheme/wmtheme_linux.c
src/detection/camera/camera_android.c
src/logo/image/aid.c
)
elseif(FreeBSD)
list(APPEND LIBFASTFETCH_SRC
@@ -1004,7 +1035,7 @@ elseif(APPLE)
src/detection/libc/libc_apple.c
src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c
src/detection/gamepad/gamepad_apple.c
src/detection/gamepad/gamepad_apple.m
src/detection/media/media_apple.m
src/detection/memory/memory_apple.c
src/detection/mouse/mouse_apple.c
@@ -1070,10 +1101,12 @@ elseif(WIN32)
src/common/windows/registry.c
src/common/windows/unicode.c
src/common/windows/variant.cpp
src/common/windows/winrt.cpp
src/common/windows/version.c
src/detection/battery/battery_windows.c
src/detection/bios/bios_windows.c
src/detection/bluetooth/bluetooth_windows.c
src/detection/bluetooth/bluetooth_windows.cpp
src/detection/bluetoothradio/bluetoothradio_windows.c
src/detection/board/board_windows.c
src/detection/bootmgr/bootmgr_windows.c
@@ -1342,7 +1375,7 @@ elseif(GNU)
src/detection/keyboard/keyboard_nosupport.c
src/detection/libc/libc_linux.c
src/detection/lm/lm_linux.c
src/detection/loadavg/loadavg_linux.c
src/detection/loadavg/loadavg_sunos.c
src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c
src/detection/gamepad/gamepad_nosupport.c
@@ -1903,23 +1936,25 @@ if(LINUX)
elseif(APPLE)
target_link_libraries(libfastfetch
PRIVATE "-framework AVFoundation"
PRIVATE "-framework Accelerate"
PRIVATE "-framework Cocoa"
PRIVATE "-framework CoreFoundation"
PRIVATE "-framework CoreAudio"
PRIVATE "-framework CoreBluetooth"
PRIVATE "-framework CoreFoundation"
PRIVATE "-framework CoreMedia"
PRIVATE "-framework CoreVideo"
PRIVATE "-framework ImageIO"
PRIVATE "-framework Accelerate"
PRIVATE "-framework CoreWLAN"
PRIVATE "-framework GameController"
PRIVATE "-framework IOBluetooth"
PRIVATE "-framework IOKit"
PRIVATE "-framework ImageIO"
PRIVATE "-framework Metal"
PRIVATE "-framework OpenGL"
PRIVATE "-framework OpenCL"
PRIVATE "-framework OpenGL"
PRIVATE "-framework SystemConfiguration"
PRIVATE "-framework VideoToolbox"
PRIVATE "-weak_framework CoreDisplay"
PRIVATE "-weak_framework CoreDisplay"
PRIVATE "-F /System/Library/PrivateFrameworks"
PRIVATE "-weak_framework DisplayServices"
PRIVATE "-weak_framework MediaRemote"
@@ -1939,8 +1974,16 @@ elseif(WIN32)
PRIVATE "winbrand"
PRIVATE "secur32"
PRIVATE "shlwapi"
PRIVATE "windowscodecs"
)
if(ENABLE_IMAGE_LOGO)
# Only src/logo/image/wic.cpp uses WIC, and that file is only compiled with image logos
# enabled (see the WIN32 source list above). Verified by building with
# `-DENABLE_IMAGE_LOGO=OFF` and removing the library from the link line: it links and runs
# without it, and nothing else in the tree references IWIC*.
target_link_libraries(libfastfetch
PRIVATE "windowscodecs"
)
endif()
if(NOT ENABLE_WIN81_COMPAT)
target_link_libraries(libfastfetch
PRIVATE "mincore"
@@ -2021,14 +2064,35 @@ elseif(ANDROID)
endif()
endif()
# Both APIs (AImageDecoder, AMediaCodec) are newer than the API level this build targets --
# AImageDecoder is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28 -- while fastfetch
# keeps running on devices below those levels. The NDK headers mark the entry points unavailable, so
# they have to be weak references: otherwise the executable would fail to load on an older device.
# Guarding every use with __builtin_available() is what makes that safe, and turning the guard
# diagnostic into an error is what keeps a use from slipping past it.
# Three APIs are newer than the API level this build targets: AImageDecoder is 30,
# AndroidBitmap_compress is 30, and AMediaCodec_getName / AMediaCodec_releaseName are 28.
# fastfetch keeps running on devices below those levels, and the NDK headers mark the entry
# points unavailable there, so they have to be weak references -- a strong reference would fail
# the load on an older device. Guarding every use with __builtin_available() is what makes that
# safe, and turning the guard diagnostic into an error keeps a use from slipping past it.
#
# Each one lives in the library that provides it, and that library is linked above:
# AImageDecoder_* and AndroidBitmap_compress are exported by libjnigraphics.so, and
# AMediaCodec_* by libmediandk.so. Checked on a device (Android 16, API 36) with
# `readelf --dyn-syms /system/lib64/libjnigraphics.so` and a dlopen/dlsym probe: every
# AImageDecoder_* entry point resolves there and none of them resolves in libandroid.so, which
# is why libandroid.so is *not* linked -- nothing fastfetch calls comes from it. Keep the
# provider library linked for the reason the NDK requires it: an undefined weak symbol resolves
# to NULL rather than failing the load, and FF_ANDROID_API_AT_LEAST() reads the platform level,
# not the symbol, so it would send the call straight through a NULL pointer on every device new
# enough to pass the guard.
#
# camera_android.c is deliberately not in this list. The camera2 NDK is a *library* that starts at
# API 24, and a weak reference weakens the symbol, never the library: DT_NEEDED would still name
# libcamera2ndk.so, and a device below API 24 would fail to load fastfetch at all. The NDK's guide
# is explicit that there is no such thing as a weak library reference, and that dlopen is the only
# way to reach a library newer than minSdkVersion. Nothing here pins ANDROID_PLATFORM, so a build
# aimed below 24 would need that; on the supported path -- Termux, which targets 24 -- the library
# is always present and the plain call is correct. libjnigraphics itself is one of them and is
# linked unconditionally above, so the encoder only needs its symbol weakened, not its library.
set_source_files_properties(
${CMAKE_CURRENT_SOURCE_DIR}/src/logo/image/aid.c
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/media/media_android.c
${CMAKE_CURRENT_SOURCE_DIR}/src/detection/codec/codec_android.c
PROPERTIES COMPILE_OPTIONS "-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__;-Werror=unguarded-availability")
elseif(Haiku)
@@ -2197,6 +2261,29 @@ endif()
###################
if (BUILD_TESTS)
add_executable(fastfetch-test-cache
tests/cache.c
)
target_link_libraries(fastfetch-test-cache
PRIVATE libfastfetch
)
add_executable(fastfetch-test-logo
tests/logo.c
)
target_link_libraries(fastfetch-test-logo
PRIVATE libfastfetch
)
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(fastfetch-test-cpu
tests/cpu.c
)
target_link_libraries(fastfetch-test-cpu
PRIVATE libfastfetch
)
endif()
add_executable(fastfetch-test-strbuf
tests/strbuf.c
)
@@ -2310,6 +2397,11 @@ if (BUILD_TESTS)
)
enable_testing()
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test(NAME test-cpu COMMAND fastfetch-test-cpu)
endif()
add_test(NAME test-cache COMMAND fastfetch-test-cache)
add_test(NAME test-logo COMMAND fastfetch-test-logo)
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)
+10
View File
@@ -195,6 +195,7 @@
"file-raw": "Text file, printed as is",
"data": "Text data, printed with color code replacement",
"data-raw": "Text data, printed as is",
"command-raw": "Command to run, whose output is printed as is",
"sixel": "Image file, rendered as sixel",
"kitty": "Image file, rendered using the Kitty graphics protocol",
"kitty-direct": "Image file, tells the terminal emulator to read image data from the specified file",
@@ -269,6 +270,14 @@
"type": "num"
}
},
{
"long": "logo-padding-bottom",
"desc": "Set the padding at the bottom of the logo",
"remark": "Only used with \"--logo-position top\"",
"arg": {
"type": "num"
}
},
{
"long": "logo-print-remaining",
"desc": "Specify whether to print remaining logo lines when the logo is taller than the module list",
@@ -284,6 +293,7 @@
"arg": {
"type": "enum",
"enum": {
"auto": "Automatically choose left or top based on terminal width",
"left": "Left",
"top": "Top",
"right": "Right"
+90 -53
View File
@@ -19,15 +19,14 @@
},
"key": {
"description": "Module key\nUse a single space (` `) to hide the key",
"type": "string",
"minLength": 1
"type": "string"
},
"keyColor": {
"description": "Color of the module key. Overrides `display.color.key`",
"description": "Color of the module key. Overrides `display.color.keys`",
"$ref": "#/$defs/colors"
},
"keyWidth": {
"description": "Width of the module key. Overrides `display.keyWidth`",
"description": "Width of the module key. Overrides `display.key.width`",
"type": "integer",
"minimum": 1
},
@@ -131,7 +130,9 @@
"SunOS",
"OpenBSD",
"NetBSD",
"Haiku"
"Haiku",
"GNU",
"Unknown"
]
},
"architectures": {
@@ -150,7 +151,9 @@
"sparc",
"alpha",
"hppa",
"m68k"
"sh",
"m68k",
"Unknown"
]
},
"conditions": {
@@ -274,19 +277,19 @@
"batteryFormat": {
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {model-name}: Battery model name\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
"description": "Output format for the `Battery` module. See Wiki for formatting syntax\n 1. {manufacturer}: Battery manufacturer\n 2. {name}: Battery model name *\n 3. {technology}: Battery technology\n 4. {capacity}: Battery capacity (percentage num)\n 5. {status}: Battery status\n 6. {temperature}: Battery temperature (formatted)\n 7. {cycle-count}: Battery cycle count\n 8. {serial}: Battery serial number\n 9. {manufacture-date}: Battery manufacture date\n 10. {capacity-bar}: Battery capacity (percentage bar)\n 11. {time-days}: Battery time remaining days\n 12. {time-hours}: Battery time remaining hours\n 13. {time-minutes}: Battery time remaining minutes\n 14. {time-seconds}: Battery time remaining seconds\n 15. {time-formatted}: Battery time remaining (formatted)",
"type": "string"
},
"biosFormat": {
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type",
"description": "Output format for the `BIOS` module. See Wiki for formatting syntax\n 1. {date}: BIOS date\n 2. {release}: BIOS release\n 3. {vendor}: BIOS vendor\n 4. {version}: BIOS version\n 5. {type}: Firmware type *",
"type": "string"
},
"bluetoothFormat": {
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {battery-percentage}: Battery percentage number\n 5. {connected}: Is connected\n 6. {battery-percentage-bar}: Battery percentage bar",
"description": "Output format for the `Bluetooth` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {address}: Address\n 3. {type}: Type\n 4. {device-type}: Bluetooth stacks the device answers on\n 5. {battery-percentage}: Battery percentage number\n 6. {battery-percentage-bar}: Battery percentage bar\n 7. {signal-quality}: Signal quality number\n 8. {signal-quality-bar}: Signal quality bar\n 9. {connected}: Is connected",
"type": "string"
},
"bluetoothradioFormat": {
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
"description": "Output format for the `BluetoothRadio` module. See Wiki for formatting syntax\n 1. {name}: Radio name for discovering *\n 2. {address}: Address\n 3. {lmp-version}: LMP version\n 4. {lmp-subversion}: LMP subversion\n 5. {version}: Bluetooth version\n 6. {vendor}: Vendor\n 7. {discoverable}: Discoverable\n 8. {connectable}: Connectable / Pairable",
"type": "string"
},
"boardFormat": {
@@ -298,11 +301,11 @@
"type": "string"
},
"brightnessFormat": {
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
"description": "Output format for the `Brightness` module. See Wiki for formatting syntax\n 1. {percentage}: Screen brightness (percentage num)\n 2. {name}: Screen name *\n 3. {max}: Maximum brightness value\n 4. {min}: Minimum brightness value\n 5. {current}: Current brightness value\n 6. {percentage-bar}: Screen brightness (percentage bar)\n 7. {is-builtin}: Is built-in screen",
"type": "string"
},
"btrfsFormat": {
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
"description": "Output format for the `Btrfs` module. See Wiki for formatting syntax\n 1. {name}: Name / Label *\n 2. {uuid}: UUID\n 3. {devices}: Associated devices\n 4. {features}: Enabled features\n 5. {used}: Size used\n 6. {allocated}: Size allocated\n 7. {total}: Size total\n 8. {used-percentage}: Used percentage num\n 9. {allocated-percentage}: Allocated percentage num\n 10. {used-percentage-bar}: Used percentage bar\n 11. {allocated-percentage-bar}: Allocated percentage bar\n 12. {node-size}: Node size\n 13. {sector-size}: Sector size",
"type": "string"
},
"cameraFormat": {
@@ -314,7 +317,7 @@
"type": "string"
},
"codecFormat": {
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name\n 2. {direction}: Decoder / Encoder\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
"description": "Output format for the `Codec` module. See Wiki for formatting syntax\n 1. {gpu}: GPU name *\n 2. {direction}: Decoder / Encoder *\n 3. {types}: Compatibility alias of codec types\n 4. {platform-api}: Platform API used for detection",
"type": "string"
},
"commandFormat": {
@@ -322,11 +325,11 @@
"type": "string"
},
"cpuFormat": {
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name\n 14. {technology}: CPU technology",
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name (x86 only)\n 14. {technology}: CPU technology (x86 only)",
"type": "string"
},
"cpucacheFormat": {
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result",
"description": "Output format for the `CPUCache` module. See Wiki for formatting syntax\n 1. {result}: Separate result\n 2. {sum}: Sum result\n 3. {level}: Cache level *",
"type": "string"
},
"cpuusageFormat": {
@@ -346,15 +349,15 @@
"type": "string"
},
"displayFormat": {
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name\n 7. {type}: Screen type (Built-in or External)\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
"description": "Output format for the `Display` module. See Wiki for formatting syntax\n 1. {width}: Screen configured width (in pixels)\n 2. {height}: Screen configured height (in pixels)\n 3. {refresh-rate}: Screen configured refresh rate (in Hz)\n 4. {scaled-width}: Screen scaled width (in pixels)\n 5. {scaled-height}: Screen scaled height (in pixels)\n 6. {name}: Screen name *\n 7. {type}: Screen type (Built-in or External) *\n 8. {rotation}: Screen rotation (in degrees)\n 9. {is-primary}: True if being the primary screen\n 10. {physical-width}: Screen physical width (in millimeters)\n 11. {physical-height}: Screen physical height (in millimeters)\n 12. {inch}: Physical diagonal length in inches\n 13. {ppi}: Pixels per inch (PPI)\n 14. {bit-depth}: Bits per color channel\n 15. {hdr-enabled}: True if high dynamic range (HDR) mode is enabled\n 16. {manufacture-year}: Year of manufacturing\n 17. {manufacture-week}: Nth week of manufacturing in the year\n 18. {serial}: Serial number\n 19. {platform-api}: The platform API used when detecting the display\n 20. {hdr-compatible}: True if the display is HDR compatible\n 21. {scale-factor}: HiDPI scale factor\n 22. {preferred-width}: Screen preferred width (in pixels)\n 23. {preferred-height}: Screen preferred height (in pixels)\n 24. {preferred-refresh-rate}: Screen preferred refresh rate (in Hz)\n 25. {dpi}: DPI",
"type": "string"
},
"diskFormat": {
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used\n 2. {size-total}: Size total\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem\n 10. {name}: Label / name\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter\n 21. {mount-from}: Mount from (device path)\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free\n 26. {size-available}: Size available",
"description": "Output format for the `Disk` module. See Wiki for formatting syntax\n 1. {size-used}: Size used\n 2. {size-total}: Size total\n 3. {size-percentage}: Size percentage num\n 4. {files-used}: Files used\n 5. {files-total}: Files total\n 6. {files-percentage}: Files percentage num\n 7. {is-external}: True if external volume\n 8. {is-hidden}: True if hidden volume\n 9. {filesystem}: Filesystem *\n 10. {name}: Label / name *\n 11. {is-readonly}: True if read-only\n 12. {create-time}: Create time in local timezone\n 13. {size-percentage-bar}: Size percentage bar\n 14. {files-percentage-bar}: Files percentage bar\n 15. {days}: Days after creation\n 16. {hours}: Hours after creation\n 17. {minutes}: Minutes after creation\n 18. {seconds}: Seconds after creation\n 19. {milliseconds}: Milliseconds after creation\n 20. {mountpoint}: Mount point / drive letter *\n 21. {mount-from}: Mount from (device path) *\n 22. {years}: Years integer after creation\n 23. {days-of-year}: Days of year after creation\n 24. {years-fraction}: Years fraction after creation\n 25. {size-free}: Size free\n 26. {size-available}: Size available",
"type": "string"
},
"diskioFormat": {
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name\n 4. {dev-path}: Device raw file path\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
"description": "Output format for the `DiskIO` module. See Wiki for formatting syntax\n 1. {size-read}: Size of data read [per second] (formatted)\n 2. {size-written}: Size of data written [per second] (formatted)\n 3. {name}: Device name *\n 4. {dev-path}: Device raw file path *\n 5. {bytes-read}: Size of data read [per second] (in bytes)\n 6. {bytes-written}: Size of data written [per second] (in bytes)\n 7. {read-count}: Number of reads\n 8. {write-count}: Number of writes",
"type": "string"
},
"dnsFormat": {
@@ -390,7 +393,7 @@
"type": "string"
},
"kernelFormat": {
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {display-version}: Display version\n 6. {page-size}: Page size",
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {page-size}: Page size",
"type": "string"
},
"keyboardFormat": {
@@ -410,7 +413,7 @@
"type": "string"
},
"localipFormat": {
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address\n 4. {ifname}: Interface name\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
"description": "Output format for the `LocalIp` module. See Wiki for formatting syntax\n 1. {ipv4}: IPv4 address\n 2. {ipv6}: IPv6 address\n 3. {mac}: MAC address *\n 4. {ifname}: Interface name *\n 5. {is-default-route}: Is default route\n 6. {mtu}: MTU size in bytes\n 7. {speed}: Link speed (formatted)\n 8. {flags}: Interface flags",
"type": "string"
},
"mediaFormat": {
@@ -422,7 +425,7 @@
"type": "string"
},
"monitorFormat": {
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
"description": "Output format for the `Monitor` module. See Wiki for formatting syntax\n 1. {name}: Display name *\n 2. {width}: Native resolution width in pixels\n 3. {height}: Native resolution height in pixels\n 4. {physical-width}: Physical width in millimeters\n 5. {physical-height}: Physical height in millimeters\n 6. {inch}: Physical diagonal length in inches\n 7. {ppi}: Pixels per inch (PPI)\n 8. {manufacture-year}: Year of manufacturing\n 9. {manufacture-week}: Nth week of manufacturing in the year\n 10. {serial}: Serial number\n 11. {refresh-rate}: Maximum refresh rate in Hz\n 12. {hdr-compatible}: True if the display is HDR compatible",
"type": "string"
},
"mouseFormat": {
@@ -430,7 +433,7 @@
"type": "string"
},
"netioFormat": {
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
"description": "Output format for the `NetIO` module. See Wiki for formatting syntax\n 1. {rx-size}: Size of data received [per second] (formatted)\n 2. {tx-size}: Size of data sent [per second] (formatted)\n 3. {ifname}: Interface name *\n 4. {is-default-route}: Is default route\n 5. {rx-bytes}: Size of data received [per second] (in bytes)\n 6. {tx-bytes}: Size of data sent [per second] (in bytes)\n 7. {rx-packets}: Number of packets received [per second]\n 8. {tx-packets}: Number of packets sent [per second]\n 9. {rx-errors}: Number of errors received [per second]\n 10. {tx-errors}: Number of errors sent [per second]\n 11. {rx-drops}: Number of packets dropped when receiving [per second]\n 12. {tx-drops}: Number of packets dropped when sending [per second]",
"type": "string"
},
"openclFormat": {
@@ -442,15 +445,15 @@
"type": "string"
},
"osFormat": {
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel\n 2. {name}: Name of the OS\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS",
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel *\n 2. {name}: Name of the OS *\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS\n 13. {kernel-release}: Release of the kernel",
"type": "string"
},
"packagesFormat": {
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {crux}: Number of crux packages\n 10. {dpkg}: Number of dpkg packages\n 11. {emerge}: Number of emerge packages\n 12. {eopkg}: Number of eopkg packages\n 13. {flatpak-system}: Number of flatpak-system app packages\n 14. {flatpak-user}: Number of flatpak-user app packages\n 15. {guix-home}: Number of guix-home packages\n 16. {guix-system}: Number of guix-system packages\n 17. {guix-user}: Number of guix-user packages\n 18. {hpkg-system}: Number of hpkg-system packages\n 19. {hpkg-user}: Number of hpkg-user packages\n 20. {install-release}: Number of install-release packages\n 21. {kiss}: Number of kiss packages\n 22. {linglong}: Number of linglong packages\n 23. {lpkg}: Number of lpkg packages\n 24. {lpkgbuild}: Number of lpkgbuild packages\n 25. {macports}: Number of macports packages\n 26. {mport}: Number of mport packages\n 27. {moss}: Number of moss packages\n 28. {nix-default}: Number of nix-default packages\n 29. {nix-system}: Number of nix-system packages\n 30. {nix-user}: Number of nix-user packages\n 31. {opkg}: Number of opkg packages\n 32. {pacman}: Number of pacman packages\n 33. {pacman-branch}: Pacman branch on manjaro\n 34. {pacstall}: Number of pacstall packages\n 35. {paludis}: Number of paludis packages\n 36. {pisi}: Number of pisi packages\n 37. {pkg}: Number of pkg packages\n 38. {pkgsrc}: Number of pkgsrc packages\n 39. {pkgtool}: Number of pkgtool packages\n 40. {porg}: Number of porg packages\n 41. {rpm}: Number of rpm packages\n 42. {scoop-global}: Number of scoop-global packages\n 43. {scoop-user}: Number of scoop-user packages\n 44. {snap}: Number of snap packages\n 45. {soar}: Number of soar packages\n 46. {sorcery}: Number of sorcery packages\n 47. {winget}: Number of winget packages\n 48. {xbps}: Number of xbps packages\n 49. {brew-all}: Total number of all brew packages\n 50. {flatpak-all}: Total number of all flatpak app packages\n 51. {guix-all}: Total number of all guix packages\n 52. {hpkg-all}: Total number of all hpkg packages\n 53. {nix-all}: Total number of all nix packages\n 54. {all}: Number of all packages",
"description": "Output format for the `Packages` module. See Wiki for formatting syntax\n 1. {am-system}: Number of am-system packages\n 2. {am-user}: Number of am-user (aka appman) packages\n 3. {appimage}: Number of appimage packages\n 4. {apk}: Number of apk packages\n 5. {brew}: Number of brew packages\n 6. {brew-cask}: Number of brew-cask packages\n 7. {cards}: Number of cards packages\n 8. {choco}: Number of choco packages\n 9. {crux}: Number of crux packages\n 10. {dpkg}: Number of dpkg packages\n 11. {emerge}: Number of emerge packages\n 12. {eopkg}: Number of eopkg packages\n 13. {flatpak-system}: Number of flatpak-system app packages\n 14. {flatpak-user}: Number of flatpak-user app packages\n 15. {guix-home}: Number of guix-home packages\n 16. {guix-system}: Number of guix-system packages\n 17. {guix-user}: Number of guix-user packages\n 18. {hpkg-system}: Number of hpkg-system packages\n 19. {hpkg-user}: Number of hpkg-user packages\n 20. {install-release}: Number of install-release packages\n 21. {kiss}: Number of kiss packages\n 22. {linglong}: Number of linglong packages\n 23. {lpkg}: Number of lpkg packages\n 24. {lpkgbuild}: Number of lpkgbuild packages\n 25. {macports}: Number of macports packages\n 26. {mport}: Number of mport packages\n 27. {moss}: Number of moss packages\n 28. {nix-default}: Number of nix-default packages\n 29. {nix-system}: Number of nix-system packages\n 30. {nix-user}: Number of nix-user packages\n 31. {opkg}: Number of opkg packages\n 32. {pacman}: Number of pacman packages\n 33. {pacman-branch}: Pacman branch on manjaro\n 34. {pacstall}: Number of pacstall packages\n 35. {paludis}: Number of paludis packages\n 36. {pisi}: Number of pisi packages\n 37. {pkg}: Number of pkg packages\n 38. {pkgsrc}: Number of pkgsrc packages\n 39. {pkgtool}: Number of pkgtool packages\n 40. {porg}: Number of porg packages\n 41. {rpm}: Number of rpm packages\n 42. {rum}: Number of rum overlay packages\n 43. {scoop-global}: Number of scoop-global packages\n 44. {scoop-user}: Number of scoop-user packages\n 45. {snap}: Number of snap packages\n 46. {soar}: Number of soar packages\n 47. {sorcery}: Number of sorcery packages\n 48. {winget}: Number of winget packages\n 49. {xbps}: Number of xbps packages\n 50. {brew-all}: Total number of all brew packages\n 51. {flatpak-all}: Total number of all flatpak app packages\n 52. {guix-all}: Total number of all guix packages\n 53. {hpkg-all}: Total number of all hpkg packages\n 54. {nix-all}: Total number of all nix packages\n 55. {all}: Number of all packages",
"type": "string"
},
"physicaldiskFormat": {
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
"description": "Output format for the `PhysicalDisk` module. See Wiki for formatting syntax\n 1. {size}: Device size (formatted)\n 2. {name}: Device name *\n 3. {interconnect}: Device interconnect type\n 4. {dev-path}: Device raw file path *\n 5. {serial}: Serial number\n 6. {physical-type}: Device kind (SSD or HDD)\n 7. {removable-type}: Device kind (Removable or Fixed)\n 8. {readonly-type}: Device kind (Read-only or Read-write)\n 9. {revision}: Product revision\n 10. {temperature}: Device temperature (formatted)",
"type": "string"
},
"physicalmemoryFormat": {
@@ -462,11 +465,11 @@
"type": "string"
},
"poweradapterFormat": {
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"type": "string"
},
"processesFormat": {
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count",
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count\n 2. {threads}: Thread count",
"type": "string"
},
"publicipFormat": {
@@ -482,7 +485,7 @@
"type": "string"
},
"swapFormat": {
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name",
"description": "Output format for the `Swap` module. See Wiki for formatting syntax\n 1. {used}: Used size\n 2. {total}: Total size\n 3. {percentage}: Percentage used (num)\n 4. {percentage-bar}: Percentage used (bar)\n 5. {name}: Name *",
"type": "string"
},
"terminalFormat": {
@@ -554,7 +557,7 @@
"type": "string"
},
"zpoolFormat": {
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name\n 2. {guid}: Zpool guid\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
"description": "Output format for the `Zpool` module. See Wiki for formatting syntax\n 1. {name}: Zpool name *\n 2. {guid}: Zpool guid *\n 3. {state}: Zpool state\n 4. {size-used}: Size used\n 5. {size-allocated}: Size allocated\n 6. {size-total}: Size total\n 7. {used-percentage}: Size used percentage num\n 8. {allocated-percentage}: Size allocated percentage num\n 9. {frag-percentage}: Fragmentation percentage num\n 10. {used-percentage-bar}: Size used percentage bar\n 11. {allocated-percentage-bar}: Size allocated percentage bar\n 12. {frag-percentage-bar}: Fragmentation percentage bar\n 13. {is-readonly}: Is read-only",
"type": "string"
}
},
@@ -782,8 +785,9 @@
},
"position": {
"type": "string",
"description": "Position of the logo",
"description": "Position of the logo. Auto places a left-side logo above the output when the terminal is too narrow to reserve 32 columns for module output.",
"enum": [
"auto",
"left",
"top",
"right"
@@ -934,6 +938,17 @@
"type": "boolean",
"description": "Whether to detect and display component versions. Mainly for benchmarking",
"default": true
},
"preload": {
"description": "Scripts to run while the config is read, before any module runs",
"type": "object",
"additionalProperties": false,
"properties": {
"lua": {
"type": "string",
"description": "Lua script to run once, as soon as the config is read, for example `dofile('/path/to/helpers.lua')`"
}
}
}
}
},
@@ -1576,6 +1591,7 @@
"command",
"colors",
"cursor",
"custom",
"datetime",
"display",
"disk",
@@ -1595,6 +1611,7 @@
"loadavg",
"locale",
"localip",
"logo",
"media",
"memory",
"monitor",
@@ -1744,6 +1761,24 @@
"type": "boolean",
"default": false
},
"showType": {
"description": "Which Bluetooth stacks to look for",
"oneOf": [
{
"const": "classic",
"description": "Look for BR/EDR devices only"
},
{
"const": "le",
"description": "Look for Bluetooth Low Energy devices only"
},
{
"const": "both",
"description": "Look for devices on either stack"
}
],
"default": "both"
},
"percent": {
"$ref": "#/$defs/percent"
},
@@ -2100,7 +2135,7 @@
"showPeCoreCount": {
"description": "Whether to detect and display CPU frequencies for different core types (for example, P-cores and E-cores), if supported",
"type": "boolean",
"default": false
"default": true
},
"key": {
"$ref": "#/$defs/key"
@@ -2134,8 +2169,10 @@
"const": "cpucache",
"description": "Print CPU cache sizes"
},
"percent": {
"$ref": "#/$defs/percent"
"compact": {
"type": "boolean",
"description": "Print one line with a total per cache level instead of one line per level",
"default": false
},
"key": {
"$ref": "#/$defs/key"
@@ -2254,6 +2291,7 @@
"description": "Set the number of white spaces to print before the symbol",
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 0
},
"block": {
@@ -2265,6 +2303,7 @@
"description": "Set the block width in spaces",
"type": "integer",
"minimum": 1,
"maximum": 9,
"default": 3
},
"range": {
@@ -2322,7 +2361,7 @@
"additionalProperties": false,
"properties": {
"type": {
"description": "Run a custom shell command",
"description": "Run custom shell scripts",
"const": "command"
},
"shell": {
@@ -2384,9 +2423,6 @@
"const": "cursor",
"description": "Print cursor style name"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2420,8 +2456,7 @@
"const": "custom"
},
"key": {
"description": "Leave empty to hide the key",
"type": "string"
"$ref": "#/$defs/key"
},
"keyColor": {
"$ref": "#/$defs/keyColor"
@@ -2453,9 +2488,6 @@
"const": "datetime",
"description": "Print the current date and time"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2832,9 +2864,6 @@
"const": "editor",
"description": "Print information about the default editor (`$VISUAL` or `$EDITOR`)"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2867,9 +2896,6 @@
"const": "font",
"description": "Print system font names"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -3775,7 +3801,9 @@
"enum": [
"am",
"apk",
"appimage",
"brew",
"cards",
"choco",
"crux",
"dpkg",
@@ -3784,10 +3812,13 @@
"flatpak",
"guix",
"hpkg",
"installrelease",
"kiss",
"linglong",
"lpkg",
"lpkgbuild",
"macports",
"moss",
"mport",
"nix",
"opkg",
@@ -3796,10 +3827,14 @@
"paludis",
"pisi",
"pkg",
"pkgsrc",
"pkgtool",
"porg",
"rpm",
"rum",
"scoop",
"snap",
"soar",
"sorcery",
"winget",
"xbps"
@@ -3811,8 +3846,7 @@
"description": "Enable all package managers",
"type": "null"
}
],
"default": []
]
},
"combined": {
"description": "Whether to combine related package managers into single counts (e.g., nix-system + nix-user = nix)",
@@ -4039,10 +4073,9 @@
"const": "publicip"
},
"url": {
"description": "URL of the public IP detection server to use. Only HTTP is supported",
"description": "URL of the public IP detection server to use. Only HTTP is supported\nIf empty, `http://ipinfo.io/json` is requested (`http://v6.ipinfo.io/json` when `ipv6` is set), with HTTP compression and TCP Fast Open enabled. A non-empty URL disables both and makes `ipv6` have no effect",
"type": "string",
"format": "url",
"default": "http://ipinfo.io/ip"
"format": "url"
},
"timeout": {
"description": "Time in milliseconds to wait for the public IP server to respond\nSet to 0 to disable the timeout",
@@ -4100,6 +4133,7 @@
"description": "Number of times to repeat the separator string, or 0 to auto-detect",
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 0
},
"condition": {
@@ -4804,6 +4838,9 @@
"const": "wifi",
"description": "Print connected Wi-Fi info (SSID, connection and security protocol)"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
+10 -2
View File
@@ -114,7 +114,7 @@ that 11.3 MB was `images/` fixtures, so the meaningful comparison is code:
## Local modifications
Three deliberate divergences from upstream. Re-apply all three when re-syncing.
Four deliberate divergences from upstream. Re-apply all four when re-syncing.
1. **`sixel.h` — `SIXELAPI` is empty.** Upstream defines it as
`__declspec(dllexport)` on Windows. This subset is compiled into `libfastfetch`
@@ -152,6 +152,14 @@ Three deliberate divergences from upstream. Re-apply all three when re-syncing.
not fill, so the identical bytes look correct there. It is also why Linux never showed
the problem: that path goes through ImageMagick, which already sends `P2 = 1`.
4. **`output.c` — release the allocator reference if output allocation fails.**
`sixel_output_new()` creates an allocator or takes an additional reference before
allocating the output context. If that allocation fails, the reference is not owned by
an output object and must be released before returning `SIXEL_BAD_ALLOCATION`.
Fastfetch passes a null allocator, so without this cleanup the newly created allocator
becomes unreachable on this error path. The successful path and returned error code are
unchanged.
The vendored sources are also exempted from `-Wconversion` in `CMakeLists.txt`:
upstream has ~160 implicit int→`unsigned char` narrowing warnings. Everything else
(`-Wall -Wextra` and fastfetch's `-Werror=` set) applies unchanged, and the subset
@@ -171,7 +179,7 @@ done
cp /tmp/libsixel/include/sixel.h .
cp /tmp/libsixel/LICENSE /tmp/libsixel/LICENSE.sixel /tmp/libsixel/LICENSE.pnmcolormap .
# 2. re-apply the three local modifications listed above
# 2. re-apply the four local modifications listed above
# 3. verify
```
+1
View File
@@ -59,6 +59,7 @@ sixel_output_new(
sixel_helper_set_additional_message(
"sixel_output_new: sixel_allocator_malloc() failed.");
status = SIXEL_BAD_ALLOCATION;
sixel_allocator_unref(allocator);
goto end;
}
+2 -1
View File
@@ -6,6 +6,7 @@
"modifications": [
"sixel.h: SIXELAPI is empty, so the statically linked subset exports nothing",
"config.h: hand-written, because only the vendored files read it",
"tosixel.c: DCS P2 initialised to 1, so unset pixels are not filled with colour-table entry 0"
"tosixel.c: DCS P2 initialised to 1, so unset pixels are not filled with colour-table entry 0",
"output.c: release the allocator reference if output-context allocation fails"
]
}
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "fastfetch.h"
// A cache for a value that is built at most once per generation.
//
// The value itself is owned by the entry owner (usually a file scope `static`); the entry only
// remembers whether that value is still valid and knows how to drop it. `--dynamic-interval`
// starts a new generation for every round, dropping all the values that have been built so far,
// so the modules re-detect them instead of replaying the first round's snapshot.
//
// An entry registers itself the first time it is used. There is therefore no central list to keep
// in sync, and a cache cannot be forgotten in it. Do not use this type for state that is carried
// across calls to derive a rate (cpuusage, diskio, netio, top): that is a baseline rather than a
// snapshot, and dropping it makes the first sample of a round read as zero.
//
// The storage pointed to by `storage` must stay valid for the whole run; only the value behind it
// is dropped by an invalidation.
//
// Not thread safe: entries are only touched from a single thread, on a frame boundary, while the
// modules are not running.
typedef struct FFcacheEntry {
const char* name; // For diagnostics.
void* storage; // Owned by the entry owner. May be nullptr.
void (*init)(void* storage); // Optional. Runs whenever the entry is stale.
void (*destroy)(void* storage); // Optional. Must be idempotent, and tolerate a partially initialized storage.
bool initialized;
bool registered;
struct FFcacheEntry* next; // Internal: registry link.
} FFcacheEntry;
// Returns `entry->storage`, running `init` on it first when the entry is stale. The entry is marked
// initialized before `init` runs, so a call made from within `init` returns the partially built
// value instead of recursing.
[[gnu::nonnull(1)]] void* ffCacheGet(FFcacheEntry* entry);
// Staleness test for entries whose initialization needs arguments that are not part of the cache
// identity (such as the cover file request of `ffDetectMedia`) and therefore cannot live behind
// `init(void*)`. Returns true exactly once per generation: the caller must then build the value
// itself.
[[gnu::nonnull(1), nodiscard]] bool ffCacheBeginInit(FFcacheEntry* entry);
// Drops the cached value of one entry, so that the next request builds it again. Idempotent, and
// safe on an entry that has never been used.
[[gnu::nonnull(1)]] void ffCacheInvalidate(FFcacheEntry* entry);
// Drops every value that has been built so far. Called once per `--dynamic-interval` round.
void ffCacheInvalidateAll(void);
+83 -11
View File
@@ -47,11 +47,12 @@ void ffBinderClose(FFBinder* binder) {
binder->sharedSize = 0;
}
const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply) {
const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply) {
reply->size = 0;
reply->code = 0;
reply->flags = 0;
reply->handleCount = 0;
reply->fdCount = 0;
if (parcel->truncated) {
return "Binder parcel does not fit the caller buffer";
@@ -62,7 +63,7 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
.target = { .handle = handle },
.cookie = 0,
.code = code,
.flags = 0, // a synchronous call; TF_ONE_WAY is the only flag the kernel acts on here
.flags = flags,
.sender_pid = 0,
.sender_euid = 0,
.data_size = parcel->size,
@@ -146,8 +147,10 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
}
}
// Walk the offset table for the flat objects the service sent us and keep the
// strong handles: those are the ones a later transaction can address.
// Walk the offset table for the flat objects the service sent us. A reply may carry
// both kinds at once and they are not interchangeable: a handle is a reference that
// has to be acquired (see below) to outlive the reply buffer, while a file
// descriptor was installed in our own fd table by the kernel and is simply ours.
for (size_t offset = 0; offset + sizeof(binder_size_t) <= offsetsSize; offset += sizeof(binder_size_t)) {
binder_size_t objectOffset = 0;
memcpy(&objectOffset, offsets + offset, sizeof(objectOffset));
@@ -156,11 +159,17 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
}
struct flat_binder_object object;
memcpy(&object, payload + objectOffset, sizeof(object));
if (object.hdr.type != BINDER_TYPE_HANDLE) {
continue;
}
if (reply->handleCount < FF_BINDER_MAX_HANDLES) {
reply->handles[reply->handleCount++] = object.handle;
if (object.hdr.type == BINDER_TYPE_HANDLE) {
if (reply->handleCount < FF_BINDER_MAX_HANDLES) {
reply->handles[reply->handleCount++] = object.handle;
}
} else if (object.hdr.type == BINDER_TYPE_FD) {
if (reply->fdCount < FF_BINDER_MAX_FDS) {
reply->fds[reply->fdCount++] = (int32_t) object.handle;
} else {
// Nothing is going to hold this one and nothing is going to close it.
close((int) object.handle);
}
}
}
@@ -197,7 +206,13 @@ const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, c
.read_consumed = 0,
.read_buffer = 0,
};
ioctl(binder->fd, BINDER_WRITE_READ, &release);
// This one ioctl carries the whole tail, so its failure is not cosmetic: without
// BC_ACQUIRE the handles handed to the caller are only weakly referenced and every
// later transaction on them fails with BR_FAILED_REPLY, which says nothing about the
// real cause. Reported, but only when nothing more specific is already at hand.
if (ioctl(binder->fd, BINDER_WRITE_READ, &release) != 0 && error == nullptr) {
error = "Releasing the binder reply failed";
}
return error;
}
@@ -218,7 +233,7 @@ const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t t
uint8_t replyBuffer[256];
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, &parcel, &reply);
const char* error = ffBinderTransact(binder, FF_BINDER_SERVICE_MANAGER_HANDLE, transactionCode, 0, &parcel, &reply);
if (error != nullptr) {
return error;
}
@@ -235,3 +250,60 @@ const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t t
*handle = reply.handles[0];
return nullptr;
}
void ffBinderServiceHandleRelease(FFBinderServiceHandle* service) {
if (service->binder == nullptr || service->handle == 0) {
return;
}
// The mirror of the acquire at the end of ffBinderTransact(): one strong and one weak reference
// each, dropped in the same order they were taken. The kernel keeps the service node alive until
// the last one goes, so this is what stops a short-lived fastfetch from holding one for the whole
// of its life.
const uint32_t commands[] = { BC_RELEASE, BC_DECREFS };
uint8_t tail[2 * 2 * sizeof(uint32_t)];
size_t tailSize = 0;
for (size_t i = 0; i < 2; i++) {
memcpy(tail + tailSize, &commands[i], sizeof(uint32_t));
tailSize += sizeof(uint32_t);
memcpy(tail + tailSize, &service->handle, sizeof(uint32_t));
tailSize += sizeof(uint32_t);
}
struct binder_write_read release = {
.write_size = tailSize,
.write_consumed = 0,
.write_buffer = (binder_uintptr_t) (uintptr_t) tail,
.read_size = 0,
.read_consumed = 0,
.read_buffer = 0,
};
// Not reported: this runs from a cleanup attribute, where there is no caller left to report to,
// and the only way for it to fail is a binder that is already dead -- which every earlier call on
// the same binder would have said already.
ioctl(service->binder->fd, BINDER_WRITE_READ, &release);
service->handle = 0;
}
void ffBinderAcquiredRelease(FFBinderAcquired* acquired) {
if (acquired->replied == nullptr || *acquired->replied == false) {
return; // nothing was acquired
}
// A descriptor is not a reference: the kernel installed it in our own fd table when it delivered
// the reply, and nobody else holds it, so closing it is the whole of giving it back.
for (uint32_t i = 0; i < acquired->fdCount; i++) {
close(acquired->fds[i]);
}
// The mirror of the acquire the transaction appended to its tail, in the same order it took them.
// No binder to talk through means the handles went with it when it was closed.
if (acquired->binder == nullptr || acquired->binder->fd < 0) {
return;
}
for (uint32_t i = 0; i < acquired->handleCount; i++) {
FFBinderServiceHandle handle = { .binder = acquired->binder, .handle = acquired->handles[i] };
ffBinderServiceHandleRelease(&handle);
}
}
+59 -6
View File
@@ -19,8 +19,6 @@
// * A handle arriving in a reply is owned by that reply buffer. It has to be acquired
// (BC_ACQUIRE + BC_INCREFS) before BC_FREE_BUFFER; otherwise only a weak reference survives and
// every later transaction on it fails, because the kernel looks up a strong reference.
//
// See .workbuddy-ai/android-binder-raw-client.md for the full write-up and the tooling.
#include "fastfetch.h" // IWYU pragma: keep
@@ -51,6 +49,12 @@
// the surplus handles are left unacquired and will fail on first use.
#define FF_BINDER_MAX_HANDLES 4
// How many file descriptors a reply may hand over. A descriptor is not a handle: the kernel installs
// it in the caller's own fd table as it delivers the reply, so it is owned rather than borrowed and
// the caller closes it. A reply carrying more than this still succeeds, and the surplus is closed
// again by ffBinderTransact() because nothing would be left holding it.
#define FF_BINDER_MAX_FDS 4
// The three int32 values Parcel::writeInterfaceToken() puts in front of the descriptor, and that
// Parcel::enforceInterface() checks the last of. These come from libbinder rather than from the
// kernel, which is why they are spelled out here.
@@ -159,10 +163,12 @@ typedef struct FFBinderReply {
uint32_t flags; // TF_* of the reply
uint32_t handleCount;
uint32_t handles[FF_BINDER_MAX_HANDLES];
uint32_t fdCount;
int32_t fds[FF_BINDER_MAX_FDS]; // owned by the caller, which closes them
} FFBinderReply;
[[nodiscard]] static inline FFBinderReply ffBinderReplyCreate(uint8_t* data, size_t capacity) {
return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {} };
return (FFBinderReply) { .data = data, .capacity = capacity, .size = 0, .code = 0, .flags = 0, .handleCount = 0, .handles = {}, .fdCount = 0, .fds = {} };
}
// A reply carrying TF_STATUS_CODE is not an AIDL parcel at all: the first four bytes are the raw
@@ -196,13 +202,21 @@ typedef struct FFBinderReply {
return value;
}
// Synchronous transaction. Returns nullptr on success, a static message otherwise. Handles the
// reply carries are acquired and added to `reply->handles` before the reply buffer is freed.
// Synchronous transaction. Returns nullptr on success, a static message otherwise.
//
// `flags` carries the TF_* values for the request, and one of them is not cosmetic: a reply that
// carries a file descriptor is only delivered at all when the request asked with TF_ACCEPT_FDS. A
// request that did not gets BR_FAILED_REPLY instead, which reads like a service that is not running
// rather than like a missing flag. TF_ONE_WAY is the other one the kernel acts on, and it is what
// makes a call fire-and-forget -- there is then no reply to fill `reply` from.
//
// Handles the reply carries are acquired and added to `reply->handles` before the reply buffer is
// freed; descriptors are added to `reply->fds`, which the caller owns and closes.
//
// `handle` 0 is the service manager; every other handle comes from ffBinderLookupService(). A
// service that never replies blocks this call forever -- none of the services fastfetch reads does,
// but there is no timeout here to fall back on.
[[gnu::nonnull(1, 4, 5), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, const FFBinderParcel* parcel, FFBinderReply* reply);
[[gnu::nonnull(1, 5, 6), nodiscard]] const char* ffBinderTransact(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, const FFBinderParcel* parcel, FFBinderReply* reply);
// ---------------------------------------------------------------------------------------------
// Service manager
@@ -222,3 +236,42 @@ typedef struct FFBinderReply {
// Resolves a service name to a handle through the service manager. Returns nullptr on success.
[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffBinderLookupService(FFBinder* binder, const char* name, uint32_t transactionCode, uint32_t* handle);
// A handle resolved through the service manager, held together with the binder it came from so that a
// single cleanup attribute can give it back on every path out of the function that took it. Zeroed --
// or left at handle 0 by a lookup that has not run or did not succeed -- it is a no-op, which is what
// makes it safe to declare above the lookup itself.
typedef struct FFBinderServiceHandle {
FFBinder* binder;
uint32_t handle;
} FFBinderServiceHandle;
// Returns the strong and weak references ffBinderLookupService() acquired on the handle. Usable as a
// cleanup attribute; safe on a zeroed or already released FFBinderServiceHandle.
[[gnu::nonnull(1)]] void ffBinderServiceHandleRelease(FFBinderServiceHandle* service);
// ---------------------------------------------------------------------------------------------
// Handles a transaction acquired
// ---------------------------------------------------------------------------------------------
// Whatever ffBinderTransact() collected for the caller, released together on the way out of a
// function -- the handles it acquired, plus the descriptors it delivered, which are owned rather
// than borrowed and would otherwise stay open for the life of the process.
//
// Unlike FFBinderServiceHandle this cannot be declared before the transaction runs, because which
// handles came back is only known afterwards. `replied` is therefore a separate flag the caller
// raises once ffBinderTransact() has returned: it acquires the handles even when it goes on to fail
// (a reply that did not fit the caller buffer still had its BC_ACQUIRE tail sent), so releasing
// without it would drop a reference that was never taken.
typedef struct FFBinderAcquired {
FFBinder* binder;
const uint32_t* handles;
uint32_t handleCount;
const int32_t* fds;
uint32_t fdCount;
const bool* replied;
} FFBinderAcquired;
// Usable as a cleanup attribute; safe on a zeroed FFBinderAcquired, and on one whose transaction did
// not reply.
[[gnu::nonnull(1)]] void ffBinderAcquiredRelease(FFBinderAcquired* acquired);
+119 -31
View File
@@ -55,10 +55,37 @@ static uint32_t dexU32(const uint8_t* p) {
return (uint32_t) p[0] | ((uint32_t) p[1] << 8) | ((uint32_t) p[2] << 16) | ((uint32_t) p[3] << 24);
}
// LEB128, at most five bytes for the 32-bit values a dex stores this way.
static uint32_t dexUleb128(const uint8_t** p) {
// The dex header points at its tables with offsets that a corrupt or hostile file is free to set
// anywhere, including past the end of the mapping. Testing `end - p` alone does not catch that: once
// `p` is past `end` the subtraction is negative and casting it to size_t turns it into a huge value,
// so the comparison passes and the read goes off the end. Both directions have to be one test.
static bool dexInRange(const uint8_t* dex, const uint8_t* end, const uint8_t* p, size_t size) {
return p >= dex && p <= end && (size_t) (end - p) >= size;
}
// Same, for a table of `count` elements of `elementSize` bytes. `count` is 64-bit because the
// callers add one to an index that came out of the file, and doing that in 32 bits can wrap to zero
// and make the check pass for an index that is not in the table. Dividing rather than multiplying is
// what keeps a hostile `count` from wrapping the size_t on a 32-bit build -- armeabi-v7a is one, and
// there `count * 4` overflows for any count above 0x3FFFFFFF and the check passes again.
static bool dexTableInRange(const uint8_t* dex, const uint8_t* end, const uint8_t* p, uint64_t count, size_t elementSize) {
if (p < dex || p > end) {
return false;
}
return (size_t) (end - p) / elementSize >= count;
}
// LEB128, at most five bytes for the 32-bit values a dex stores this way. `end` bounds every read:
// the lengths and indexes decoded here come out of the file, so a truncated one must not walk off
// the end. A read that runs into `end` yields what was decoded so far, and the callers treat the
// resulting value as untrusted -- it is either compared against another count or used as an index
// that is range checked in turn.
static uint32_t dexUleb128(const uint8_t** p, const uint8_t* end) {
uint32_t value = 0;
for (int shift = 0; shift <= 28; shift += 7) {
if (*p >= end) {
break;
}
const uint8_t byte = *(*p)++;
value |= (uint32_t) (byte & 0x7f) << shift;
if ((byte & 0x80) == 0) {
@@ -68,22 +95,60 @@ static uint32_t dexUleb128(const uint8_t** p) {
return value;
}
// encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes. A
// `static final int` constant is written as VALUE_INT, whose payload is a sign-extended
// little-endian integer. Reading through 64 bits keeps every shift in range even if the size field
// is nonsense; the caller's length check is what rejects such a dex.
static int32_t dexEncodedInt(const uint8_t** p) {
// The type tag a dex encoded_value carries in the low five bits of its header byte, per the
// `encoded_value` table of the dex format (`VALUE_INT = 0x04`, sign-extended four-byte integer).
// 0x1f is VALUE_BOOLEAN in that same table, which is the trap: naming it VALUE_INT and comparing
// against it rejects every real int and accepts booleans.
#define FF_DEX_VALUE_INT 0x04
// encoded_value: one header byte holding ((size - 1) << 5) | type, then that many payload bytes.
//
// The `static_values` array is not a list of ints: `IInterface.DESCRIPTOR` is a `static final
// String` and sits in it, and a real Stub class mixes ints, strings and booleans. So a value whose
// tag is not VALUE_INT is skipped -- its payload is stepped over to keep the walk aligned with the
// field list -- and only `isInt` reports whether the *wanted* field ended up being one. Failing on
// the first non-int instead would abort on `DESCRIPTOR`, before the transaction constants that
// follow it are ever reached. Measured on a device: `IWifiManager$Stub` carries 347 static fields
// whose values are mostly VALUE_INT but include that string, and `getConnectionInfo` is 98 there.
//
// A payload wider than four bytes is not an int either, and the byte count is what keeps a size
// field read out of a corrupt file from shifting past the end. Reading through 64 bits keeps every
// shift in range while counting down; `end` is what keeps a truncated payload from running off the
// end of the mapping.
static bool dexEncodedValue(const uint8_t** p, const uint8_t* end, int32_t* result, bool* isInt) {
if (*p >= end) {
return false;
}
const uint8_t header = *(*p)++;
const uint32_t type = (uint32_t) (header & 0x1f);
const uint32_t size = (uint32_t) (header >> 5) + 1;
if (size > 8) {
return false;
}
uint64_t value = 0;
for (uint32_t i = 0; i < size && i < 8; ++i) {
value |= (uint64_t) (*(*p)++) << (i * 8);
for (uint32_t i = 0; i < size; ++i) {
if (*p >= end) {
return false;
}
if (i < sizeof(value)) {
value |= (uint64_t) (*(*p)++) << (i * 8);
} else {
(void) *(*p)++;
}
}
const uint32_t bits = size * 8;
if (bits < 64 && (value & (1ull << (bits - 1))) != 0) {
value |= ~((1ull << bits) - 1);
*isInt = type == FF_DEX_VALUE_INT && size <= sizeof(uint32_t);
if (*isInt) {
// VALUE_INT is sign-extended from its own width, not from 32 bits: a one-byte -1 is written
// as `21 ff`, one byte of 0xff, and has to come back as -1 rather than 255.
const uint32_t bits = size * 8;
if ((value & (1ull << (bits - 1))) != 0) {
value |= ~((1ull << bits) - 1);
}
}
return (int32_t) (uint32_t) value;
*result = (int32_t) (uint32_t) value;
return true;
}
// string_data_item: a uleb128 length in UTF-16 code units, then MUTF-8 bytes and a NUL terminator.
@@ -91,14 +156,17 @@ static int32_t dexEncodedInt(const uint8_t** p) {
// bytes can be used as they are.
static const char* dexString(const uint8_t* dex, const uint8_t* end, uint32_t index) {
const uint8_t* ids = dex + dexU32(dex + FF_DEX_OFF_STRING_IDS);
if (ids < dex || (size_t) (end - ids) < (size_t) index * 4 + 4) {
// `string_ids_size` is not read: the check is that the entry `index` names lies inside the
// mapping, which is what the read below needs, and the table is the only thing that can say it
// without trusting a second field out of the same file.
if (!dexTableInRange(dex, end, ids, (uint64_t) index + 1, 4)) {
return nullptr;
}
const uint8_t* p = dex + dexU32(ids + (size_t) index * 4);
if (p < dex || p >= end) {
return nullptr;
}
(void) dexUleb128(&p);
(void) dexUleb128(&p, end);
if (p >= end) {
return nullptr;
}
@@ -123,7 +191,7 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla
// The class's type index, so that the class_def_item and the field ids can be filtered by class.
const uint8_t* types = dex + dexU32(dex + FF_DEX_OFF_TYPE_IDS);
const uint32_t typeCount = dexU32(dex + FF_DEX_OFF_TYPE_IDS_SIZE);
if (types < dex || (size_t) (end - types) < (size_t) typeCount * 4) {
if (!dexTableInRange(dex, end, types, typeCount, 4)) {
return "The dex type table is out of range";
}
uint32_t typeIndex = UINT32_MAX;
@@ -140,7 +208,7 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla
const uint8_t* classes = dex + dexU32(dex + FF_DEX_OFF_CLASS_DEFS);
const uint32_t classCount = dexU32(dex + FF_DEX_OFF_CLASS_DEFS_SIZE);
if (classes < dex || (size_t) (end - classes) < (size_t) classCount * FF_DEX_CLASS_DEF_SIZE) {
if (!dexTableInRange(dex, end, classes, classCount, FF_DEX_CLASS_DEF_SIZE)) {
return "The dex class table is out of range";
}
@@ -152,15 +220,16 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla
const uint8_t* fields = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_DATA);
const uint8_t* values = dex + dexU32(classDef + FF_DEX_OFF_CLASS_DEF_STATIC_VALUES);
if (fields < dex || fields >= end || values < dex || values >= end) {
// One byte each, so that the first uleb128 below has something to read.
if (!dexInRange(dex, end, fields, 1) || !dexInRange(dex, end, values, 1)) {
return "The dex class data is out of range";
}
const uint32_t staticFields = dexUleb128(&fields);
(void) dexUleb128(&fields); // instance_fields_size
(void) dexUleb128(&fields); // direct_methods_size
(void) dexUleb128(&fields); // virtual_methods_size
const uint32_t staticValues = dexUleb128(&values);
const uint32_t staticFields = dexUleb128(&fields, end);
(void) dexUleb128(&fields, end); // instance_fields_size
(void) dexUleb128(&fields, end); // direct_methods_size
(void) dexUleb128(&fields, end); // virtual_methods_size
const uint32_t staticValues = dexUleb128(&values, end);
if (staticFields != staticValues) {
return "The dex static fields and values do not pair up";
}
@@ -168,18 +237,34 @@ static const char* dexStaticInt(const uint8_t* dex, size_t size, const char* cla
const uint8_t* fieldIds = dex + dexU32(dex + FF_DEX_OFF_FIELD_IDS);
uint32_t fieldIndex = 0;
for (uint32_t j = 0; j < staticFields; ++j) {
if (fields >= end) {
// The two arrays are walked in step, so running out of either one is a truncation.
if (fields >= end || values >= end) {
return "The dex static field list is truncated";
}
fieldIndex += dexUleb128(&fields); // field_idx_diff
(void) dexUleb128(&fields); // access_flags
const int32_t value = dexEncodedInt(&values);
if ((size_t) (end - fieldIds) < ((size_t) fieldIndex + 1) * FF_DEX_FIELD_ID_SIZE) {
const uint32_t fieldIndexDiff = dexUleb128(&fields, end); // field_idx_diff
if (fieldIndexDiff > UINT32_MAX - fieldIndex) {
return "The dex field index is out of range";
}
fieldIndex += fieldIndexDiff;
(void) dexUleb128(&fields, end); // access_flags
int32_t value;
bool isInt;
if (!dexEncodedValue(&values, end, &value, &isInt)) {
return "The dex static value list is truncated";
}
if (!dexTableInRange(dex, end, fieldIds, (uint64_t) fieldIndex + 1, FF_DEX_FIELD_ID_SIZE)) {
return "The dex field table is out of range";
}
const char* name = dexString(dex, end, dexU32(fieldIds + (size_t) fieldIndex * FF_DEX_FIELD_ID_SIZE + FF_DEX_OFF_FIELD_ID_NAME));
if (name != nullptr && strcmp(name, fieldName) == 0) {
if (!isInt) {
// The field exists but is not an int, so its value is not the constant asked
// for. Reported here rather than on the first non-int in the list, because the
// list legitimately holds strings (`DESCRIPTOR`) and booleans.
return "The dex static field is not an int";
}
*result = value;
return nullptr;
}
@@ -335,8 +420,6 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con
if (data == nullptr) {
return "No dex in the jar";
}
} else if (method == FF_ZIP_METHOD_STORED) {
dataSize = uncompressedSize;
} else if (method == FF_ZIP_METHOD_DEFLATED) {
#ifdef FF_HAVE_ZLIB
const char* error = inflateDex(data, dataSize, uncompressedSize, &mapping.inflated);
@@ -348,9 +431,14 @@ const char* ffDexStaticInt(const char* jarPath, const char* classDescriptor, con
#else
return "The jar deflates classes.dex and fastfetch was built without zlib";
#endif
} else {
} else if (method != FF_ZIP_METHOD_STORED) {
return "classes.dex uses an unsupported compression method";
}
// A STORED entry needs no further work: `dataSize` already holds the `compressedSize` that
// `findDexEntry` checked against the mapping, and for a STORED entry the payload is the file
// itself. The header's `uncompressedSize` is deliberately not used for it -- nothing bounds that
// value, so a corrupt one reaches past the end of the mapping, which is exactly what the dex
// header would then be validated against.
mapping.data = data;
mapping.size = dataSize;
+65
View File
@@ -0,0 +1,65 @@
#include "common/android/package.h"
#include "common/android/api.h"
#include "common/debug.h"
#include "common/strutil.h"
#include <string.h>
#include <unistd.h>
// There is no way for a process to ask for its own package name: it is not in /proc/self/status, and
// an app cannot list /data/data. The executable path carries it instead -- an app's binaries live
// under /data/data/<package>/ or /data/user/<user>/<package>/, and /proc/self/exe resolves there.
// Under Termux that is /data/data/com.termux/files/usr/bin/<binary>, whose third component is the
// package.
//
// The memory cgroup carries the same information (4:memory:/apps/com.termux on the device this was
// verified on), but the unified hierarchy spells it out as /apps/uid_10478/pid_26943 with no name
// left in it, so the executable is the portable half of the two.
bool ffAndroidGetOwnPackage(char* buffer, size_t capacity) {
char path[4096];
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (length <= 0) {
FF_DEBUG("Cannot read /proc/self/exe, so the calling package cannot be derived");
return false;
}
path[length] = '\0';
const char* rest = nullptr;
const char* const dataPrefix = "/data/data/";
const char* const userPrefix = "/data/user/";
if (ffStrStartsWith(path, dataPrefix)) {
rest = path + strlen(dataPrefix);
} else if (ffStrStartsWith(path, userPrefix)) {
// /data/user/<user>/<package>/... -- the user number has to be stepped over before the
// package name starts, and only strchr() answers where that is.
rest = strchr(path + strlen(userPrefix), '/');
if (rest != nullptr) {
rest += 1;
}
}
if (rest == nullptr) {
const uint32_t uid = instance.state.platform.uid;
FF_DEBUG("The executable is not in an app data directory (\"%s\"), uid %u", path, uid);
if (uid != FF_ANDROID_PRIVILEGED_UID_SHELL) {
return false;
}
static const char shellPackage[] = FF_ANDROID_SHELL_PACKAGE;
if (sizeof(shellPackage) > capacity) {
return false;
}
memcpy(buffer, shellPackage, sizeof(shellPackage));
FF_DEBUG("The calling package is \"%s\" (the shell owns it)", buffer);
return true;
}
const char* end = strchr(rest, '/');
const size_t nameLength = end != nullptr ? (size_t) (end - rest) : strlen(rest);
if (nameLength == 0 || nameLength >= capacity) {
return false;
}
memcpy(buffer, rest, nameLength);
buffer[nameLength] = '\0';
FF_DEBUG("The calling package is \"%s\"", buffer);
return true;
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
// The one package name the shell UID owns. android.os.Process.SHELL_UID is a single app UID that
// owns exactly one package, so a process that cannot derive a package of its own -- a static build
// pushed to /data/local/tmp is the case that showed up -- can answer with this name and have the
// system services accept it. See FF_ANDROID_PRIVILEGED_UID_SHELL in common/android/api.h.
#define FF_ANDROID_SHELL_PACKAGE "com.android.shell"
// Some of the system services take the caller's package name as an argument and check it against the
// calling UID rather than against the name being real: a name nobody owns, and a real name belonging
// to another app, are refused exactly like a null one. So it has to be the caller's own name.
//
// Whether the name is checked at all depends on the service and the release -- `getConnectionInfo`
// answered "Package com.termux does not belong to <shell uid>" on one Android 16 device while the
// same name was accepted on an Android 11 one, and `getWallpaper` refuses a null, empty or foreign
// name on that Android 16 device -- so passing the real one is what works everywhere.
//
// Writes the name into `buffer`, terminator included, and returns whether one was found. `capacity`
// is the caller's, because the parcel the name ends up in is sized from it.
[[gnu::nonnull(1), nodiscard]] bool ffAndroidGetOwnPackage(char* buffer, size_t capacity);
+53
View File
@@ -0,0 +1,53 @@
#include "common/FFcache.h"
// Entries are file scope statics owned by the cache owner, so they can be linked into a list
// without any allocation. The list holds every entry that has been used at least once: an entry
// that was never used has nothing to drop.
static FFcacheEntry* entries = nullptr;
static void registerEntry(FFcacheEntry* entry) {
if (entry->registered) {
return;
}
entry->registered = true;
entry->next = entries;
entries = entry;
}
void* ffCacheGet(FFcacheEntry* entry) {
if (ffCacheBeginInit(entry) && entry->init) {
entry->init(entry->storage);
}
return entry->storage;
}
bool ffCacheBeginInit(FFcacheEntry* entry) {
registerEntry(entry);
if (entry->initialized) {
return false;
}
entry->initialized = true;
return true;
}
void ffCacheInvalidate(FFcacheEntry* entry) {
if (!entry->initialized) {
return;
}
entry->initialized = false;
if (entry->destroy) {
entry->destroy(entry->storage);
}
}
void ffCacheInvalidateAll(void) {
for (FFcacheEntry* entry = entries; entry; entry = entry->next) {
ffCacheInvalidate(entry);
}
}
+19 -1
View File
@@ -85,11 +85,29 @@ void ffPrepareCommandOption(FFdata* data) {
#define FF_IF_MODULE_MATCH(moduleNameConstant) if (ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
switch (moduleType[0]) {
#if !FF_MODULE_DISABLE_CPUUSAGE
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
case 'C':
case 'c':
#endif
#if !FF_MODULE_DISABLE_CPUUSAGE
FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name)
ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved
#endif
#if !FF_MODULE_DISABLE_COMMAND
FF_IF_MODULE_MATCH(ffCommandModuleInfo.name) {
// `command` runs its shell commands in parallel: `ffPrepareCommand` spawns them here,
// in the prepare pass, and `ffDetectCommand` collects them in the print pass. The
// command line itself can only be read from the JSON config, and this code path does
// not consult it, so nothing would ever be queued and the module would report an
// internal error marker. Refuse it up front instead of letting it get that far.
fputs("Error: module `command` is not supported with `--structure`\n", stderr);
fputs(" Its options can only be read from the JSON config, which `--structure` bypasses.\n", stderr);
fputs(" Add it to the config instead, e.g. `{ \"modules\": [ { \"type\": \"command\", \"text\": \"uname -r\" } ] }`\n", stderr);
exit(481);
}
#endif
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
break;
#endif
+85 -30
View File
@@ -147,11 +147,26 @@ static inline bool formatArgSet(const FFformatarg* arg) {
static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) {
const char* err = lua_tolstring(L, -1, nullptr);
if (err) {
const char* tmp = strchr(err, ':');
if (tmp) {
err = tmp + 1;
while (*err == ' ') {
++err;
// Drop the position Lua puts in front of an error and keep the message only. Both places
// that build one -- `luaG_addinfo` and `luaL_where` -- format it as `%s:%d: %s`, so a
// position is a colon, a line number, a colon and a space:
// general.preload.lua:1: <name> expected near '=' -> <name> expected near '='
// [string ""]:1: x -> x
// Not every error has one, and cutting at the first colon mangled those. `luaL_loadfilex`
// reports `cannot open <path>: <strerror>`, where that colon is the one after the path, so
// the path -- the only part naming the problem -- was dropped and `No such file or
// directory` was left on its own. A colon followed by a line number does not identify a
// position either, because a path can contain one: `cannot open /tmp/x:12:y/nope.lua: ...`
// has to survive whole. The trailing space is what separates the two, and a real position
// always has it.
for (const char* colon = strchr(err, ':'); colon; colon = strchr(colon + 1, ':')) {
const char* line = colon + 1;
while (*line >= '0' && *line <= '9') {
++line;
}
if (line > colon + 1 && line[0] == ':' && line[1] == ' ') {
err = line + 2;
break;
}
}
}
@@ -205,7 +220,11 @@ static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t script
lua_pushboolean(L, *(bool*) arg->value);
break;
case FF_ARG_TYPE_STRING:
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
if (!arg->value) {
lua_pushlstring(L, "", 0);
} else {
lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
}
break;
case FF_ARG_TYPE_STRBUF: {
const FFstrbuf* sb = (const FFstrbuf*) arg->value;
@@ -438,31 +457,67 @@ static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t sc
}
#endif
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) {
// skip ANSI escape codes at the start of the string for truncation
const char* p = in->chars + 2;
while (*p && (ffCharIsDigit(*p) || *p == ';')) {
++p;
}
if (*p && isascii(*p)) {
++p;
}
uint32_t prefixLen = (uint32_t) (p - in->chars);
ffStrbufAppendNS(out, prefixLen, in->chars);
ffStrbufSubstrAfter(in, prefixLen - 1);
if (trailingEscape) {
// likely have a `CSI m` reset at the end of the string
uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e');
if (iLastEscape != in->length) {
ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape);
ffStrbufSubstrBefore(in, iLastEscape);
}
}
return true;
// Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none.
// A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
static uint32_t ansiEscapeLength(const char* str) {
if (str[0] != '\e' || str[1] != '[') {
return 0;
}
return false;
const char* p = str + 2;
while (*p >= '0' && *p <= '?') {
++p;
}
while (*p >= ' ' && *p <= '/') {
++p;
}
if (*p < '@' || *p > '~') {
return 0; // not a complete CSI sequence
}
return (uint32_t) (p - str) + 1;
}
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
// Skip all ANSI escape codes at the start of the string, so that they don't count
// towards the truncation / padding length. A value may carry more than one of them,
// e.g. `\e[31m\e[1m`.
uint32_t prefixLen = 0;
while (in->length - prefixLen >= 2) {
uint32_t len = ansiEscapeLength(in->chars + prefixLen);
if (len == 0) {
break;
}
prefixLen += len;
}
if (prefixLen == 0) {
return false;
}
ffStrbufAppendNS(out, prefixLen, in->chars);
ffStrbufSubstrAfter(in, prefixLen - 1);
if (trailingEscape) {
// likely have one or more `CSI m` resets at the end of the string
uint32_t end = in->length;
while (end >= 3) {
uint32_t start = end - 1;
while (start > 0 && in->chars[start] != '\e') {
--start;
}
uint32_t len = ansiEscapeLength(in->chars + start);
if (len == 0 || start + len != end) {
break;
}
end = start;
}
if (end < in->length) {
ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end);
ffStrbufSubstrBefore(in, end);
}
}
return true;
}
static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
+97 -18
View File
@@ -26,15 +26,16 @@
// 0: title
// 1: blank
// 2: logo type row
// 3: output mode row
// 4: blank
// 5: modules title row
// 6: blank
// 7..7+listRows-1: module grid
// 7+listRows: description row
// 8+listRows: help line 1
// 9+listRows: help line 2
#define FF_GEN_CONFIG_LIST_TOP 7
// 3: logo position row
// 4: output mode row
// 5: blank
// 6: modules title row
// 7: blank
// 8..8+listRows-1: module grid
// 8+listRows: description row
// 9+listRows: help line 1
// 10+listRows: help line 2
#define FF_GEN_CONFIG_LIST_TOP 8
#define FF_GEN_CONFIG_BOTTOM_CHROME 3
// Poll wait timeout (ms) while idle. A longer wait avoids frequent full-screen
@@ -66,6 +67,7 @@ typedef struct FFGenConfigUI {
uint32_t cursor;
uint32_t viewOffset;
FFLogoType logoType;
FFLogoPosition logoPosition;
bool fullConfig;
bool confirmingOverwrite;
uint16_t rows;
@@ -165,7 +167,7 @@ static void enterRawMode(void) {
static void getTerminalSize(uint16_t* rows, uint16_t* cols) {
FFTerminalSizeResult size;
if (ffDetectTerminalSize(&size) && size.rows > 0 && size.columns > 0) {
if (ffDetectTerminalSize(&size, true) && size.rows > 0 && size.columns > 0) {
*rows = size.rows;
*cols = size.columns;
} else {
@@ -248,8 +250,8 @@ static void removeAllBreaksSeparators(FFGenConfigUI* ui) {
static void computeLayout(FFGenConfigUI* ui) {
FFGenConfigLayout* layout = &ui->layout;
if (ui->rows < 10) {
ui->rows = 10;
if (ui->rows < 11) {
ui->rows = 11;
}
int32_t listRows = (int32_t) ui->rows - FF_GEN_CONFIG_LIST_TOP - FF_GEN_CONFIG_BOTTOM_CHROME;
layout->listRows = listRows < 0 ? 0 : (uint16_t) listRows;
@@ -367,6 +369,26 @@ static void cycleLogoType(FFGenConfigUI* ui, int delta) {
}
}
static void cycleLogoPosition(FFGenConfigUI* ui, int delta) {
static const FFLogoPosition positions[] = {
FF_LOGO_POSITION_AUTO,
FF_LOGO_POSITION_LEFT,
FF_LOGO_POSITION_RIGHT,
FF_LOGO_POSITION_TOP,
};
uint32_t index = 0;
for (uint32_t i = 0; i < ARRAY_SIZE(positions); ++i) {
if (positions[i] == ui->logoPosition) {
index = i;
break;
}
}
index = (index + (delta > 0 ? 1 : ARRAY_SIZE(positions) - 1)) % ARRAY_SIZE(positions);
ui->logoPosition = positions[index];
}
static uint32_t countSelectedModules(const FFlist* items) {
uint32_t count = 0;
FF_LIST_FOR_EACH (FFGenConfigItem, item, *items) {
@@ -558,7 +580,46 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 3: output mode
// Row 3: logo position
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD "m");
rowAppendVisual(&row, "Logo ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_UNDERLINE_ "m");
rowAppendVisual(&row, "p");
rowAppendRaw(&row, "\e[24m"); // Reset underline
rowAppendVisual(&row, "osition:");
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "auto", ui->logoPosition == FF_LOGO_POSITION_AUTO);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "left", ui->logoPosition == FF_LOGO_POSITION_LEFT);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "right", ui->logoPosition == FF_LOGO_POSITION_RIGHT);
rowAppendVisual(&row, " ");
drawLogoOption(&row, "top", ui->logoPosition == FF_LOGO_POSITION_TOP);
if (cols >= 120) {
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_SOFT);
switch (ui->logoPosition) {
case FF_LOGO_POSITION_AUTO:
rowAppendVisual(&row, " - Choose left or top from terminal width");
break;
case FF_LOGO_POSITION_RIGHT:
rowAppendVisual(&row, " - Place the logo to the right");
break;
case FF_LOGO_POSITION_TOP:
rowAppendVisual(&row, " - Place the logo above module output");
break;
case FF_LOGO_POSITION_LEFT:
default:
rowAppendVisual(&row, " - Place the logo to the left");
break;
}
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 4: output mode
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_BOLD FF_COLOR_MODE_UNDERLINE_ "m");
@@ -581,11 +642,11 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
}
finishRow(&row, out, ++rowCount == ui->rows);
// Row 4: blank
// Row 5: blank
rowInit(&row, cols);
finishRow(&row, out, ++rowCount == ui->rows);
// Row 5: modules title
// Row 6: modules title
rowInit(&row, cols);
rowAppendVisual(&row, " ");
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_BOLT);
@@ -596,7 +657,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
finishRow(&row, out, ++rowCount == ui->rows);
// Row 6: blank
// Row 7: blank
rowInit(&row, cols);
finishRow(&row, out, ++rowCount == ui->rows);
@@ -618,7 +679,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
finishRow(&row, out, ++rowCount == ui->rows);
}
// Row after grid: description
// Row 9+listRows: description
rowInit(&row, cols);
if (ui->cursor < ui->items.length) {
const FFGenConfigItem* item = FF_LIST_GET(FFGenConfigItem, ui->items, ui->cursor);
@@ -650,7 +711,7 @@ static void renderFrame(FFGenConfigUI* ui, FFstrbuf* out) {
rowInit(&row, cols);
rowAppendRaw(&row, "\e[" FF_COLOR_MODE_DIM_ "m");
rowAppendVisual(&row, " l/L logo o minimal/full s/Enter save q/Esc quit g/G top/bottom");
rowAppendVisual(&row, " l/L logo p/P position o minimal/full s/Enter save q/Esc quit g/G top/bottom");
rowAppendRaw(&row, FASTFETCH_TEXT_MODIFIER_RESET);
finishRow(&row, out, ++rowCount == ui->rows);
@@ -904,6 +965,10 @@ static int handleKey(FFGenConfigUI* ui, FFGenConfigKey key, char ch, bool fileEx
cycleLogoType(ui, 1);
} else if (ch == 'L') {
cycleLogoType(ui, -1);
} else if (ch == 'p') {
cycleLogoPosition(ui, 1);
} else if (ch == 'P') {
cycleLogoPosition(ui, -1);
} else if (ch == '\x03' || ch == '\x04' || ch == '\x1a' || ch == '\x1c') {
return 0;
}
@@ -940,6 +1005,18 @@ static FFLogoType initialLogoType(void) {
}
}
static FFLogoPosition initialLogoPosition(void) {
switch (instance.config.logo.position) {
case FF_LOGO_POSITION_TOP:
case FF_LOGO_POSITION_RIGHT:
case FF_LOGO_POSITION_AUTO:
return instance.config.logo.position;
case FF_LOGO_POSITION_LEFT:
default:
return FF_LOGO_POSITION_LEFT;
}
}
static bool applyConfig(FFdata* data, const FFlist* items, bool fullConfig) {
if (data->resultDoc) {
fputs("Error: duplicated `--gen-config` or `--format json` flags found\n", stderr);
@@ -972,6 +1049,7 @@ bool ffGenConfigInteractive(FFdata* data) {
.cursor = 0,
.viewOffset = 0,
.logoType = initialLogoType(),
.logoPosition = initialLogoPosition(),
.fullConfig = false,
.confirmingOverwrite = false,
.rows = 24,
@@ -993,6 +1071,7 @@ bool ffGenConfigInteractive(FFdata* data) {
if (result == 1) {
if (applyConfig(data, &ui.items, ui.fullConfig)) {
instance.config.logo.type = ui.logoType;
instance.config.logo.position = ui.logoPosition;
success = true;
}
} else {
+4
View File
@@ -329,6 +329,10 @@ FFNativeFD ffGetNullFD(void) {
return hNullFile;
}
bool ffIsTerminal(int fd) {
return isatty(fd) != 0;
}
bool ffRemoveFile(const char* fileName) {
return unlink(fileName) == 0;
}
+46 -9
View File
@@ -8,6 +8,20 @@
#include <windows.h>
// TODO: long paths on Windows.
//
// Every path that crosses into the wide API from this file is converted into a fixed
// `wchar_t[MAX_PATH]` buffer, and `RtlUTF8ToUnicodeN` refuses to fill it for a path of MAX_PATH
// characters or more (STATUS_BUFFER_TOO_SMALL) -- which makes each of those call sites a silent
// failure above that length rather than an error anyone sees. Raising a buffer on its own does not
// help: the wide APIs accept a longer path only either with a `\\?\` prefix on every absolute path
// (which also switches off the normalization the rest of the code relies on) or with a
// `longPathAware` manifest plus the LongPathsEnabled registry value.
//
// The plan is the second route: define MAX_PATH as 32768 program-wide, add the manifest flag, and
// size these buffers from the UTF-8 input rather than from the constant. It has to be done in one
// pass, which is why it is recorded here instead of being applied at the call sites the review
// happened to look at.
static bool createSubfolders(wchar_t* fileName) {
HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle;
bool closeRoot = false;
@@ -348,8 +362,18 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
}
ffStrbufAppendC(folder, '*');
WIN32_FIND_DATAA entry;
HANDLE hFind = FindFirstFileA(folder->chars, &entry);
// The wide API on purpose: every path fastfetch holds is UTF-8, and FindFirstFileA would read
// it in the active code page, so a directory whose name the code page cannot represent -- a
// Chinese or Japanese name under an English or German locale, for one -- answers
// ERROR_PATH_NOT_FOUND and the whole listing disappears. The names it returns are converted
// back to UTF-8 below.
wchar_t wideFolder[PATH_MAX];
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wideFolder, sizeof(wideFolder), nullptr, folder->chars, folder->length + 1))) {
ffStrbufTrimRight(folder, '*');
return;
}
WIN32_FIND_DATAW entry;
HANDLE hFind = FindFirstFileW(wideFolder, &entry);
ffStrbufTrimRight(folder, '*');
if (hFind == INVALID_HANDLE_VALUE) {
return;
@@ -357,14 +381,15 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
do {
if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (ffStrEquals(entry.cFileName, ".") || ffStrEquals(entry.cFileName, "..")) {
if (entry.cFileName[0] == L'.') {
continue;
}
ffStrbufSubstrBefore(folder, folderLength);
ffStrbufAppendS(folder, entry.cFileName);
ffStrbufAppendWS(folder, entry.cFileName);
ffStrbufAppendC(folder, '/');
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry.cFileName, pretty);
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), name.chars, pretty);
ffStrbufSubstrBefore(folder, folderLength);
continue;
}
@@ -377,8 +402,9 @@ void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indenta
fputs(folder->chars + baseLength, stdout);
}
puts(entry.cFileName);
} while (FindNextFileA(hFind, &entry));
FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(entry.cFileName);
ffStrbufPutTo(&name, stdout);
} while (FindNextFileW(hFind, &entry));
FindClose(hFind);
}
@@ -500,8 +526,19 @@ FFNativeFD ffGetNullFD(void) {
return hNullFile;
}
bool ffIsTerminal(int fd) {
intptr_t handle = _get_osfhandle(fd);
if (handle == -1 || handle == -2) {
// -1: an invalid fd; -2: a valid fd with no file behind it (`_get_osfhandle`'s way of
// saying FF_INVALID_FD). Neither is a terminal.
return false;
}
DWORD mode;
return GetConsoleMode((HANDLE) handle, &mode) != 0;
}
bool ffRemoveFile(const char* fileName) {
wchar_t fileNameW[MAX_PATH];
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
ULONG len;
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
return false;
@@ -513,7 +550,7 @@ bool ffRemoveFile(const char* fileName) {
}
uint64_t ffPathGetMtime(const char* path) {
wchar_t fileNameW[MAX_PATH];
wchar_t fileNameW[MAX_PATH]; // see the long path TODO at the top of this file
ULONG len;
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, path, (ULONG) strlen(path) + 1))) {
return 0;
+53 -1
View File
@@ -60,6 +60,22 @@ void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* m
}
}
bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max) {
if (!yyjson_is_int(val)) {
return false;
}
// `yyjson_get_sint()` is exact for every value that fits into `int64_t`, and is negative for the
// `uint64_t` values above `INT64_MAX`, which no caller accepts anyway.
int64_t num = unsafe_yyjson_get_sint(val);
if (num < 0 || (uint64_t) num > max) {
return false;
}
*result = (uint32_t) num;
return true;
}
const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
if (yyjson_is_int(val)) {
int intVal = yyjson_get_int(val);
@@ -133,7 +149,10 @@ static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_
static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
switch (type[0]) {
#if !FF_MODULE_DISABLE_CPUUSAGE
// Both `cpuusage` and `command` start with a c, and they are disabled by separate options,
// so the block has to survive either one being off -- otherwise a build with only the other
// one enabled would never prepare this module (see the same guard in `commandoption.c`).
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
case 'c':
case 'C': {
if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
@@ -276,6 +295,7 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
}
yyjson_val* module = item;
bool gatedOnSucceeded = false;
const char* type = yyjson_get_str(module);
if (type) {
module = nullptr;
@@ -311,6 +331,38 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
if (!unsafe_yyjson_is_bool(previousSucceeded)) {
return "Property 'succeeded' in 'condition' must be a boolean";
}
gatedOnSucceeded = true;
if (prepare) {
// Whether this module is printed at all depends on the result of the previous
// one, which is only known in the print pass. Preparing it here would leave a
// result behind that no print pass ever consumes, and since the modules are
// paired by position (see `ffPrepareCommand` / `ffDetectCommand`), that
// would silently shift every later module onto the wrong result.
// Skip it, and let the module report the missing preparation if it turns out
// to be printed after all.
continue;
}
#if !FF_MODULE_DISABLE_COMMAND
// A `command` module that runs in parallel is exactly the pairing above, so this
// combination cannot be left to the print pass: the module would be handed the
// result prepared for the next `command` module and print that instead of its
// own output. Refused here rather than in the prepare pass, because returning
// from there would leave every later module unprepared -- which the print pass
// would then report as a failure of theirs.
// `parallel` is on by default, so leaving the key out is the parallel case too.
if (gatedOnSucceeded && ffStrEqualsIgnCase(yyjson_get_str(yyjson_obj_get(module, "type")), ffCommandModuleInfo.name)) {
yyjson_val* parallel = yyjson_obj_get(module, "parallel");
if (parallel == nullptr || yyjson_get_bool(parallel)) {
return "Module \"command\" cannot be combined with \"condition.succeeded\" while it runs in "
"parallel: whether it is printed is only known in the print pass, so it cannot be "
"prepared, and the prepared results are matched to modules by position. Set "
"\"parallel\": false on it, or drop the condition.";
}
}
#endif
if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
continue;
}
+39 -14
View File
@@ -1,22 +1,47 @@
#include "common/netif.h"
#include "common/FFcache.h"
#ifndef _WIN32
#include <net/if.h>
#include <netinet/in.h>
#endif
#include <string.h>
// netio, localip and dns resolve the default route on every round, and it can change while a
// `--dynamic-interval` run is going on: a laptop switching from Wi-Fi to Ethernet, a VPN coming up.
static FFNetifDefaultRouteResult defaultRouteV4;
static FFNetifDefaultRouteResult defaultRouteV6;
static void resetDefaultRoute(void* storage) {
// The impls only write the fields they found, so without this a round that finds no default
// route would keep reporting the interface of the previous one.
memset(storage, 0, sizeof(FFNetifDefaultRouteResult));
}
static void initDefaultRouteV4(void* storage) {
FFNetifDefaultRouteResult* result = storage;
result->status = ffNetifGetDefaultRouteImplV4(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
}
static void initDefaultRouteV6(void* storage) {
FFNetifDefaultRouteResult* result = storage;
result->status = ffNetifGetDefaultRouteImplV6(result) ? FF_NETIF_OK : FF_NETIF_INVALID;
}
static FFcacheEntry ffCacheEntryDefaultRouteV4 = {
.name = "default route V4",
.storage = &defaultRouteV4,
.init = initDefaultRouteV4,
.destroy = resetDefaultRoute,
};
static FFcacheEntry ffCacheEntryDefaultRouteV6 = {
.name = "default route V6",
.storage = &defaultRouteV6,
.init = initDefaultRouteV6,
.destroy = resetDefaultRoute,
};
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) {
static FFNetifDefaultRouteResult result;
if (result.status == FF_NETIF_UNINITIALIZED) {
result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
}
return &result;
return ffCacheGet(&ffCacheEntryDefaultRouteV4);
}
const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) {
static FFNetifDefaultRouteResult result;
if (result.status == FF_NETIF_UNINITIALIZED) {
result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
}
return &result;
return ffCacheGet(&ffCacheEntryDefaultRouteV6);
}
+17 -6
View File
@@ -325,7 +325,12 @@ int ffNetworkingChunkedComplete(const char* body, uint32_t bodyLen, uint32_t* co
// Keeps the status line and every header except `dropHeader` and `Content-Length`,
// then emits a `Content-Length` matching the (already decoded) body.
// `body` may point into `buffer->chars`; it is copied before `buffer` is released.
static void rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* body, uint32_t bodyLen, const char* dropHeader) {
//
// Returns the offset of the `\r` that opens the terminating CRLF CRLF of the new response. The
// rebuild drops a header line and rewrites another, so the header it produces is shorter than the
// one that went in: the offset the caller held before is no longer the one that describes this
// buffer, and handing it on would point into the middle of the body.
static uint32_t rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* body, uint32_t bodyLen, const char* dropHeader) {
FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerEnd + bodyLen + 64);
uint32_t pos = 0;
@@ -349,18 +354,24 @@ static void rebuildResponse(FFstrbuf* buffer, uint32_t headerEnd, const char* bo
ffStrbufDestroy(buffer);
ffStrbufInitMove(buffer, &newBuffer);
// The terminating CRLF CRLF is the four bytes in front of the body
return buffer->length - bodyLen - 4;
}
bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd) {
bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd) {
assert(buffer->allocated > 0);
// Held in a local because the rebuild below hands a different value back
const uint32_t headerLength = *headerEnd;
// The header block is terminated by CR LF CR LF
if (headerEnd + 4 > buffer->length) {
if (headerLength + 4 > buffer->length) {
return false;
}
char* body = buffer->chars + headerEnd + 4;
uint32_t bodyLen = buffer->length - headerEnd - 4;
char* body = buffer->chars + headerLength + 4;
uint32_t bodyLen = buffer->length - headerLength - 4;
uint32_t consumed = 0;
if (ffNetworkingChunkedComplete(body, bodyLen, &consumed) != 1) {
@@ -391,6 +402,6 @@ bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd) {
}
FF_DEBUG("Decoded chunked body: %u bytes encoded, %u bytes decoded", bodyLen, outLen);
rebuildResponse(buffer, headerEnd, out, outLen, "Transfer-Encoding:");
*headerEnd = rebuildResponse(buffer, headerLength, out, outLen, "Transfer-Encoding:");
return true;
}
+11 -3
View File
@@ -322,6 +322,8 @@ error:
close(state->sockfd);
state->sockfd = -1;
}
ffStrbufClear(&state->command);
return ret;
}
@@ -464,7 +466,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "Response too large";
}
FF_DEBUG("Receive buffer is full, extending it");
ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated);
// Asking for exactly the room that is left under the cap, rather than for as much as
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
// from a size that is not one of those lands past the cap the check above just cleared.
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
}
// When the remaining length is known, ask for exactly that much. MSG_WAITALL then
@@ -587,7 +592,7 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "No HTTP header end found";
}
if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) {
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
return "Failed to decode chunked response";
}
@@ -597,7 +602,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
}
FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length);
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
// says not to send both; when one does, the chunked framing is the one that was acted on.
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
return "Content length mismatch";
}
+9 -3
View File
@@ -298,7 +298,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "Response too large";
}
FF_DEBUG("Receive buffer is full, extending it");
ffStrbufEnsureFreeNoCheck(buffer, buffer->allocated);
// Asking for exactly the room that is left under the cap, rather than for as much as
// the buffer holds again: the allocation is rounded up to a power of two, so a doubling
// from a size that is not one of those lands past the cap the check above just cleared.
ffStrbufEnsureFreeNoCheck(buffer, FF_NETWORKING_MAX_RESPONSE_SIZE - buffer->length - 1);
}
FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
@@ -413,7 +416,7 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
return "No HTTP header end found";
}
if (chunked && !ffNetworkingDecodeChunked(buffer, headerEnd)) {
if (chunked && !ffNetworkingDecodeChunked(buffer, &headerEnd)) {
return "Failed to decode chunked response";
}
@@ -425,7 +428,10 @@ const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buf
contentLength,
buffer->length);
if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
// A chunked response was framed by its last-chunk and has been rewritten around it, so any
// `Content-Length` the server also sent describes a body that no longer exists. RFC 9112 6.3
// says not to send both; when one does, the chunked framing is the one that was acted on.
if (!chunked && contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
return "Content length mismatch";
}
+4 -4
View File
@@ -3,7 +3,7 @@
#include "common/textModifier.h"
#include "logo/logo.h"
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
ffLogoPrintLine();
// This is used by --set-keyless, in this case we want neither the module name nor the separator
@@ -40,7 +40,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
fputs(moduleName, stdout);
if (moduleIndex > 0) {
printf(" %hhu", moduleIndex);
printf(" %u", moduleIndex);
}
} else {
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
@@ -82,7 +82,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
}
}
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
bool success;
if (__builtin_expect(moduleArgs != nullptr, 1)) {
@@ -102,7 +102,7 @@ bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleAr
return success;
}
void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
if (!instance.config.display.showErrors) {
return;
}
+18 -5
View File
@@ -132,12 +132,18 @@ static inline ssize_t ffReadFDData(FFNativeFD fd, size_t dataSize, void* data) {
return ffReadFDData(fd, dataSize, data);
}
// `CreateFileA` reads the path in the ANSI code page, so a UTF-8 path holding anything outside ASCII
// names a different file, or none at all. The wide API is the one that takes what UTF-8 decodes to,
// and the conversion is done the same way ffPathExists() below does it.
[[gnu::nonnull(1, 2)]] static inline bool ffAppendFileBuffer(const char* fileName, FFstrbuf* buffer) {
FF_AUTO_CLOSE_FD FFNativeFD fd =
#ifndef _WIN32
open(fileName, O_RDONLY | O_CLOEXEC);
#ifdef _WIN32
wchar_t fileNameW[MAX_PATH];
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), nullptr, fileName, (ULONG) strlen(fileName) + 1))) {
return false;
}
FF_AUTO_CLOSE_FD FFNativeFD fd = CreateFileW(fileNameW, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
#else
CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
FF_AUTO_CLOSE_FD FFNativeFD fd = open(fileName, O_RDONLY | O_CLOEXEC);
#endif
if (!ffIsValidNativeFD(fd)) {
@@ -228,7 +234,7 @@ typedef enum FFPathType: uint8_t {
[[gnu::nonnull(1, 2)]] bool ffPathExpandEnv(const char* in, FFstrbuf* out);
#define FF_IO_TERM_RESP_WAIT_MS 200 // #554
#define FF_IO_TERM_RESP_WAIT_MS 1000 // Terminal may respond slowly, especially when printing large image/gif logos. Found on iTerm
[[gnu::format(scanf, 3, 4), gnu::nonnull(1, 3)]] const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...);
@@ -286,6 +292,13 @@ static inline void wrapClosedir(HANDLE* pdir) {
}
FFNativeFD ffGetNullFD(void);
// Whether a descriptor is attached to a real terminal, i.e. whether a human is looking at it.
//
// Do not use `isatty()` for this on Windows. It is `_isatty()`, which answers "is this a character
// device" rather than "is this a console", and `NUL` -- as well as MSYS2's `/dev/null` -- is a
// character device. A redirected run therefore looks interactive, which is how
// `fastfetch --gen-config <path> > /dev/null` ends up on the interactive path and blocks forever.
bool ffIsTerminal(int fd);
// Returns whether the file was removed; callers that only want it gone discard that, so not `nodiscard`
[[gnu::nonnull(1)]] bool ffRemoveFile(const char* fileName);
// Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read.
+6
View File
@@ -8,6 +8,12 @@
// `val` may be null (yyjson predicates tolerate it); `result` and `pairs` may not.
// Returns an error string, null on success, so it is `nodiscard`.
[[gnu::nonnull(2, 3), nodiscard]] const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]);
// Reads an integer in `[0, max]` into `result`. Returns false for anything else.
//
// `yyjson_get_uint()` accepts the signed number subtype as well and reinterprets its bits, so the
// bare `(uint32_t) yyjson_get_uint(val)` it replaces turned `-1` into `UINT32_MAX` — 4.3 billion
// characters for `separator.times`, a ~50 day sleep for `waitTime`.
[[gnu::nonnull(1, 2), nodiscard]] bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max);
// The three helpers below are routinely called as statements, to add a key to a document, so their
// result is not `nodiscard`.
+2
View File
@@ -8,7 +8,9 @@ extern "C" {
// `memrchr` is a GNU extension and may not be declared by system headers even when the symbol exists.
// Declare it unconditionally; the build system provides a fallback implementation when missing.
#ifndef memrchr
void* memrchr(const void* s, int c, size_t n);
#endif
#ifdef __cplusplus
}
+13 -1
View File
@@ -34,6 +34,15 @@ typedef struct FFNetworkingState {
[[gnu::nonnull(1, 2, 4), nodiscard]] const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, uint16_t port, const char* path, const char* headers);
[[gnu::nonnull(1, 2), nodiscard]] const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer);
// Releases what a finished request holds and returns `state` to the idle state, so that the next
// `ffNetworkingSendHttpRequest` starts from scratch. A request that never reached the wire can
// still own the request buffer, so a plain zeroing of the struct is not enough.
// The caller stays responsible for its own bookkeeping, such as clearing a "request sent" latch.
[[gnu::nonnull(1)]] static inline void ffNetworkingResetState(FFNetworkingState* state) {
ffStrbufDestroy(&state->command);
*state = (FFNetworkingState) {};
}
// Case-insensitive header lookup restricted to the header block [0, headerEnd).
// Restricting the range matters because the body may already share the same buffer.
// Returns a pointer to the first character of the value; `valueLen` receives its
@@ -51,7 +60,10 @@ typedef struct FFNetworkingState {
// Decodes a `Transfer-Encoding: chunked` body in place and rewrites the response with a
// `Content-Length` header in place of `Transfer-Encoding`.
[[gnu::nonnull(1), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t headerEnd);
// `headerEnd` is updated in place: the rewrite changes the header length, so the value the caller
// passed in no longer describes the buffer afterwards. Callers must use the new one for anything
// that addresses the body, such as ffNetworkingDecompressGzip().
[[gnu::nonnull(1, 2), nodiscard]] bool ffNetworkingDecodeChunked(FFstrbuf* buffer, uint32_t* headerEnd);
// Result of parsing a `Transfer-Encoding` header value
typedef enum FFNetworkingTransferEncoding {
+8
View File
@@ -20,6 +20,14 @@ typedef struct FFPercentageModuleConfig {
FFPercentageTypeFlags type;
} FFPercentageModuleConfig;
// Maps a received signal strength in dBm onto the 0-100 scale the percentage formatters want.
// -50 dBm and above counts as a perfect link, -100 dBm and below as no link at all, and the range in
// between is linear. Both ends are clamped rather than extrapolated, so the result is always a
// percentage -- which also keeps it usable with `{?x}` guards, unlike a raw negative RSSI.
[[gnu::const]] static inline double ffRssiToSignalQuality(int32_t rssi) {
return (double) (rssi >= -50 ? 100 : rssi <= -100 ? 0 : (rssi + 100) * 2);
}
// if (green <= yellow)
// [0, green]: print green
// (green, yellow]: print yellow
+3 -3
View File
@@ -14,14 +14,14 @@ typedef enum FFPrintType: uint8_t {
// Both `moduleName` and `moduleArgs` are optional and explicitly handled: a null `moduleName` is what
// `--set-keyless` relies on, and a null `moduleArgs` (used by ffPrintError callers) skips the custom
// key entirely. So neither takes `nonnull`.
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
// Same for `moduleArgs`; `numArgs` / `arguments` are only read when `moduleArgs` is non-null, so
// `arguments` is not `nonnull(6)` either. At least one caller discards the result, so not `nodiscard`.
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
#define FF_PRINT_FORMAT_CHECKED(moduleName, moduleIndex, moduleArgs, printType, arguments) \
ffPrintFormat((moduleName), (moduleIndex), (moduleArgs), (printType), (sizeof(arguments) / sizeof(*arguments)), (arguments));
// `moduleName` / `moduleArgs` are forwarded to ffPrintLogoAndKey, which accepts null for both;
// only `message` is dereferenced here (by `vprintf`).
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
[[gnu::nonnull(1)]] void ffPrintColor(const FFstrbuf* colorValue);
void ffPrintCharTimes(char c, uint32_t times);
+284
View File
@@ -0,0 +1,284 @@
#include "winrt.hpp"
#if FF_HAVE_WINRT
#include <wchar.h>
#if defined(__GNUC__) || defined(__clang__)
extern "C" [[noreturn]] void __cxa_pure_virtual(void) {
__builtin_trap();
}
#endif
// C++/WinRT supplies the ABI vtable shape, including IUnknown and IInspectable. These concrete
// classes only provide the implementation-specific state and methods; placement new keeps their
// storage under the C allocator used by the rest of fastfetch.
namespace {
using WinrtIterableAbi = abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>;
using WinrtIteratorAbi = abi_t<winrt::Windows::Foundation::Collections::IIterator<winrt::hstring>>;
using WinrtHstringAbiOut = winrt::impl::arg_out<winrt::hstring>;
struct FFWinrtHstringIterable;
struct FFWinrtHstringIterator;
struct FFWinrtHstringIterable final : WinrtIterableAbi {
std::uint32_t refCount;
FFlist strings; // HSTRING
std::int32_t STDMETHODCALLTYPE QueryInterface(winrt::guid const& riid, void** result) noexcept override;
std::uint32_t STDMETHODCALLTYPE AddRef() noexcept override;
std::uint32_t STDMETHODCALLTYPE Release() noexcept override;
std::int32_t STDMETHODCALLTYPE GetIids(std::uint32_t* count, winrt::guid** iids) noexcept override;
std::int32_t STDMETHODCALLTYPE GetRuntimeClassName(void** name) noexcept override;
std::int32_t STDMETHODCALLTYPE GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept override;
std::int32_t STDMETHODCALLTYPE First(void** result) noexcept override;
};
struct FFWinrtHstringIterator final : WinrtIteratorAbi {
std::uint32_t refCount;
FFWinrtHstringIterable* owner;
std::uint32_t index;
std::int32_t STDMETHODCALLTYPE QueryInterface(winrt::guid const& riid, void** result) noexcept override;
std::uint32_t STDMETHODCALLTYPE AddRef() noexcept override;
std::uint32_t STDMETHODCALLTYPE Release() noexcept override;
std::int32_t STDMETHODCALLTYPE GetIids(std::uint32_t* count, winrt::guid** iids) noexcept override;
std::int32_t STDMETHODCALLTYPE GetRuntimeClassName(void** name) noexcept override;
std::int32_t STDMETHODCALLTYPE GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept override;
std::int32_t STDMETHODCALLTYPE get_Current(WinrtHstringAbiOut value) noexcept override;
std::int32_t STDMETHODCALLTYPE get_HasCurrent(bool* value) noexcept override;
std::int32_t STDMETHODCALLTYPE MoveNext(bool* value) noexcept override;
std::int32_t STDMETHODCALLTYPE GetMany(std::uint32_t capacity, WinrtHstringAbiOut values, std::uint32_t* actual) noexcept override;
};
// ---------------------------------------------------------------------------------------------
// IIterable<HSTRING>
// ---------------------------------------------------------------------------------------------
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::QueryInterface(winrt::guid const& riid, void** result) noexcept {
if (!result) {
return E_POINTER;
}
*result = nullptr;
if (riid == winrt::guid_of<winrt::Windows::Foundation::IUnknown>() ||
riid == winrt::guid_of<winrt::Windows::Foundation::IInspectable>() ||
riid == winrt::guid_of<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>()) {
*result = this;
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterable::AddRef() noexcept {
return ++refCount;
}
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterable::Release() noexcept {
std::uint32_t remaining = --refCount;
if (remaining == 0) {
FF_LIST_FOR_EACH(HSTRING, string, strings) {
WindowsDeleteString(*string);
}
ffListDestroy(&strings);
this->~FFWinrtHstringIterable();
free(this);
}
return remaining;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetIids(std::uint32_t* count, winrt::guid** iids) noexcept {
*count = 0;
*iids = nullptr;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetRuntimeClassName(void** name) noexcept {
*name = nullptr;
return S_OK;
}
// `level` is the ABI's 32-bit TrustLevel enum; BaseTrust is 0.
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept {
*level = winrt::Windows::Foundation::TrustLevel::BaseTrust;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterable::First(void** result) noexcept {
if (!result) {
return E_POINTER;
}
*result = nullptr;
auto* iterator = (FFWinrtHstringIterator*) malloc(sizeof(FFWinrtHstringIterator));
if (!iterator) {
return E_OUTOFMEMORY;
}
::new (iterator) FFWinrtHstringIterator();
iterator->refCount = 1;
iterator->owner = this;
iterator->index = 0;
// The iterator outlives the call that created it, so it holds a reference of its own.
AddRef();
*result = iterator;
return S_OK;
}
// ---------------------------------------------------------------------------------------------
// IIterator<HSTRING>
// ---------------------------------------------------------------------------------------------
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::QueryInterface(winrt::guid const& riid, void** result) noexcept {
if (!result) {
return E_POINTER;
}
*result = nullptr;
if (riid == winrt::guid_of<winrt::Windows::Foundation::IUnknown>() ||
riid == winrt::guid_of<winrt::Windows::Foundation::IInspectable>() ||
riid == winrt::guid_of<winrt::Windows::Foundation::Collections::IIterator<winrt::hstring>>()) {
*result = this;
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterator::AddRef() noexcept {
return ++refCount;
}
std::uint32_t STDMETHODCALLTYPE FFWinrtHstringIterator::Release() noexcept {
std::uint32_t remaining = --refCount;
if (remaining == 0) {
owner->Release();
this->~FFWinrtHstringIterator();
free(this);
}
return remaining;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetIids(std::uint32_t* count, winrt::guid** iids) noexcept {
*count = 0;
*iids = nullptr;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetRuntimeClassName(void** name) noexcept {
*name = nullptr;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetTrustLevel(winrt::Windows::Foundation::TrustLevel* level) noexcept {
*level = winrt::Windows::Foundation::TrustLevel::BaseTrust;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::get_Current(WinrtHstringAbiOut value) noexcept {
if (!value) {
return E_POINTER;
}
*value = nullptr;
if (index >= owner->strings.length) {
return E_BOUNDS;
}
// The caller owns what it gets, so hand out a duplicate rather than the stored string.
return WindowsDuplicateString(*(HSTRING*) ffListGet(&owner->strings, sizeof(HSTRING), index), reinterpret_cast<HSTRING*>(value));
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::get_HasCurrent(bool* value) noexcept {
if (!value) {
return E_POINTER;
}
*value = index < owner->strings.length;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::MoveNext(bool* value) noexcept {
if (!value) {
return E_POINTER;
}
if (index < owner->strings.length) {
++index;
}
*value = index < owner->strings.length;
return S_OK;
}
std::int32_t STDMETHODCALLTYPE FFWinrtHstringIterator::GetMany(std::uint32_t capacity, WinrtHstringAbiOut values, std::uint32_t* actual) noexcept {
if (!actual) {
return E_POINTER;
}
*actual = 0;
if (capacity && !values) {
return E_POINTER;
}
std::uint32_t available = owner->strings.length - index;
std::uint32_t taken = capacity < available ? capacity : available;
HSTRING* hstrings = reinterpret_cast<HSTRING*>(values);
for (std::uint32_t i = 0; i < taken; ++i) {
HRESULT hr = WindowsDuplicateString(*(HSTRING*) ffListGet(&owner->strings, sizeof(HSTRING), index + i), &hstrings[i]);
if (FAILED(hr)) {
// Whatever was handed out already belongs to the caller, so a half filled array has to
// be taken back before the error goes out: the caller has no way to free a partial one.
for (std::uint32_t j = 0; j < i; ++j) {
WindowsDeleteString(hstrings[j]);
}
return hr;
}
}
index += taken;
*actual = taken;
return taken == capacity ? S_OK : S_FALSE;
}
} // namespace
HRESULT ffWinrtCreateHstringIterable(const wchar_t* const* items, uint32_t count, abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>** result) {
*result = nullptr;
if (count > 0 && !items) {
return E_INVALIDARG;
}
auto* iterable = (FFWinrtHstringIterable*) malloc(sizeof(FFWinrtHstringIterable));
if (!iterable) {
return E_OUTOFMEMORY;
}
::new (iterable) FFWinrtHstringIterable();
iterable->refCount = 1;
ffListInitA(&iterable->strings, sizeof(HSTRING), count > 0 ? count : FF_LIST_DEFAULT_ALLOC);
for (uint32_t i = 0; i < count; ++i) {
HSTRING* slot = (HSTRING*) ffListAdd(&iterable->strings, sizeof(HSTRING));
// ffListAdd does not zero the slot it hands out, and Release() walks every one of them, so
// the slot has to hold a usable HSTRING before WindowsCreateString gets a chance to fail.
*slot = nullptr;
HRESULT hr = WindowsCreateString(items[i], (UINT32) ::wcslen(items[i]), slot);
if (FAILED(hr)) {
iterable->Release();
return hr;
}
}
*result = static_cast<WinrtIterableAbi*>(iterable);
return S_OK;
}
#endif // FF_HAVE_WINRT
+132
View File
@@ -0,0 +1,132 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#include "common/FFlist.h"
#include "common/FFstrbuf.h"
#include "common/time.h"
#include "common/windows/com.h"
#include "common/windows/unicode.h"
}
#if FF_HAVE_WINRT
#include <roapi.h>
#include <winstring.h>
#include <asyncinfo.h>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
using winrt::impl::abi_t;
// Frees an HSTRING and clears the pointer, so it can be used with [[gnu::cleanup]].
static inline void ffDeleteHstring(HSTRING* value) {
if (*value) {
WindowsDeleteString(*value);
*value = nullptr;
}
}
// Copies an HSTRING into an FFstrbuf, transcoding from UTF-16 to UTF-8.
static inline void ffStrbufSetHstring(FFstrbuf* destination, HSTRING value) {
uint32_t length = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(value, &length);
ffStrbufSetNWS(destination, length, raw);
}
// Wraps `items` in an IVector<hstring> and hands out its IIterable<hstring> view, which is the
// parameter shape the FindAllAsync overloads taking additional properties expect. The caller owns
// the result.
HRESULT ffWinrtCreateHstringIterable(const wchar_t* const* items, uint32_t count, abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>** result);
// RoGetActivationFactory() for a WinRT runtime class.
//
// `Projection` must be the *projection* type -- e.g.
// winrt::Windows::Devices.Bluetooth::IBluetoothLEDeviceStatics -- and never abi_t<...>:
// winrt::impl::guid_v is only specialized for projection types, and abi_t<T> expands to
// impl::abi<T>::type, which is a non-deduced context.
template <typename Projection>
static inline HRESULT ffGetActivationFactory(const wchar_t* className, abi_t<Projection>** factory) {
HSTRING_HEADER header;
HSTRING runtimeClass;
HRESULT hr = WindowsCreateStringReference(className, (UINT32) ::wcslen(className), &header, &runtimeClass);
if (FAILED(hr)) {
return hr;
}
return RoGetActivationFactory(runtimeClass, winrt::guid_of<Projection>(), reinterpret_cast<void**>(factory));
}
template <typename TargetProjection, typename SourceAbi>
static inline HRESULT ffQueryInterface(SourceAbi* source, abi_t<TargetProjection>** target) {
return source->QueryInterface(winrt::guid_of<TargetProjection>(), reinterpret_cast<void**>(target));
}
// Spins until `operation` leaves AsyncStatus::Started, then hands its results to `result`.
template <typename TOperationAbi, typename TResultAbi>
static HRESULT ffWaitForAsyncOperation(TOperationAbi* operation, TResultAbi** result) {
FF_AUTO_RELEASE_COM_OBJECT IAsyncInfo* asyncInfo = nullptr;
HRESULT hr = ffQueryInterface<IAsyncInfo>(operation, &asyncInfo);
if (FAILED(hr)) {
return hr;
}
AsyncStatus status = AsyncStatus::Started;
for (;;) {
hr = asyncInfo->get_Status(&status);
if (FAILED(hr)) {
return hr;
}
if (status == AsyncStatus::Started) {
ffTimeSleep(0);
} else {
break;
}
}
if (status != AsyncStatus::Completed) {
HRESULT errorCode = E_FAIL;
asyncInfo->get_ErrorCode(&errorCode);
return FAILED(errorCode) ? errorCode : E_FAIL;
}
return operation->GetResults((void**) result);
}
template <typename TResultProjection, typename TOperation>
static HRESULT ffRunAndWait(TOperation&& operation, abi_t<TResultProjection>** result) {
*result = nullptr;
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IAsyncOperation<TResultProjection>>* opResult = nullptr;
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
if (FAILED(hr) || !opResult) {
return hr;
}
return ffWaitForAsyncOperation(opResult, result);
}
template <typename TResultProjection, typename TOperation>
static HRESULT ffRunAndWait2(TOperation&& operation, abi_t<TResultProjection>** result) {
*result = nullptr;
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IAsyncOperationWithProgress<TResultProjection, int32_t>>* opResult = nullptr;
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
if (FAILED(hr) || !opResult) {
return hr;
}
return ffWaitForAsyncOperation(opResult, result);
}
#endif // FF_HAVE_WINRT
#else
#error Must be included in C++ source file
#endif
+7 -6
View File
@@ -120,7 +120,7 @@ static const char* getProperty(FFBinder* binder, uint32_t handle, uint32_t prope
uint8_t replyBuffer[128];
FFBinderReply reply = ffBinderReplyCreate(replyBuffer, sizeof(replyBuffer));
const char* error = ffBinderTransact(binder, handle, transaction, &parcel, &reply);
const char* error = ffBinderTransact(binder, handle, transaction, 0, &parcel, &reply);
if (error != nullptr) {
FF_DEBUG("Property %u could not be transacted: %s", property, error);
return error;
@@ -172,23 +172,24 @@ static const char* parseBinder(FFlist* results) {
return error;
}
uint32_t handle = 0;
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &handle);
// Released on every path out of this function, including the ones below that return early.
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_BATTERY_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
if (error != nullptr) {
FF_DEBUG("Looking up the \"%s\" service failed: %s", FF_BATTERY_ANDROID_SERVICE, error);
return error;
}
FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, handle);
FF_DEBUG("The \"%s\" service is handle %u", FF_BATTERY_ANDROID_SERVICE, service.handle);
uint64_t capacity = 0;
error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_CAPACITY, &capacity);
if (error != nullptr) {
FF_DEBUG("The capacity is what failed, so no battery is reported at all");
return error;
}
uint64_t status = 0;
error = getProperty(&binder, handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
error = getProperty(&binder, service.handle, FF_BATTERY_ANDROID_PROPERTY_STATUS, &status);
if (error != nullptr) {
FF_DEBUG("The status is what failed, so no battery is reported at all");
return error;
+3 -1
View File
@@ -7,7 +7,9 @@ typedef struct FFBluetoothResult {
FFstrbuf name;
FFstrbuf address;
FFstrbuf type;
uint8_t battery; // 0-100%
FFBluetoothDeviceType deviceType;
uint8_t battery; // 0-100%
double signalQuality; // 0-100%, -DBL_MAX if unknown
bool connected;
} FFBluetoothResult;
+450 -80
View File
@@ -1,5 +1,8 @@
#include "bluetooth.h"
#include "common/percent.h"
#include "common/strutil.h"
#import <CoreBluetooth/CoreBluetooth.h>
#import <IOBluetooth/IOBluetooth.h>
@interface IOBluetoothDevice()
@@ -8,9 +11,380 @@
@property (nonatomic) uint8_t batteryPercentLeft;
@property (nonatomic) uint8_t batteryPercentRight;
@property (nonatomic) uint8_t batteryPercentSingle;
// Not in the public header. The class is a wrapper around the private Core Bluetooth peer
// objects, and this is the one that carries the `NSUUID` the LE side hands out: `CBClassicPeer`
// and `CBPeripheral` both derive from `CBPeer`, which has one `identifier` and a separate
// `connectedTransport` byte -- one identity per device, not one per stack, which is the same idea
// as the container id the Windows backend joins on. The wrapper's own `identifier` below answers
// nil on macOS 27, so it is kept only as a second source.
@property (nonatomic) NSUUID* identifier;
@property (nonatomic) CBPeripheral* peripheral;
@end
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */)
@interface CBPeripheral()
// Not in the public header. `state` and `isConnected` both answer *disconnected* for a
// peripheral the system has connected but this process has not -- which is every peripheral
// this module ever sees, because it reads the list and never connects to anything. Measured on
// macOS 27 across the seven peripherals `retrieveConnectedPeripheralsWithServices:` returned,
// two of them connected: `state` was 0 on all seven, `isConnected` 0 on all seven, and
// `isConnectedToSystem` 1 on exactly the two.
@property (readonly, nonatomic) BOOL isConnectedToSystem;
@end
// ---------------------------------------------------------------------------------------------
// The LE stack
// ---------------------------------------------------------------------------------------------
//
// Core Bluetooth is the only public route to a peripheral that speaks Low Energy rather than BR/EDR.
// What it will and will not answer was measured on macOS 27 with a standalone probe program
// (12 paired devices, one of them connected), because none of it follows from the headers:
//
// * `retrieveConnectedPeripheralsWithServices:@[]` answers with **nothing** -- 0 devices in 0.1 ms.
// The empty list is not "everything", so the assigned 16-bit UUIDs have to be spelled out. With
// them the same call answers in 0.5 ms, which is the reason this is the entry point that is used.
//
// * Scanning is not an option. `scanForPeripheralsWithServices:nil` turned up **57** peripherals in
// five seconds and 54 of them had no name at all: it reports every phone, watch and beacon in
// radio range, not the devices the machine knows. It is also the only way to see a peripheral that
// is not connected, so `showDisconnected` cannot be honoured on this platform -- there is no
// "paired LE peripherals" query the way `IOBluetoothDevice.pairedDevices` is one for classic.
//
// * The two stacks are joined on `NSUUID`, and the join is exact rather than a guess. The LE side
// hands one out as `CBPeripheral.identifier`, and the classic side has the same value on the
// `CBPeripheral` that `IOBluetoothDevice` wraps, the class being a wrapper around the private Core
// Bluetooth peer objects. A `CBPeer` carries a single `identifier` and a separate
// `connectedTransport` byte, so a dual-mode device is one identity rather than two -- the same
// shape as the container id the Windows backend joins on. It is not the device address: no LE
// identifier carried any of the twelve classic addresses.
// Two sources were measured on macOS 27 and only one of them is safe. `IOBluetoothDevice`'s own
// `identifier` answers nil for all twelve paired devices, and `peer.identifier` disagrees with
// `peripheral.identifier` on three of them (ERAZER G501: `23F3EDAC-...` against `18BDC152-...`),
// so `peer` is the classic peer and `peripheral` the LE one. Only `peripheral` is read here, and
// only where it is non-nil; the name is the fallback for the rest, trusted only when exactly one
// classic entry carries it. The machine this was written against has two devices called
// "ERAZER N500", and guessing between them would put one device's LE link on the other.
//
// * The object that would make this trivially exact -- the private `CBDevice`, which holds
// `bleAddressData` and `btAddressData` at once -- is out of reach: `+[CBDiscovery
// devicesWithDiscoveryFlags:error:]` answers `CBErrorDomain -71168` for every flag tried, which is
// the entitlement it needs and a plain command line tool does not have.
//
// * `pairedDevices` hands a dual-mode device out **twice** on macOS 27, once per stack, under the
// same name and the same address: the headset this was written against comes back as two entries,
// both carrying the same `peer` and `peripheral`. The second copy is therefore folded into the
// first as it is detected, rather than appended and removed again, both because printing one
// headset twice is wrong and because the duplicate makes the name ambiguous, which would leave the
// fallback above unusable. It matters that the fold happens while walking the list rather than
// afterwards: the identifier registered for the LE join comes from the *second* copy, which is the
// one holding the `CBPeripheral`.
//
// * The classic half cannot be folded into this one, and was measured rather than assumed. Core
// Bluetooth's public surface has no paired-device query at all: `CBCentralManager` enumerates
// *connected* peripherals only, which on this machine is 2 of the 12. The paired query does exist,
// as `CBClassicManager.retrievePairedPeersWithOptions:`, but it is private and it is what
// `IOBluetoothDevice.pairedDevices` already calls -- `IOBluetoothCoreBluetoothCoordinator`, which
// lives in IOBluetooth.framework, holds the manager and is the caller. Both Core-Bluetooth-only
// routes were measured and neither replaces it: a `CBClassicManager` this process creates never
// fills its peer map (0 entries after two seconds, `retrievePairedPeersWithOptions:` answers nil),
// and `CBCentralManager.sharedPairingAgent.retrievePairedPeers` answers with 7 of the 11 devices
// as bare `CBPeripheral` objects carrying no address, no battery and no class of device.
//
// * `CBPeripheral.state` is not the connection state the caller means. It answers `Disconnected` for
// every peripheral the retrieval above returns, and so does `isConnected`; only the private
// `isConnectedToSystem` reflects that the system has the device. Reading `state` reported every LE
// peripheral as disconnected, which both suppressed the signal strength below and dropped a
// connected LE-only peripheral whenever `showDisconnected` was off.
//
// * The signal strength is a `readRSSI` round trip. It is asked for only where the system says the
// link is up, and the callbacks are waited for as a group, so a machine with no LE link pays
// nothing.
//
// * `showType` is honoured here rather than in the module, because the two stacks are two separate
// walks with two separate permission surfaces: a stack the user switched off is not asked about
// at all. The LE pass keeps working without the classic one, it only loses the address join and
// falls back to matching by name.
//
// Note on the permission model, which is what the first bullet above cannot show: any process that
// touches Bluetooth is subject to the `kTCCServiceBluetoothAlways` service, and the request is
// attributed to the *responsible* process. A host application that does not declare
// `NSBluetoothAlwaysUsageDescription` in its own Info.plist does not get a prompt -- the process is
// killed with SIGABRT instead. Terminal.app is an Apple platform binary and iTerm2, Ghostty and
// kitty all declare the key, so a normal terminal run prompts and remembers the answer. fastfetch
// deliberately does not embed a usage description of its own: that would give it a second TCC
// identity, which for an unsigned binary changes on every rebuild and would re-prompt each time.
#define FF_BLUETOOTH_LE_STATE_WAIT_MS 500 // The state callback lands in 18.7 ms when powered on
// A cap, not a measured latency. `didReadRSSI` was never reached on macOS 27 until the
// `isConnectedToSystem` fix below, so what one round trip costs is still unmeasured; this is ten
// times the 18.7 ms the state callback was measured at, and it is only ever paid when the system
// reports a live LE link.
#define FF_BLUETOOTH_LE_RSSI_WAIT_MS 200
@interface FFBluetoothLeCentral : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
@property(nonatomic, strong) CBCentralManager* central;
@property(nonatomic, strong) NSMutableDictionary<NSString*, NSNumber*>* signalStrength;
@property(nonatomic) NSUInteger signalStrengthAsked;
@property(nonatomic) NSUInteger signalStrengthAnswered;
@property(nonatomic) BOOL stateKnown;
@end
@implementation FFBluetoothLeCentral
- (instancetype) init {
self = [super init];
_signalStrength = [NSMutableDictionary dictionary];
return self;
}
- (void) centralManagerDidUpdateState:(CBCentralManager*)central {
self.stateKnown = YES;
}
- (void) peripheral:(CBPeripheral*)peripheral didReadRSSI:(NSNumber*)RSSI error:(NSError*)error {
++self.signalStrengthAnswered;
if (!error && RSSI) {
self.signalStrength[peripheral.identifier.UUIDString] = RSSI;
}
}
@end
// The assigned 16-bit UUIDs, plus the 0xFEE0-0xFEFF block, which is where the vendor-specific ones
// live. `retrieveConnectedPeripheralsWithServices:` filters on this list, so anything exposing only
// a 128-bit custom service is out of reach -- that is a limit of the API, not of this list.
static NSArray<CBUUID*>* leServiceUuids(void) {
static NSArray<CBUUID*>* uuids;
static dispatch_once_t once;
dispatch_once(&once, ^{
NSMutableArray<CBUUID*>* list = [NSMutableArray array];
for (uint32_t i = 0x1800; i <= 0x18FF; ++i) {
[list addObject: [CBUUID UUIDWithString: [NSString stringWithFormat: @"%04X", i]]];
}
for (uint32_t i = 0xFEE0; i <= 0xFEFF; ++i) {
[list addObject: [CBUUID UUIDWithString: [NSString stringWithFormat: @"%04X", i]]];
}
uuids = list;
});
return uuids;
}
// See the note on `CBPeripheral.isConnectedToSystem` at the top of the file. The fallback keeps the
// module working on a system that does not answer the private property.
static bool leIsConnected(CBPeripheral* peripheral) {
if ([peripheral respondsToSelector: @selector(isConnectedToSystem)]) {
return peripheral.isConnectedToSystem;
}
return peripheral.state == CBPeripheralStateConnected;
}
// The address is what a device is looked up by, both while the classic list is walked and by the LE
// pass, and it is the only field the two copies of a dual-mode device are guaranteed to agree on.
static FFBluetoothResult* findByAddress(FFlist* devices, const char* address) {
FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) {
if (ffStrbufEqualS(&device->address, address)) {
return device;
}
}
return nullptr;
}
// The class of device, as the classic stack reports it. It is built into a scratch buffer rather than
// straight into the entry because the entry may already exist: see the merge in `ffDetectBluetooth`,
// where the second copy of a dual-mode device only fills what the first copy left empty.
static void setClassOfDevice(FFstrbuf* type, IOBluetoothDevice* ioDevice) {
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorLimitedDiscoverableMode)
ffStrbufAppendS(type, "Limited Discoverable Mode, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved1)
ffStrbufAppendS(type, "LE audio, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved2)
ffStrbufAppendS(type, "Reserved for future use, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorPositioning)
ffStrbufAppendS(type, "Positioning, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorNetworking)
ffStrbufAppendS(type, "Networking, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorRendering)
ffStrbufAppendS(type, "Rendering, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorCapturing)
ffStrbufAppendS(type, "Capturing, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorObjectTransfer)
ffStrbufAppendS(type, "Object Transfer, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorAudio)
ffStrbufAppendS(type, "Audio, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorTelephony)
ffStrbufAppendS(type, "Telephony, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorInformation)
ffStrbufAppendS(type, "Information, ");
if(type->length == 0)
{
switch(ioDevice.deviceClassMajor)
{
case kBluetoothDeviceClassMajorMiscellaneous:
ffStrbufAppendS(type, "Miscellaneous");
break;
case kBluetoothDeviceClassMajorComputer:
ffStrbufAppendS(type, "Computer");
break;
case kBluetoothDeviceClassMajorPhone:
ffStrbufAppendS(type, "Phone");
break;
case kBluetoothDeviceClassMajorLANAccessPoint:
ffStrbufAppendS(type, "LAN/Network Access point");
break;
case kBluetoothDeviceClassMajorAudio:
ffStrbufAppendS(type, "Audio/Video");
break;
case kBluetoothDeviceClassMajorPeripheral:
ffStrbufAppendS(type, "Peripheral");
break;
case kBluetoothDeviceClassMajorImaging:
ffStrbufAppendS(type, "Imaging");
break;
case kBluetoothDeviceClassMajorWearable:
ffStrbufAppendS(type, "Wearable");
break;
case kBluetoothDeviceClassMajorToy:
ffStrbufAppendS(type, "Toy");
break;
case kBluetoothDeviceClassMajorHealth:
ffStrbufAppendS(type, "Health");
break;
case kBluetoothDeviceClassMajorUnclassified:
ffStrbufAppendS(type, "Uncategorized");
break;
default:
ffStrbufAppendS(type, "Unknown");
break;
}
}
else
{
ffStrbufTrimRight(type, ' ');
ffStrbufTrimRight(type, ',');
}
}
// The fallback for a system whose `IOBluetoothDevice` does not answer `identifier`. A name that is
// carried by more than one classic entry cannot identify anything, so an ambiguous name joins
// nothing and the peripheral is reported as its own device instead of being merged.
static FFBluetoothResult* findByName(FFlist* devices, const char* name) {
FFBluetoothResult* found = nullptr;
FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) {
// Only the classic half counts. The LE pass runs after the classic one and appends its
// entries to the same list, so the peripheral an earlier iteration of this very loop created
// would otherwise make its own name look ambiguous -- and the rule is there to keep one name
// from standing for two classic devices, not to count the LE copy of one of them.
if (!(device->deviceType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT)) {
continue;
}
if (!ffStrEqualsIgnCase(device->name.chars, name)) {
continue;
}
if (found) {
return nullptr;
}
found = device;
}
return found;
}
static const char* detectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSDictionary<NSString*, NSString*>* classicAddressByIdentifier) {
CBManagerAuthorization authorization = CBCentralManager.authorization;
if (authorization == CBManagerAuthorizationDenied || authorization == CBManagerAuthorizationRestricted) {
return nullptr; // The user said no. `NotDetermined` is left to the manager below, which asks.
}
FFBluetoothLeCentral* delegate = [FFBluetoothLeCentral new];
delegate.central = [[CBCentralManager alloc] initWithDelegate: delegate queue: dispatch_get_main_queue()];
// Every CoreBluetooth entry point answers on a delegate callback, so the run loop has to be
// pumped until the state callback arrives. It is bounded because a module cannot wait forever.
NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow: (double) FF_BLUETOOTH_LE_STATE_WAIT_MS / 1000];
while (!delegate.stateKnown && [deadline timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]];
}
if (delegate.central.state != CBManagerStatePoweredOn) {
return nullptr; // Powered off, unauthorized or no radio: the classic list stands on its own
}
NSArray<CBPeripheral*>* peripherals = [delegate.central retrieveConnectedPeripheralsWithServices: leServiceUuids()];
if (peripherals.count == 0) {
return nullptr;
}
for (CBPeripheral* peripheral in peripherals) {
if (leIsConnected(peripheral)) {
peripheral.delegate = delegate;
++delegate.signalStrengthAsked;
[peripheral readRSSI];
}
}
if (delegate.signalStrengthAsked > 0) {
deadline = [NSDate dateWithTimeIntervalSinceNow: (double) FF_BLUETOOTH_LE_RSSI_WAIT_MS / 1000];
while (delegate.signalStrengthAnswered < delegate.signalStrengthAsked && [deadline timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.01]];
}
}
for (CBPeripheral* peripheral in peripherals) {
const char* name = peripheral.name.length > 0 ? peripheral.name.UTF8String : nullptr;
bool connected = leIsConnected(peripheral);
// The classic half wins wherever it has an answer: its name, address and type come from the
// class of device rather than from a name the two stacks may spell differently. The
// identifier is what makes the join exact; the name only stands in when it is missing.
FFBluetoothResult* target = nullptr;
NSString* address = classicAddressByIdentifier[peripheral.identifier.UUIDString];
if (address) {
target = findByAddress(devices, address.UTF8String);
}
if (!target && name) {
target = findByName(devices, name);
}
if (!target && !connected && !options->showDisconnected) {
continue; // An LE-only peripheral nobody asked to see
}
if (!target) {
target = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInit(&target->name);
ffStrbufInit(&target->address);
ffStrbufInit(&target->type);
target->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
target->battery = 0;
target->signalQuality = -DBL_MAX;
target->connected = false;
if (name) {
ffStrbufSetS(&target->name, name);
} else {
ffStrbufSetStatic(&target->name, "Unknown Device");
}
// Core Bluetooth never hands out the device address, so the identifier is the only
// identity there is. It is stable on the host but is not an address, and it is not the
// 17-character form the other backends report.
ffStrbufSetS(&target->address, peripheral.identifier.UUIDString.UTF8String);
}
target->deviceType = (FFBluetoothDeviceType) (target->deviceType | FF_BLUETOOTH_DEVICE_TYPE_LE_BIT);
target->connected |= connected;
NSNumber* rssi = delegate.signalStrength[peripheral.identifier.UUIDString];
if (rssi) {
target->signalQuality = ffRssiToSignalQuality(rssi.intValue);
}
}
return nullptr;
}
// The classic (BR/EDR) half. `classicAddressByIdentifier` is an output rather than an input: the
// classic list is the only place a device address can be had from, and the LE pass needs those
// addresses to recognise a peripheral that has already been listed.
static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */, NSMutableDictionary<NSString*, NSString*>* classicAddressByIdentifier)
{
NSArray<IOBluetoothDevice*>* ioDevices = IOBluetoothDevice.pairedDevices;
if(!ioDevices)
@@ -21,91 +395,87 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF
if (!options->showDisconnected && !ioDevice.isConnected)
continue;
FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInitS(&device->name, ioDevice.name.UTF8String);
ffStrbufInitS(&device->address, ioDevice.addressString.UTF8String);
ffStrbufReplaceAllC(&device->address, '-', ':');
ffStrbufUpperCase(&device->address);
ffStrbufInit(&device->type);
// Normalised before the lookup rather than after the entry is created, because the address is
// what decides whether this is a device already seen: `pairedDevices` reports a dual-mode one
// once per stack, under one name and one address.
FF_STRBUF_AUTO_DESTROY address = ffStrbufCreateS(ioDevice.addressString.UTF8String);
ffStrbufReplaceAllC(&address, '-', ':');
ffStrbufUpperCase(&address);
// Registered before the duplicate is folded away, because it is the *second* copy of a
// dual-mode device that carries the `CBPeripheral` -- the first one has `peripheral` nil. The
// wrapper's own `identifier` answers nil here, but that `CBPeripheral` carries the same
// `NSUUID` the LE side hands out. Both accesses are guarded because both are private: a system
// that answers neither falls back to matching by name.
if (address.length > 0 && [ioDevice respondsToSelector: @selector(peripheral)] && ioDevice.peripheral) {
NSUUID* identifier = ioDevice.peripheral.identifier;
if (identifier)
classicAddressByIdentifier[identifier.UUIDString] = [NSString stringWithUTF8String: address.chars];
}
FF_STRBUF_AUTO_DESTROY type = ffStrbufCreate();
setClassOfDevice(&type, ioDevice);
uint8_t battery = 0;
if (ioDevice.batteryPercentSingle)
device->battery = ioDevice.batteryPercentSingle;
battery = ioDevice.batteryPercentSingle;
else if (ioDevice.batteryPercentCombined)
device->battery = ioDevice.batteryPercentCombined;
battery = ioDevice.batteryPercentCombined;
else if (ioDevice.batteryPercentCase)
device->battery = ioDevice.batteryPercentCase;
battery = ioDevice.batteryPercentCase;
device->connected = !!ioDevice.isConnected;
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorLimitedDiscoverableMode)
ffStrbufAppendS(&device->type, "Limited Discoverable Mode, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved1)
ffStrbufAppendS(&device->type, "LE audio, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved2)
ffStrbufAppendS(&device->type, "Reserved for future use, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorPositioning)
ffStrbufAppendS(&device->type, "Positioning, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorNetworking)
ffStrbufAppendS(&device->type, "Networking, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorRendering)
ffStrbufAppendS(&device->type, "Rendering, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorCapturing)
ffStrbufAppendS(&device->type, "Capturing, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorObjectTransfer)
ffStrbufAppendS(&device->type, "Object Transfer, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorAudio)
ffStrbufAppendS(&device->type, "Audio, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorTelephony)
ffStrbufAppendS(&device->type, "Telephony, ");
if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorInformation)
ffStrbufAppendS(&device->type, "Information, ");
// A device with no address cannot be recognised as a repeat, so it is always its own entry.
FFBluetoothResult* device = address.length > 0 ? findByAddress(devices, address.chars) : nullptr;
if (!device)
{
device = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInit(&device->name);
ffStrbufInit(&device->address);
ffStrbufInit(&device->type);
device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
device->battery = 0;
device->signalQuality = -DBL_MAX;
device->connected = false;
ffStrbufSetS(&device->name, ioDevice.name.UTF8String);
ffStrbufSet(&device->address, &address);
}
if(device->type.length == 0)
{
switch(ioDevice.deviceClassMajor)
{
case kBluetoothDeviceClassMajorMiscellaneous:
ffStrbufAppendS(&device->type, "Miscellaneous");
break;
case kBluetoothDeviceClassMajorComputer:
ffStrbufAppendS(&device->type, "Computer");
break;
case kBluetoothDeviceClassMajorPhone:
ffStrbufAppendS(&device->type, "Phone");
break;
case kBluetoothDeviceClassMajorLANAccessPoint:
ffStrbufAppendS(&device->type, "LAN/Network Access point");
break;
case kBluetoothDeviceClassMajorAudio:
ffStrbufAppendS(&device->type, "Audio/Video");
break;
case kBluetoothDeviceClassMajorPeripheral:
ffStrbufAppendS(&device->type, "Peripheral");
break;
case kBluetoothDeviceClassMajorImaging:
ffStrbufAppendS(&device->type, "Imaging");
break;
case kBluetoothDeviceClassMajorWearable:
ffStrbufAppendS(&device->type, "Wearable");
break;
case kBluetoothDeviceClassMajorToy:
ffStrbufAppendS(&device->type, "Toy");
break;
case kBluetoothDeviceClassMajorHealth:
ffStrbufAppendS(&device->type, "Health");
break;
case kBluetoothDeviceClassMajorUnclassified:
ffStrbufAppendS(&device->type, "Uncategorized");
break;
default:
ffStrbufAppendS(&device->type, "Unknown");
break;
}
}
else
{
ffStrbufTrimRight(&device->type, ' ');
ffStrbufTrimRight(&device->type, ',');
}
// The two copies of a dual-mode device are not identical -- one is the classic peer and the
// other the LE one -- so each field is taken wherever the entry already kept has nothing,
// rather than the second copy simply being dropped. The name is not merged: both copies spell
// it the same way, and the first one is the one the LE pass looks up by.
if (device->battery == 0)
device->battery = battery;
if (device->type.length == 0)
ffStrbufSet(&device->type, &type);
device->deviceType = (FFBluetoothDeviceType) (device->deviceType | FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT); // IOBluetooth is the BR/EDR stack
device->connected |= !!ioDevice.isConnected;
}
return nullptr;
}
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */)
{
// `showType` selects which stacks are walked, not merely which entries are printed: a stack the
// user switched off is not asked about at all. The classic pass is the one that produces the
// identifier map the LE pass joins on, so it is nil when that pass does not run -- the LE pass
// then has nothing to match against and reports every peripheral as a device of its own.
NSMutableDictionary<NSString*, NSString*>* classicAddressByIdentifier = nil;
if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) {
classicAddressByIdentifier = [NSMutableDictionary dictionary];
const char* error = detectClassic(options, devices, classicAddressByIdentifier);
if (error) {
return error;
}
}
// Last, so that a peripheral answering on both stacks is matched against the classic list and
// ends up as one entry rather than two.
if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) {
detectLe(options, devices, classicAddressByIdentifier);
}
return nullptr;
+9 -1
View File
@@ -15,12 +15,20 @@ static int enumDev([[maybe_unused]] int sockfd, struct bt_devinfo const* dev, FF
ffStrbufInitS(&device->address, bt_ntoa(&dev->bdaddr, nullptr));
ffStrbufUpperCase(&device->address);
ffStrbufInit(&device->type);
device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT; // Netgraph only carries the BR/EDR stack
device->battery = 0;
device->signalQuality = -DBL_MAX;
device->connected = true;
return 0;
}
const char* ffDetectBluetooth([[maybe_unused]] FFBluetoothOptions* options, [[maybe_unused]] FFlist* devices /* FFBluetoothResult */) {
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
// Netgraph carries the BR/EDR stack and nothing else, so there is no second function to dispatch
// to here: a Low Energy-only configuration simply has nothing to walk.
if (!(options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT)) {
return nullptr;
}
// struct hostent* ent = bt_gethostent();
if (bt_devenum((void*) enumDev, devices) < 0) {
return "bt_devenum() failed";
+9 -1
View File
@@ -5,7 +5,13 @@ extern "C" {
#include <bluetooth/LocalDevice.h>
const char* ffDetectBluetooth([[maybe_unused]] FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
// The local adapter's device class is a BR/EDR concept and the adapter is the only device this
// backend has, so a Low Energy-only configuration leaves the list empty.
if (!(options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT)) {
return nullptr;
}
using namespace Bluetooth;
FF_SUPPRESS_IO();
@@ -21,7 +27,9 @@ const char* ffDetectBluetooth([[maybe_unused]] FFBluetoothOptions* options, FFli
ffStrbufInitS(&device->name, dev->GetFriendlyName());
ffStrbufInitS(&device->address, bdaddrUtils::ToString(dev->GetBluetoothAddress()).String());
ffStrbufInitS(&device->type, devClass.String());
device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT; // The local adapter's device class is a BR/EDR concept
device->battery = 0;
device->signalQuality = -DBL_MAX;
device->connected = true;
// TODO: more devices?
+101 -1
View File
@@ -1,4 +1,5 @@
#include "bluetooth.h"
#include "common/percent.h"
#include "common/strutil.h"
#ifdef FF_HAVE_DBUS
@@ -29,6 +30,10 @@ array [ //root
string "Connected"
variant boolean true
)
dict entry( //value
string "RSSI"
variant int16 -63
)
]
)
dict entry( //property
@@ -45,6 +50,66 @@ array [ //root
]
*/
/* How the two stack bits are inferred here, and why the LE one is mostly unattainable.
*
* `org.bluez.Device1` has no "does this device support LE" property, so both bits are read off
* the only two stack-specific ones it has:
*
* Class - BR/EDR Class of Device, learned from BR/EDR EIR / inquiry data
* (BlueZ src/eir.c, EIR_CLASS_OF_DEV) and persisted in the device file.
* Appearance - GATT appearance, learned from LE advertising data only
* (BlueZ src/adapter.c -> src/eir.c, EIR_GAP_APPEARANCE, AD type 0x19).
*
* For a dual-mode device only the first one ever shows up, for two independent reasons:
*
* 1. BlueZ never publishes `Appearance` for a device that has a `Class`. Its getter short
* circuits on it, and the property table registers that getter as the `exists` callback,
* so the key is absent from `GetManagedObjects` altogether rather than present with a
* default value:
*
* static gboolean get_appearance(...)
* {
* if (dev_property_exists_class(property, data)) // device->class != 0
* return FALSE;
* if (device->appearance) { ... return TRUE; }
* return FALSE;
* }
*
* `btd_device_get_icon()` prefers `Class` the same way, which is why `Icon` describes a
* classic-seen device by its CoD. BlueZ treats the two as mutually exclusive descriptions
* of the device's *category*, not as a record of which radios it speaks. Checked against
* BlueZ 5.87, src/device.c.
*
* 2. Even without (1) the value is usually unknown: it is only learned if the device
* advertised AD type 0x19 while we were scanning (phones rarely do), a zero is never
* persisted (`/var/lib/bluetooth/<adapter>/<addr>/info` then has no `Appearance=` line),
* and that directory is mode 0700 so it could not be read here anyway.
*
* Consequence: a device ever seen over BR/EDR -- every paired phone, since all of them have a
* CoD -- can only ever be reported as Classic, however LE capable it really is, and no amount
* of extra data collected on this path can change that. Windows gets it right because it
* enumerates the two stacks separately (bluetooth_windows.cpp walks the WinRT
* BluetoothLEDevice endpoints and ORs in the LE bit); BlueZ has no equivalent enumeration --
* one Device1 object per address, no per-stack capability -- and its own storage only records
* `SupportedTechnologies=BR/EDR;LE` once a real LE connection or an LE sighting has happened.
*
* Signals that look like LE proof but are not, and are deliberately not used here:
* - GAP / GATT in `UUIDs` (0x1800 / 0x1801): BlueZ mirrors BR/EDR SDP records advertising
* the ATT bearer (L2CAP PSM 0x001F) into the same `attributes` cache it loads GATT
* primaries from, so those UUIDs can come from a purely classic SDP browse.
* - `AddressType`: a BR/EDR-only record reports "public" too -- `device->bdaddr_type` is
* forced to `BDADDR_BREDR` when the record is loaded.
* - `ManufacturerData` / `ServiceData`: BlueZ parses them out of BR/EDR EIR as well.
*
* The one durable LE-only signal BlueZ does publish, if this is ever revisited:
* - `AdvertisingFlags` exists once the device has been seen in an LE advertising report.
* It is written only from the `bdaddr_type != BDADDR_BREDR` branch of BlueZ's device-found
* handler, starts out invalid (0xff) and is never cleared, so it survives the end of a
* discovery session -- but it is not persisted, so a bluetoothd restart loses it again.
* `PreferredBearer` would be even better (it exists exactly when both bearers are known),
* yet it is flagged experimental and needs `Experimental=true` to be exported at all.
*/
static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) {
if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) {
return true;
@@ -67,7 +132,21 @@ static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBlue
} else if (ffStrEquals(deviceProperty, "Name")) {
ffDBusGetString(dbus, &dictIter, &device->name);
} else if (ffStrEquals(deviceProperty, "Icon")) {
// BlueZ derives this from the Class of Device whenever there is one and only falls back
// to the LE appearance otherwise, so `type` describes a classic-seen device by its CoD.
ffDBusGetString(dbus, &dictIter, &device->type);
} else if (ffStrEquals(deviceProperty, "Class")) {
// The BR/EDR Class of Device. Published iff BlueZ ever learned a CoD, which comes from
// BR/EDR EIR / inquiry data, so this means "seen over BR/EDR" -- not "classic only".
// Its mere presence is also what hides `Appearance` in the branch below.
device->deviceType |= FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT;
} else if (ffStrEquals(deviceProperty, "Appearance")) {
// The LE-only GATT appearance. Absence does *not* mean "not LE": BlueZ omits this
// property entirely for every device that has a `Class` (see the block comment above),
// and knows the value only if the device advertised AD type 0x19 while we were scanning.
// The two properties are mutually exclusive in BlueZ's view, so a dual-mode phone that
// was ever seen classically reports Classic only, whatever it is capable of.
device->deviceType |= FF_BLUETOOTH_DEVICE_TYPE_LE_BIT;
} else if (ffStrEquals(deviceProperty, "Percentage")) {
uint64_t percentage;
if (ffDBusGetUint(dbus, &dictIter, &percentage)) {
@@ -75,6 +154,14 @@ static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBlue
}
} else if (ffStrEquals(deviceProperty, "Connected")) {
ffDBusGetBool(dbus, &dictIter, &device->connected);
} else if (ffStrEquals(deviceProperty, "RSSI")) {
// `int16` on the wire, in dBm. Written only from discovery reports and invalidated when
// the discovery session ends, so it says nothing about the connection state: a connected
// device that has not been scanned lately reports no RSSI at all.
int64_t rssi;
if (ffDBusGetInt(dbus, &dictIter, &rssi)) {
device->signalQuality = ffRssiToSignalQuality((int) rssi);
}
} else if (ffStrEquals(deviceProperty, "Paired")) {
bool paired = true;
ffDBusGetBool(dbus, &dictIter, &paired);
@@ -155,7 +242,9 @@ static FFBluetoothResult* detectBluetoothObject(FFlist* devices, FFDBusData* dbu
ffStrbufInit(&device->name);
ffStrbufInit(&device->address);
ffStrbufInit(&device->type);
device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
device->battery = 0;
device->signalQuality = -DBL_MAX;
device->connected = false;
do {
@@ -177,11 +266,22 @@ static void detectBluetoothRoot(FFBluetoothOptions* options, FFlist* devices, FF
FFBluetoothResult* device = detectBluetoothObject(devices, dbus, &arrayIter);
if (device) {
if (!options->showDisconnected && !device->connected) {
// `showType` selects which stacks are reported. Unlike the other backends there is no
// per-stack call to skip here: both bits are read off this one walk of Device1, so the
// filter lands on the result instead. That filter is necessarily one sided -- see the
// block comment above detectBluetoothValue(): `Class` and `Appearance` are mutually
// exclusive in BlueZ, so a dual-mode device that was ever seen classically carries only
// the classic bit, and `showType: le` drops it even though it does speak LE. A device
// whose stack could not be determined at all is kept -- BlueZ promises neither property,
// and dropping those would lose devices the default configuration reports.
bool deviceTypeWanted = device->deviceType == FF_BLUETOOTH_DEVICE_TYPE_NONE || (device->deviceType & options->showType);
if (!deviceTypeWanted || (!options->showDisconnected && !device->connected)) {
ffStrbufDestroy(&device->name);
ffStrbufDestroy(&device->address);
ffStrbufDestroy(&device->type);
--devices->length;
continue;
}
if (device->name.length == 0) {
+164 -117
View File
@@ -23,12 +23,45 @@ DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_ServiceGUID, 0x2bd67d8b, 0x8beb, 0x48d5, 0x8
/* DEVPROP_TYPE_UINT8 */
DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_BatteryLevel, 0x104ea319, 0x6ee2, 0x4701, 0xbd, 0x47, 0x8d, 0xdb, 0xf4, 0x25, 0xbb, 0xe5, 2);
// TODO: use CM API to fetch bluetooth devices instead of BluetoothFindFirstDevice, if we find DEVPKEY_Bluetooth_IsConnected or similar
// Windows keeps the classic (BR/EDR) and the Low Energy stacks apart, and so does fastfetch: the
// device search below only ever returns BR/EDR devices, and the LE half lives in
// bluetooth_windows.cpp because reaching it means WinRT, which is C++ only. This is the bridge --
// it walks the LE stack, then folds what it finds into the list the classic half already built.
const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */);
// The device tree cannot answer "is this connected". DEVPKEY_DeviceContainer_IsConnected and
// IsPaired are absent from every present devnode, and CM_Get_DevNode_PropertyW answers
// CR_NO_SUCH_VALUE for PKEY_Devices_Aep_IsConnected. The value does exist, but only in the
// Association Endpoint property store, which is reachable through WinRT alone -- see
// ffBluetoothDetectLe() and the note there about the cost of each way in.
#define GUID_DEVCLASS_BLUETOOTH_STRING L"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}" // Found in <devguid.h>
#define GUID_DEVCLASS_MEDIA_STRING L"{4d36e96c-e325-11ce-bfc1-08002be10318}" // Found in <devguid.h>
static const char* ffBluetoothDetectBattery(FFlist* devices) {
// The device tree spells an address without separators, while the list holds the printed form. The
// case has to be folded because it depends on the enumerator: the BTHENUM nodes are upper case and
// the BTHLE / BTHLEDevice ones lower case, and the list is always upper case.
static bool addressEquals(const wchar_t* plain, const FFstrbuf* address) {
if (address->length != 17) {
return false;
}
for (uint32_t i = 0; i < 6; ++i) {
if (toupper((unsigned char) plain[i * 2]) != toupper((unsigned char) address->chars[i * 3]) ||
toupper((unsigned char) plain[i * 2 + 1]) != toupper((unsigned char) address->chars[i * 3 + 1])) {
return false;
}
}
return true;
}
static const char* detectBattery(FFlist* devices) {
ULONG idListLength = 0;
// The class filter is a no-op here, and deliberately so: CM_Get_Device_ID_ListW() only honours the
// filter string together with CM_GETIDLIST_FILTER_CLASS, so this walks every present node of the
// tree rather than the media class. That is what finds the level -- the node carrying
// DEVPKEY_Bluetooth_BatteryLevel is a BTHENUM service node (the Hands-Free profile one), which is
// not in the media class at all, so narrowing the filter would lose it.
CONFIGRET status = CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_MEDIA_STRING, CM_GETIDLIST_FILTER_PRESENT);
if (status != CR_SUCCESS) {
return "CM_Get_Device_ID_List_SizeW failed";
@@ -51,8 +84,6 @@ static const char* ffBluetoothDetectBattery(FFlist* devices) {
for (const wchar_t* deviceId = idList; *deviceId; deviceId += wcslen(deviceId) + 1) {
DEVINST devInst = 0;
// Hands-Free profile service; headsets often expose battery level through this media device node rather than the Bluetooth device node
// BthHFEnum
if (CM_Locate_DevNodeW(&devInst, (DEVINSTID_W) deviceId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) {
continue;
}
@@ -76,18 +107,7 @@ static const char* ffBluetoothDetectBattery(FFlist* devices) {
}
FF_LIST_FOR_EACH (FFBluetoothResult, bt, *devices) {
if (deviceAddress[0] == bt->address.chars[0] &&
deviceAddress[1] == bt->address.chars[1] &&
deviceAddress[2] == bt->address.chars[3] &&
deviceAddress[3] == bt->address.chars[4] &&
deviceAddress[4] == bt->address.chars[6] &&
deviceAddress[5] == bt->address.chars[7] &&
deviceAddress[6] == bt->address.chars[9] &&
deviceAddress[7] == bt->address.chars[10] &&
deviceAddress[8] == bt->address.chars[12] &&
deviceAddress[9] == bt->address.chars[13] &&
deviceAddress[10] == bt->address.chars[15] &&
deviceAddress[11] == bt->address.chars[16]) {
if (addressEquals(deviceAddress, &bt->address)) {
bt->battery = battery;
break;
}
@@ -97,7 +117,10 @@ static const char* ffBluetoothDetectBattery(FFlist* devices) {
return nullptr;
}
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
// The classic (BR/EDR) half. `BluetoothFindFirstDevice` only ever answers with BR/EDR devices; the
// Low Energy stack is a separate walk, in bluetooth_windows.cpp, which folds what it finds into the
// list this function builds.
static const char* detectClassic(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1)
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstDevice)
FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextDevice)
@@ -113,110 +136,134 @@ const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FF
.dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS) },
&btdi);
if (!hFind) {
if (GetLastError() == ERROR_NO_MORE_ITEMS) {
return nullptr;
if (GetLastError() != ERROR_NO_MORE_ITEMS) {
return "BluetoothFindFirstDevice() failed";
}
return "BluetoothFindFirstDevice() failed";
}
do {
if (!options->showDisconnected && !btdi.fConnected) {
continue;
}
FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInitWS(&device->name, btdi.szName);
ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", btdi.Address.rgBytes[5], btdi.Address.rgBytes[4], btdi.Address.rgBytes[3], btdi.Address.rgBytes[2], btdi.Address.rgBytes[1], btdi.Address.rgBytes[0]);
ffStrbufInit(&device->type);
device->battery = 0;
device->connected = !!btdi.fConnected;
// https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf
if (BitTest(&btdi.ulClassofDevice, 13)) {
ffStrbufAppendS(&device->type, "Limited Discoverable Mode, ");
}
if (BitTest(&btdi.ulClassofDevice, 14)) {
ffStrbufAppendS(&device->type, "LE audio, ");
}
if (BitTest(&btdi.ulClassofDevice, 15)) {
ffStrbufAppendS(&device->type, "Reserved for future use, ");
}
if (BitTest(&btdi.ulClassofDevice, 16)) {
ffStrbufAppendS(&device->type, "Positioning, ");
}
if (BitTest(&btdi.ulClassofDevice, 17)) {
ffStrbufAppendS(&device->type, "Networking, ");
}
if (BitTest(&btdi.ulClassofDevice, 18)) {
ffStrbufAppendS(&device->type, "Rendering, ");
}
if (BitTest(&btdi.ulClassofDevice, 19)) {
ffStrbufAppendS(&device->type, "Capturing, ");
}
if (BitTest(&btdi.ulClassofDevice, 20)) {
ffStrbufAppendS(&device->type, "Object Transfer, ");
}
if (BitTest(&btdi.ulClassofDevice, 21)) {
ffStrbufAppendS(&device->type, "Audio, ");
}
if (BitTest(&btdi.ulClassofDevice, 22)) {
ffStrbufAppendS(&device->type, "Telephony, ");
}
if (BitTest(&btdi.ulClassofDevice, 23)) {
ffStrbufAppendS(&device->type, "Information, ");
}
if (device->type.length == 0) {
uint32_t majorDeviceClasses = (btdi.ulClassofDevice >> 8) & ~(UINT32_MAX << 5);
switch (majorDeviceClasses) {
case 0b00000:
ffStrbufAppendS(&device->type, "Miscellaneous");
break;
case 0b00001:
ffStrbufAppendS(&device->type, "Computer");
break;
case 0b00010:
ffStrbufAppendS(&device->type, "Phone");
break;
case 0b00011:
ffStrbufAppendS(&device->type, "LAN/Network Access point");
break;
case 0b00100:
ffStrbufAppendS(&device->type, "Audio/Video");
break;
case 0b00101:
ffStrbufAppendS(&device->type, "Peripheral");
break;
case 0b00110:
ffStrbufAppendS(&device->type, "Imaging");
break;
case 0b00111:
ffStrbufAppendS(&device->type, "Wearable");
break;
case 0b01000:
ffStrbufAppendS(&device->type, "Toy");
break;
case 0b01001:
ffStrbufAppendS(&device->type, "Health");
break;
case 0b11111:
ffStrbufAppendS(&device->type, "Uncategorized");
break;
default:
ffStrbufAppendS(&device->type, "Unknown");
break;
// "Nothing found" is not an error here: this search only ever sees BR/EDR devices, and a
// machine whose only peripheral is a Low Energy one reaches this branch with a working
// radio. The LE half below is what finds that device, so falling through is the point.
} else {
do {
if (!options->showDisconnected && !btdi.fConnected) {
continue;
}
} else {
ffStrbufTrimRight(&device->type, ' ');
ffStrbufTrimRight(&device->type, ',');
}
} while (ffBluetoothFindNextDevice(hFind, &btdi));
ffBluetoothFindDeviceClose(hFind);
FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInitWS(&device->name, btdi.szName);
ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", btdi.Address.rgBytes[5], btdi.Address.rgBytes[4], btdi.Address.rgBytes[3], btdi.Address.rgBytes[2], btdi.Address.rgBytes[1], btdi.Address.rgBytes[0]);
ffStrbufInit(&device->type);
device->deviceType = FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT;
device->battery = 0;
device->signalQuality = -DBL_MAX; // Only the LE stack can report a signal strength
device->connected = !!btdi.fConnected;
if (devices->length > 0) {
ffBluetoothDetectBattery(devices);
// https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf
if (BitTest(&btdi.ulClassofDevice, 13)) {
ffStrbufAppendS(&device->type, "Limited Discoverable Mode, ");
}
if (BitTest(&btdi.ulClassofDevice, 14)) {
ffStrbufAppendS(&device->type, "LE audio, ");
}
if (BitTest(&btdi.ulClassofDevice, 15)) {
ffStrbufAppendS(&device->type, "Reserved for future use, ");
}
if (BitTest(&btdi.ulClassofDevice, 16)) {
ffStrbufAppendS(&device->type, "Positioning, ");
}
if (BitTest(&btdi.ulClassofDevice, 17)) {
ffStrbufAppendS(&device->type, "Networking, ");
}
if (BitTest(&btdi.ulClassofDevice, 18)) {
ffStrbufAppendS(&device->type, "Rendering, ");
}
if (BitTest(&btdi.ulClassofDevice, 19)) {
ffStrbufAppendS(&device->type, "Capturing, ");
}
if (BitTest(&btdi.ulClassofDevice, 20)) {
ffStrbufAppendS(&device->type, "Object Transfer, ");
}
if (BitTest(&btdi.ulClassofDevice, 21)) {
ffStrbufAppendS(&device->type, "Audio, ");
}
if (BitTest(&btdi.ulClassofDevice, 22)) {
ffStrbufAppendS(&device->type, "Telephony, ");
}
if (BitTest(&btdi.ulClassofDevice, 23)) {
ffStrbufAppendS(&device->type, "Information, ");
}
if (device->type.length == 0) {
uint32_t majorDeviceClasses = (btdi.ulClassofDevice >> 8) & ~(UINT32_MAX << 5);
switch (majorDeviceClasses) {
case 0b00000:
ffStrbufAppendS(&device->type, "Miscellaneous");
break;
case 0b00001:
ffStrbufAppendS(&device->type, "Computer");
break;
case 0b00010:
ffStrbufAppendS(&device->type, "Phone");
break;
case 0b00011:
ffStrbufAppendS(&device->type, "LAN/Network Access point");
break;
case 0b00100:
ffStrbufAppendS(&device->type, "Audio/Video");
break;
case 0b00101:
ffStrbufAppendS(&device->type, "Peripheral");
break;
case 0b00110:
ffStrbufAppendS(&device->type, "Imaging");
break;
case 0b00111:
ffStrbufAppendS(&device->type, "Wearable");
break;
case 0b01000:
ffStrbufAppendS(&device->type, "Toy");
break;
case 0b01001:
ffStrbufAppendS(&device->type, "Health");
break;
case 0b11111:
ffStrbufAppendS(&device->type, "Uncategorized");
break;
default:
ffStrbufAppendS(&device->type, "Unknown");
break;
}
} else {
ffStrbufTrimRight(&device->type, ' ');
ffStrbufTrimRight(&device->type, ',');
}
} while (ffBluetoothFindNextDevice(hFind, &btdi));
ffBluetoothFindDeviceClose(hFind);
}
return nullptr;
}
const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
// `showType` selects which stacks are walked, not merely which entries are printed. The classic
// and the Low Energy stack are two different APIs here, so a stack the user switched off is not
// called at all.
if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) {
const char* error = detectClassic(options, devices);
if (error) {
return error;
}
}
if (options->showType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) {
// A failure here is not fatal: the LE half only adds to the classic list, which stands on
// its own, and "no LE link on this machine" is the ordinary case rather than an error.
ffBluetoothDetectLe(options, devices);
}
if (devices->length > 0) {
detectBattery(devices);
}
return nullptr;
@@ -0,0 +1,425 @@
extern "C" {
#include "bluetooth.h"
#include "common/mallocHelper.h"
#include "common/percent.h"
#include "common/strutil.h"
}
#if FF_HAVE_WINRT
#include "common/windows/winrt.hpp"
#define INITGUID
#include <windows.h>
#include <cfgmgr32.h>
#include <devpkey.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Enumeration.h>
// Same definition as the one in bluetooth_windows.c; both are `selectany` and identical, so the
// linker keeps either. It is not in the SDK headers, hence the hand-written GUID.
/* DEVPROP_TYPE_STRING */
DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_DeviceAddress, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 1);
#define GUID_DEVCLASS_BLUETOOTH_STRING L"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}" // Found in <devguid.h>
using FFBluetoothDeviceInformation = winrt::Windows::Devices::Enumeration::DeviceInformation;
using FFBluetoothDeviceInformationItf = abi_t<winrt::Windows::Devices::Enumeration::IDeviceInformation>;
using FFBluetoothDeviceList = abi_t<winrt::Windows::Foundation::Collections::IVectorView<FFBluetoothDeviceInformation>>;
using FFBluetoothProperties = abi_t<winrt::Windows::Foundation::Collections::IMapView<winrt::hstring, winrt::Windows::Foundation::IInspectable>>;
using FFBluetoothPropertyValue = abi_t<winrt::Windows::Foundation::IPropertyValue>;
// The AEP properties this file asks for. `FindAllAsync` rejects the whole call with
// `0x8002802B` (property key syntax error) if any name is not canonical, so every one of these was
// checked against the fast selector on a machine with paired LE devices; the ones that look
// plausible but do not exist (`...Bluetooth.Cod.Services`, `...Bluetooth.LastConnectedTime`,
// `...Bluetooth.Le.DeviceAddress`) are deliberately absent.
static const wchar_t* const ffBluetoothLeProperties[] = {
L"System.Devices.Aep.IsConnected",
L"System.Devices.Aep.SignalStrength",
L"System.Devices.Aep.DeviceAddress",
L"System.Devices.Aep.ContainerId",
L"System.Devices.Aep.Bluetooth.Le.Appearance",
};
// ---------------------------------------------------------------------------------------------
// Reading the AEP property store
// ---------------------------------------------------------------------------------------------
// Every value in `DeviceInformation.Properties()` is boxed, and the one box that can be read back
// without knowing the concrete type is the property-value interface. So: ask the map whether the key
// is there, then ask the box for the type that key is documented to carry.
//
// "The device does not have it" is an ordinary answer rather than a failure -- an endpoint that has
// never been in range has no SignalStrength, and one whose firmware leaves the appearance at its
// default reports 0 instead of omitting the key. The caller owns the returned value.
static FFBluetoothPropertyValue* ffBluetoothLeLookup(FFBluetoothProperties* properties, const wchar_t* key) {
HSTRING_HEADER header;
HSTRING name;
if (FAILED(WindowsCreateStringReference(key, (UINT32) ::wcslen(key), &header, &name))) {
return nullptr;
}
bool present = false; // `Lookup` on a missing key is an error, so ask first
if (FAILED(properties->HasKey(reinterpret_cast<void*>(name), &present)) || !present) {
return nullptr;
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::IInspectable>* boxed = nullptr;
if (FAILED(properties->Lookup(reinterpret_cast<void*>(name), reinterpret_cast<void**>(&boxed))) || !boxed) {
return nullptr;
}
FFBluetoothPropertyValue* value = nullptr;
if (FAILED(ffQueryInterface<winrt::Windows::Foundation::IPropertyValue>(boxed, &value))) {
return nullptr;
}
return value;
}
static bool ffBluetoothLeGetBool(FFBluetoothProperties* properties, const wchar_t* key, bool* result) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
return value && SUCCEEDED(value->GetBoolean(result));
}
static bool ffBluetoothLeGetInt32(FFBluetoothProperties* properties, const wchar_t* key, int32_t* result) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
return value && SUCCEEDED(value->GetInt32(result));
}
static bool ffBluetoothLeGetUInt16(FFBluetoothProperties* properties, const wchar_t* key, uint16_t* result) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
return value && SUCCEEDED(value->GetUInt16(result));
}
static bool ffBluetoothLeGetGuid(FFBluetoothProperties* properties, const wchar_t* key, GUID* result) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
// `winrt::guid` and `GUID` have the same four members in the same order, and C++/WinRT itself
// converts between them with exactly this cast.
return value && SUCCEEDED(value->GetGuid(reinterpret_cast<winrt::guid*>(result)));
}
// The caller owns `result`.
static bool ffBluetoothLeGetString(FFBluetoothProperties* properties, const wchar_t* key, HSTRING* result) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothPropertyValue* value = ffBluetoothLeLookup(properties, key);
return value && SUCCEEDED(value->GetString(reinterpret_cast<void**>(result)));
}
// ---------------------------------------------------------------------------------------------
// The device tree: which addresses belong to the same physical device
// ---------------------------------------------------------------------------------------------
// A Bluetooth device is one container with one node per stack it speaks, and the two nodes carry
// *different* addresses: the phone is FC:AB:D0:72:40:90 over BR/EDR and 47:E1:D7:21:C7:FC over LE,
// both inside {B3F019F1-...}. The container is therefore the only identity the classic and the LE
// enumerations can be joined on, and the device tree is where the classic side gets it from.
typedef struct FFBluetoothNodeInfo {
char address[13]; // 12 hex digits + terminator, upper case, no separators
GUID container;
} FFBluetoothNodeInfo;
static void ffBluetoothCollectNodes(FFlist* nodes /* FFBluetoothNodeInfo */) {
ULONG idListLength = 0;
if (CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_BLUETOOTH_STRING, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS || idListLength == 0) {
return;
}
FF_AUTO_FREE wchar_t* idList = (wchar_t*) malloc((size_t) idListLength * sizeof(wchar_t));
if (!idList) {
return;
}
if (CM_Get_Device_ID_ListW(GUID_DEVCLASS_BLUETOOTH_STRING, idList, idListLength, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS) {
return;
}
for (const wchar_t* deviceId = idList; *deviceId; deviceId += wcslen(deviceId) + 1) {
DEVINST devInst = 0;
if (CM_Locate_DevNodeW(&devInst, (DEVINSTID_W) deviceId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) {
continue;
}
FFBluetoothNodeInfo node = {};
{
// Both enumerators carry this: BTHENUM nodes spell the address in upper case, BTHLE nodes
// in lower case, so it is folded here and the comparison later is plain byte equality.
wchar_t address[13];
DEVPROPTYPE type = DEVPROP_TYPE_EMPTY;
ULONG size = sizeof(address);
if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_DeviceAddress, &type, (PBYTE) address, &size, 0) != CR_SUCCESS || type != DEVPROP_TYPE_STRING || size != sizeof(address)) {
continue;
}
for (size_t i = 0; i < 12; ++i) {
node.address[i] = (char) toupper((unsigned char) address[i]);
}
}
{
DEVPROPTYPE type = DEVPROP_TYPE_EMPTY;
ULONG size = sizeof(node.container);
if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Device_ContainerId, &type, (PBYTE) &node.container, &size, 0) != CR_SUCCESS || type != DEVPROP_TYPE_GUID || size != sizeof(node.container)) {
continue;
}
}
*FF_LIST_ADD(FFBluetoothNodeInfo, *nodes) = node;
}
}
// `address` is the printed form ("AA:BB:CC:DD:EE:FF"); the device tree stores it without separators.
static bool ffBluetoothFindContainer(const FFlist* nodes, const FFstrbuf* address, GUID* container) {
if (address->length != 17) {
return false;
}
char plain[13];
for (uint32_t i = 0; i < 6; ++i) {
plain[i * 2] = (char) toupper((unsigned char) address->chars[i * 3]);
plain[i * 2 + 1] = (char) toupper((unsigned char) address->chars[i * 3 + 1]);
}
plain[12] = '\0';
FF_LIST_FOR_EACH (FFBluetoothNodeInfo, node, *nodes) {
if (memcmp(node->address, plain, sizeof(plain)) == 0) {
*container = node->container;
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------------------------
// The appearance, and the device it describes
// ---------------------------------------------------------------------------------------------
// The GAP appearance is a 16-bit value whose upper 10 bits are the category. Windows names only the
// first nineteen of them -- `BluetoothLEAppearanceCategories` stops at Cycling (0x12) and has no
// AudioSink -- so these names are the Bluetooth SIG ones, checked against that class where it has an
// entry and against `BluetoothLEAppearance::FromRawValue()` for the rest: a pair of earbuds reports
// 0x0842, which the OS itself decodes as category 0x21, the value used below for Audio Sink.
static const char* ffBluetoothLeAppearanceName(uint16_t appearance) {
switch (appearance >> 6) {
case 0x00: return "Unknown";
case 0x01: return "Phone";
case 0x02: return "Computer";
case 0x03: return "Watch";
case 0x04: return "Clock";
case 0x05: return "Display";
case 0x06: return "Remote Control";
case 0x07: return "Eye Glasses";
case 0x08: return "Tag";
case 0x09: return "Keyring";
case 0x0A: return "Media Player";
case 0x0B: return "Barcode Scanner";
case 0x0C: return "Thermometer";
case 0x0D: return "Heart Rate Sensor";
case 0x0E: return "Blood Pressure";
case 0x0F: return "Human Interface Device";
case 0x10: return "Glucose Meter";
case 0x11: return "Running Walking Sensor";
case 0x12: return "Cycling";
case 0x13: return "Control Device";
case 0x14: return "Network Device";
case 0x15: return "Sensor";
case 0x16: return "Light Fixture";
case 0x17: return "Fan";
case 0x18: return "HVAC";
case 0x19: return "Air Conditioning";
case 0x1A: return "Humidifier";
case 0x1B: return "Heating";
case 0x1C: return "Access Control";
case 0x1D: return "Motorized Device";
case 0x1E: return "Power Device";
case 0x1F: return "Light Source";
case 0x20: return "Window Covering";
case 0x21: return "Audio Sink";
case 0x22: return "Audio Source";
case 0x23: return "Motorized Vehicle";
case 0x24: return "Domestic Appliance";
case 0x25: return "Wearable Audio Device";
case 0x26: return "Aircraft";
case 0x27: return "AV Equipment";
case 0x28: return "Display Equipment";
case 0x29: return "Hearing Aid";
case 0x2A: return "Gaming";
case 0x2B: return "Signage";
default: return "Unknown";
}
}
// Which entry of `devices` each classic device occupies, and which container it belongs to. The
// index is stored rather than a pointer because appending an LE-only device can move the list.
typedef struct FFBluetoothClassicEntry {
uint32_t index;
bool hasContainer;
GUID container;
} FFBluetoothClassicEntry;
// ---------------------------------------------------------------------------------------------
// The LE stack
// ---------------------------------------------------------------------------------------------
// `devices` already holds everything the classic (BR/EDR) stack knows. This adds what only the LE
// stack knows: devices that never speak BR/EDR at all, and -- for the ones that speak both -- the
// fact that they do, plus the signal strength the LE link is the only source of.
//
// The way in matters. `BluetoothLEDevice::GetDeviceSelectorFromPairingState(true)` as an AQS filter
// answers in 9-22 ms and brings the AEP properties along, whereas enumerating
// `DeviceInformationKind::AssociationEndpoint` returns the same data but costs about 61 seconds per
// call because it scans the radio. The 7.7-second trap is one layer further out: any `Uncached` GATT
// call, which the cache-backed service table hides by returning in a few milliseconds.
extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
const char* error = ffInitCom();
if (error) {
return error;
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>* ledeviceStatics = nullptr;
if (FAILED(ffGetActivationFactory<winrt::Windows::Devices::Bluetooth::IBluetoothLEDeviceStatics2>(L"Windows.Devices.Bluetooth.BluetoothLEDevice", &ledeviceStatics)) || !ledeviceStatics) {
return "winrt: RoGetActivationFactory(BluetoothLEDevice) failed";
}
[[gnu::cleanup(ffDeleteHstring)]] HSTRING selector = nullptr;
if (FAILED(ledeviceStatics->GetDeviceSelectorFromPairingState(true, reinterpret_cast<void**>(&selector))) || !selector) {
return "winrt: GetDeviceSelectorFromPairingState() failed";
}
// Without additional properties the map carries only the eight generic ones, and every AEP key
// is absent, so the list is not optional.
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Foundation::Collections::IIterable<winrt::hstring>>* properties = nullptr;
if (FAILED(ffWinrtCreateHstringIterable(ffBluetoothLeProperties, (uint32_t) ARRAY_SIZE(ffBluetoothLeProperties), &properties)) || !properties) {
return "winrt: building the additional-properties list failed";
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>* enumerationStatics = nullptr;
if (FAILED(ffGetActivationFactory<winrt::Windows::Devices::Enumeration::IDeviceInformationStatics>(L"Windows.Devices.Enumeration.DeviceInformation", &enumerationStatics)) || !enumerationStatics) {
return "winrt: RoGetActivationFactory(DeviceInformation) failed";
}
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothDeviceList* found = nullptr;
HRESULT hr = ffRunAndWait<winrt::Windows::Foundation::Collections::IVectorView<FFBluetoothDeviceInformation>>([&](void** result) {
return enumerationStatics->FindAllAsyncAqsFilterAndAdditionalProperties(reinterpret_cast<void*>(selector), reinterpret_cast<void*>(properties), result);
},
&found);
if (FAILED(hr) || !found) {
return "winrt: DeviceInformation::FindAllAsync() failed";
}
uint32_t count = 0;
if (FAILED(found->get_Size(&count)) || count == 0) {
return nullptr;
}
FF_LIST_AUTO_DESTROY nodes = ffListCreate();
ffBluetoothCollectNodes(&nodes);
FF_LIST_AUTO_DESTROY classic = ffListCreate();
{
uint32_t index = 0;
FF_LIST_FOR_EACH (FFBluetoothResult, device, *devices) {
FFBluetoothClassicEntry* entry = FF_LIST_ADD(FFBluetoothClassicEntry, classic);
entry->index = index++;
entry->hasContainer = ffBluetoothFindContainer(&nodes, &device->address, &entry->container);
}
}
for (uint32_t i = 0; i < count; ++i) {
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothDeviceInformationItf* info = nullptr;
if (FAILED(found->GetAt(i, reinterpret_cast<void**>(&info))) || !info) {
continue;
}
FF_AUTO_RELEASE_COM_OBJECT FFBluetoothProperties* map = nullptr;
if (FAILED(info->get_Properties(reinterpret_cast<void**>(&map))) || !map) {
continue;
}
GUID container = {};
if (!ffBluetoothLeGetGuid(map, L"System.Devices.Aep.ContainerId", &container)) {
continue; // Without a container there is no way to tell whether this is a device we know
}
bool connected = false;
ffBluetoothLeGetBool(map, L"System.Devices.Aep.IsConnected", &connected);
FFBluetoothResult* target = nullptr;
FF_LIST_FOR_EACH (FFBluetoothClassicEntry, entry, classic) {
if (entry->hasContainer && IsEqualGUID(entry->container, container)) {
target = FF_LIST_GET(FFBluetoothResult, *devices, entry->index);
break;
}
}
if (!target && !connected && !options->showDisconnected) {
continue; // An LE-only device nobody asked to see. A known one is already in the list.
}
if (!target) {
target = FF_LIST_ADD(FFBluetoothResult, *devices);
ffStrbufInit(&target->name);
ffStrbufInit(&target->address);
ffStrbufInit(&target->type);
target->deviceType = FF_BLUETOOTH_DEVICE_TYPE_NONE;
target->battery = 0;
target->signalQuality = -DBL_MAX;
target->connected = false;
}
target->deviceType = (FFBluetoothDeviceType) (target->deviceType | FF_BLUETOOTH_DEVICE_TYPE_LE_BIT);
// The classic half wins wherever it has an answer: its name and address are the ones the
// Settings app shows, and its type comes from the class of device rather than from an
// appearance value the firmware may never have set.
if (target->name.length == 0) {
[[gnu::cleanup(ffDeleteHstring)]] HSTRING name = nullptr;
if (SUCCEEDED(info->get_Name(reinterpret_cast<void**>(&name))) && name) {
ffStrbufSetHstring(&target->name, name);
}
ffStrbufTrimSpace(&target->name); // A device whose name is missing is reported as a space
}
if (target->name.length == 0) {
ffStrbufSetStatic(&target->name, "Unknown Device");
}
if (target->address.length == 0) {
[[gnu::cleanup(ffDeleteHstring)]] HSTRING address = nullptr;
if (ffBluetoothLeGetString(map, L"System.Devices.Aep.DeviceAddress", &address)) {
ffStrbufSetHstring(&target->address, address);
ffStrbufUpperCase(&target->address);
}
}
if (target->type.length == 0) {
uint16_t appearance = 0;
if (ffBluetoothLeGetUInt16(map, L"System.Devices.Aep.Bluetooth.Le.Appearance", &appearance)) {
ffStrbufSetStatic(&target->type, ffBluetoothLeAppearanceName(appearance));
}
}
// The signal strength is what the LE stack last saw, and it stays readable while the link is
// down, so it is reported whenever the endpoint has one. Only the LE side can produce it.
int32_t signalStrength = 0;
if (ffBluetoothLeGetInt32(map, L"System.Devices.Aep.SignalStrength", &signalStrength)) {
target->signalQuality = ffRssiToSignalQuality(signalStrength);
}
// A dual-mode device is connected if either of its two links is up, and the two can disagree.
target->connected |= connected;
}
return nullptr;
}
#else
extern "C" const char* ffBluetoothDetectLe(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) {
FF_UNUSED(options, devices);
return nullptr;
}
#endif // FF_HAVE_WINRT
+1 -1
View File
@@ -77,9 +77,9 @@ static const char* detectAllocation(FFBtrfsResult* item, int dfd, FFstrbuf* buff
}
if (ffReadFileBufferRelative(subfd, "global_rsv_reserved", buffer)) {
// global_rsv_reserved is already the used part of the reservation, global_rsv_size its total
item->globalReservationUsed = ffStrbufToUInt(buffer, 0);
}
item->globalReservationUsed = item->globalReservationTotal - item->globalReservationUsed;
#define FF_BTRFS_DETECT_PROFILE(_index, _type, _profile, _copies) \
else if (faccessat(subfd, _type "/" _profile "/", F_OK, 0) == 0) { \
+1
View File
@@ -37,6 +37,7 @@ const char* ffDetectCamera(FFlist* result)
ffStrbufInitS(&camera->name, device.localizedName.UTF8String);
ffStrbufInitS(&camera->vendor, device.manufacturer.UTF8String);
ffStrbufInitS(&camera->id, device.uniqueID.UTF8String);
ffStrbufInit(&camera->colorspace);
switch (device.activeColorSpace)
{
case AVCaptureColorSpace_sRGB: ffStrbufInitStatic(&camera->colorspace, "sRGB"); break;
+1 -1
View File
@@ -1,5 +1,5 @@
#include "camera.h"
const char* ffDetectCamera([[maybe_unused]] FFlist* result) {
return "Not support on this platform";
return "Not supported on this platform";
}
+7 -1
View File
@@ -42,7 +42,13 @@ const char* ffDetectCommand(FFCommandOptions* options, FFstrbuf* result) {
if (!options->parallel) {
bundle.error = spawnProcess(options, &bundle.handle);
} else if (!FF_LIST_SHIFT(commandQueue, &bundle)) {
return "[BUG] command queue is empty";
// The module was printed without having been prepared first, so there is no process to collect.
// That is the expected outcome for a module gated on `condition.succeeded`: whether it is
// printed at all is only known in the print pass, so the prepare pass skips it (see
// `printJsonConfig`). In every other case the queue and the printed modules must line up.
return "Module was not prepared, so it cannot run in parallel. Modules gated on "
"`condition.succeeded` are never prepared, which is the expected cause; "
"anything else is a bug in fastfetch, please report it";
}
if (bundle.error) {
+63 -41
View File
@@ -706,6 +706,68 @@ static bool detectFrequency(FFCPUResult* cpu, const FFCPUOptions* options) {
return true;
}
// Also used in tests/cpu.c
bool ffCPUQualcommSnapdragonToName(FFstrbuf* name, const char* model) {
// X2 device trees use platform codenames rather than exact retail part
// numbers. qcom,glymur is used by X2 Elite Extreme boards, qcom,mahua by
// X2 Elite boards, and qcom,kalambo by X2 Plus boards. Do not append a
// part number from /sys/bus/soc/devices/soc0/soc_id: it contains
// SMEM_HW_SW_BUILD_ID.id (the platform ID), 662 is unnamed in the public
// qcom,ids.h, and one compatible can cover multiple retail SKUs.
// https://github.com/fastfetch-cli/fastfetch/issues/2611
if (ffStrEquals(model, "glymur")) {
ffStrbufSetS(name, "Qualcomm Snapdragon X2 Elite Extreme");
return true;
}
if (ffStrEquals(model, "mahua")) {
ffStrbufSetS(name, "Qualcomm Snapdragon X2 Elite");
return true;
}
if (ffStrEquals(model, "kalambo")) {
ffStrbufSetS(name, "Qualcomm Snapdragon X2 Plus");
return true;
}
// SoC models are enumerated in the form of `x<gen>[e|p]<part><rev>`, e.g. `x1e80100`,
// which is marketed as `X1E-80-100`, where `e` stands for Elite and `p` for Plus.
// https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips#Snapdragon_X_series
const char* prefix;
uint32_t prefixLen;
if (ffStrStartsWith(model, "x1e")) {
prefix = "Qualcomm Snapdragon X Elite";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x1p")) {
prefix = "Qualcomm Snapdragon X Plus";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x2e")) {
// Only X2E-94-100 and X2E-96-100 are branded as Extreme
bool extreme = ffStrStartsWith(model, "x2e94") || ffStrStartsWith(model, "x2e96");
prefix = extreme ? "Qualcomm Snapdragon X2 Elite Extreme" : "Qualcomm Snapdragon X2 Elite";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x2p")) {
prefix = "Qualcomm Snapdragon X2 Plus";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x1")) {
prefix = "Qualcomm Snapdragon X";
prefixLen = 2;
} else {
return false;
}
uint32_t length = (uint32_t) strlen(model);
bool splitCode = length - prefixLen == 5;
ffStrbufSetF(name, "%s X", prefix);
for (uint32_t i = 1; i < length; ++i) {
if (i == prefixLen || (splitCode && i == length - 3)) {
ffStrbufAppendC(name, '-');
}
ffStrbufAppendC(name, (char) toupper(model[i]));
}
return true;
}
#if __i386__ || __x86_64__
[[maybe_unused]] static uint16_t getPackageCount(FFstrbuf* cpuinfo) {
@@ -840,46 +902,6 @@ static const char* detectPhysicalCores(FFCPUResult* cpu) {
return nullptr;
}
[[maybe_unused]] static bool detectSnapdragonX(FFstrbuf* name, const char* model) {
// SoC models are enumerated in the form of `x<gen>[e|p]<part><rev>`, e.g. `x1e80100`,
// which is marketed as `X1E-80-100`, where `e` stands for Elite and `p` for Plus.
// https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips#Snapdragon_X_series
const char* prefix;
uint32_t prefixLen;
if (ffStrStartsWith(model, "x1e")) {
prefix = "Qualcomm Snapdragon X Elite";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x1p")) {
prefix = "Qualcomm Snapdragon X Plus";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x2e")) {
// Only X2E-94-100 and X2E-96-100 are branded as Extreme
prefix = model[3] == '9' ? "Qualcomm Snapdragon X2 Elite Extreme" : "Qualcomm Snapdragon X2 Elite";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x2p")) {
prefix = "Qualcomm Snapdragon X2 Plus";
prefixLen = 3;
} else if (ffStrStartsWith(model, "x1")) {
prefix = "Qualcomm Snapdragon X";
prefixLen = 2;
} else {
return false;
}
uint32_t length = (uint32_t) strlen(model);
bool splitCode = length - prefixLen == 5;
ffStrbufSetF(name, "%s X", prefix);
for (uint32_t i = 1; i < length; ++i) {
if (i == prefixLen || (splitCode && i == length - 3)) {
ffStrbufAppendC(name, '-');
}
ffStrbufAppendC(name, (char) toupper(model[i]));
}
return true;
}
[[maybe_unused]] static void parseIsa(FFstrbuf* cpuIsa) {
// Always use the last part of the ISA string. Ref: #590 #1204
ffStrbufSubstrAfterLastC(cpuIsa, ' ');
@@ -982,7 +1004,7 @@ static const char* detectPhysicalCores(FFCPUResult* cpu) {
#endif
else if (ffStrEquals(vendor, "qcom")) {
// https://elixir.bootlin.com/linux/latest/source/arch/arm64/boot/dts/qcom
if (!detectSnapdragonX(&cpu->name, model)) {
if (!ffCPUQualcommSnapdragonToName(&cpu->name, model)) {
if (ffStrStartsWith(model, "sc")) {
const char* code = model + 2;
uint32_t deviceId = (uint32_t) strtoul(code, nullptr, 10);
-5
View File
@@ -1,5 +0,0 @@
#include "cpu.h"
const char* ffDetectCPUImpl([[maybe_unused]] const FFCPUOptions* options, [[maybe_unused]] FFCPUResult* cpu) {
return "Not supported on this platform";
}
@@ -1,6 +0,0 @@
#include "fastfetch.h"
#include "detection/cpuusage/cpuusage.h"
const char* ffGetCpuUsageInfo([[maybe_unused]] FFlist* cpuTimes) {
return "Not support on this platform";
}
+4 -1
View File
@@ -248,6 +248,9 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti
getKDE(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_GNOME)) {
getGnome(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_GNOME_CLASSIC)) {
// GNOME Classic is GNOME Shell in classic mode, so gnome-shell reports the same version
getGnome(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_CINNAMON)) {
getCinnamon(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_XFCE4)) {
@@ -260,7 +263,7 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti
getBudgie(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_UNITY)) {
getUnity(result, options);
} else if (ffStrbufEqualS(deName, "trinity")) {
} else if (ffStrbufIgnCaseEqualS(deName, "trinity") || ffStrbufIgnCaseEqualS(deName, "tde")) {
getTrinity(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_COSMIC)) {
getCosmic(result, options);
-5
View File
@@ -1,5 +0,0 @@
#include "disk.h"
const char* ffDetectDisksImpl([[maybe_unused]] FFDiskOptions* options, [[maybe_unused]] FFlist* disks) {
return "Not supported on this platform";
}
+42 -14
View File
@@ -1,4 +1,5 @@
#include "displayserver.h"
#include "common/FFcache.h"
FFDisplayResult* ffdsAppendDisplay(
FFDisplayServerResult* result,
@@ -50,18 +51,45 @@ FFDisplayResult* ffdsAppendDisplay(
void ffConnectDisplayServerImpl(FFDisplayServerResult* ds);
const FFDisplayServerResult* ffConnectDisplayServer() {
static FFDisplayServerResult result;
static bool initialized = false;
if (!initialized) {
initialized = true;
ffStrbufInit(&result.wmProcessName);
ffStrbufInit(&result.wmPrettyName);
ffStrbufInit(&result.wmProtocolName);
ffStrbufInit(&result.deProcessName);
ffStrbufInit(&result.dePrettyName);
ffListInit(&result.displays);
ffConnectDisplayServerImpl(&result);
}
return &result;
static FFDisplayServerResult result;
static void initDisplayServerResult(void* storage) {
FFDisplayServerResult* ds = storage;
ffStrbufInit(&ds->wmProcessName);
ffStrbufInit(&ds->wmPrettyName);
ffStrbufInit(&ds->wmProtocolName);
ffStrbufInit(&ds->deProcessName);
ffStrbufInit(&ds->dePrettyName);
ffListInit(&ds->displays);
ffConnectDisplayServerImpl(ds);
}
static void destroyDisplayServerResult(void* storage) {
FFDisplayServerResult* ds = storage;
ffStrbufDestroy(&ds->wmProcessName);
ffStrbufDestroy(&ds->wmPrettyName);
ffStrbufDestroy(&ds->wmProtocolName);
ffStrbufDestroy(&ds->deProcessName);
ffStrbufDestroy(&ds->dePrettyName);
// Every display owns its name and serial, which are lost if the list is dropped as a whole
FF_LIST_FOR_EACH (FFDisplayResult, display, ds->displays) {
ffStrbufDestroy(&display->name);
ffStrbufDestroy(&display->serial);
}
ffListDestroy(&ds->displays);
}
static FFcacheEntry ffCacheEntryDisplayServer = {
.name = "displayServer",
.storage = &result,
.init = initDisplayServerResult,
.destroy = destroyDisplayServerResult,
};
const FFDisplayServerResult* ffConnectDisplayServer() {
return ffCacheGet(&ffCacheEntryDisplayServer);
}
+5 -1
View File
@@ -46,13 +46,17 @@ const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) {
}
for (IP_ADAPTER_DNS_SERVER_ADDRESS_XP* ifa = adapter->FirstDnsServerAddress; ifa; ifa = ifa->Next) {
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
// The element must only be added once it can be filled: `FF_LIST_ADD` does not zero the
// new element, so an unrecognized address family used to leave an uninitialized FFstrbuf
// in the list, which the callers then read and free
if (ifa->Address.lpSockaddr->sa_family == AF_INET) {
SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr;
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
ffStrbufInitA(item, INET_ADDRSTRLEN);
item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4->sin_addr, item->chars) - item->chars);
} else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) {
SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr;
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
ffStrbufInitA(item, INET6_ADDRSTRLEN);
item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6->sin6_addr, item->chars) - item->chars);
}
+36 -12
View File
@@ -32,17 +32,33 @@ static bool extractNanoVersionFromBinary(const char* str, [[maybe_unused]] uint3
return false;
}
// The message is printed to the user verbatim, so it has to name the variable that was wrong:
// `"Executable not found"` from `ffFindExecutableInPath()` doesn't say what was searched for.
static const char* notAnExecutable(bool isVisual) {
return isVisual ? "$VISUAL does not point to an executable" : "$EDITOR does not point to an executable";
}
const char* ffDetectEditor(FFEditorResult* result) {
bool isVisual = true;
ffStrbufSetS(&result->name, getenv("VISUAL"));
if (result->name.length) {
result->type = "Visual";
} else {
if (!result->name.length) {
isVisual = false;
ffStrbufSetS(&result->name, getenv("EDITOR"));
if (result->name.length) {
result->type = "Editor";
} else {
return "$VISUAL or $EDITOR not set";
}
}
if (!result->name.length) {
return "$VISUAL or $EDITOR not set";
}
result->type = isVisual ? "Visual" : "Editor";
// `$VISUAL`/`$EDITOR` hold a shell command line rather than a bare name, so they routinely
// carry arguments: `EDITOR="code -w"`, `EDITOR="vim -f"`. Only the first word names the
// program. The whole string used to be handed to `ffFindExecutableInPath()`, which could never
// match it, and the module then printed the unparsed value as if it had been resolved.
ffStrbufSubstrBefore(&result->name, (uint32_t) strcspn(result->name.chars, " \t\n\v\f\r"));
if (result->name.length == 0) {
return notAnExecutable(isVisual);
}
if (ffIsAbsolutePath(result->name.chars)) {
@@ -50,14 +66,19 @@ const char* ffDetectEditor(FFEditorResult* result) {
} else {
const char* error = ffFindExecutableInPath(result->name.chars, &result->path);
if (error) {
return nullptr;
// Reporting success here is what made `EDITOR="code -w"` print the raw value: the module
// reads `nullptr` as "detection succeeded" and then falls back to printing `name`.
return notAnExecutable(isVisual);
}
}
{
char buf[PATH_MAX + 1];
if (!realpath(result->path.chars, buf)) {
return nullptr;
// An absolute `$VISUAL`/`$EDITOR` is copied into `path` before it is verified, so a path
// that does not exist has to be reported here instead of being handed to the caller as
// if it had been checked.
return notAnExecutable(isVisual);
}
// WIN32: Should we handle scoop shim exe here?
@@ -77,11 +98,13 @@ const char* ffDetectEditor(FFEditorResult* result) {
#endif
);
if (index == result->path.length) {
return nullptr;
// `realpath()` always yields an absolute path, so neither of these two is reachable in
// practice. They used to `return nullptr`, which the module read as a success.
return "Failed to determine the executable name";
}
ffStrbufSetS(&result->exe, &result->path.chars[index + 1]);
if (!result->exe.length) {
return nullptr;
return "Failed to determine the executable name";
}
#ifdef _WIN32
@@ -92,6 +115,7 @@ const char* ffDetectEditor(FFEditorResult* result) {
}
if (!instance.config.general.detectVersion) {
// Not an error: the editor was found, version probing was just turned off.
return nullptr;
}
-7
View File
@@ -1,7 +0,0 @@
#include "fastfetch.h"
#include "font.h"
const char* ffDetectFontImpl([[maybe_unused]] FFFontResult* result) {
FF_UNUSED(result);
return "Not supported on this platform";
}
-48
View File
@@ -1,48 +0,0 @@
#include "gamepad.h"
#include "common/apple/cf_helpers.h"
#include "common/mallocHelper.h"
#include <IOKit/IOKitLib.h>
#include <IOKit/hid/IOHIDLib.h>
static void enumSet(IOHIDDeviceRef value, FFlist* results) {
FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *results);
ffStrbufInit(&device->serial);
ffStrbufInit(&device->name);
device->battery = 0;
CFStringRef manufacturer = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDManufacturerKey));
ffCfStrGetString(manufacturer, &device->name);
CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey));
if (device->name.length) {
ffCfStrGetString(product, &device->serial);
ffStrbufAppendC(&device->name, ' ');
ffStrbufAppend(&device->name, &device->serial);
} else {
ffCfStrGetString(product, &device->name);
}
CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey));
ffCfStrGetString(serialNumber, &device->serial);
}
const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
FF_CFTYPE_AUTO_RELEASE IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
return "IOHIDManagerOpen() failed";
}
FF_CFTYPE_AUTO_RELEASE CFDictionaryRef matching1 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_Joystick) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
FF_CFTYPE_AUTO_RELEASE CFDictionaryRef matching2 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_GamePad) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
FF_CFTYPE_AUTO_RELEASE CFArrayRef matchings = CFArrayCreate(kCFAllocatorDefault, (const void**) (CFTypeRef[]) { matching1, matching2 }, 2, &kCFTypeArrayCallBacks);
IOHIDManagerSetDeviceMatchingMultiple(manager, matchings);
FF_CFTYPE_AUTO_RELEASE CFSetRef set = IOHIDManagerCopyDevices(manager);
if (set) {
CFSetApplyFunction(set, (CFSetApplierFunction) &enumSet, devices);
}
IOHIDManagerClose(manager, kIOHIDOptionsTypeNone);
return nullptr;
}
+318
View File
@@ -0,0 +1,318 @@
#include "gamepad.h"
#include "common/apple/cf_helpers.h"
#include <IOKit/IOKitLib.h>
#include <IOKit/hid/IOHIDLib.h>
#include <GameController/GameController.h>
#include <math.h>
// macOS offers two overlapping views of the same controller, and neither replaces the other:
//
// * `IOHIDManager` sees every HID device whose primary usage is a joystick or a gamepad, third
// party pads included. It is the only source of the manufacturer, the product and the serial
// number.
// * `GCController` is the GameController framework's view of the pads Apple has whitelisted -- the
// official Sony, Microsoft, Nintendo and MFi ones. It is the only source of a battery level
// (`GCDeviceBattery`, macOS 11.0+), and it is what the system's own games read.
//
// Neither view replaces the other, so each contributes whole entries rather than a name from one and
// a battery from the other. Nothing on the public surface can pair them: `GCDevice` carries only the
// display strings `vendorName` and `productCategory`, `+[GCController supportsHIDDevice:]` answers in
// the other direction only, and SDL -- the reference implementation for all of this -- does not pair
// them either ("we don't have an easy way to know if those devices correspond to a specific
// GCController", `SDL_mfijoystick.m`).
//
// The serial is the one field both views carry, so it is also the one field that would otherwise be
// spelled two different ways for the same pad depending on which pass contributed the entry. It is
// joined through a private property -- see the `GCController()` category below for what was measured
// -- and the join only *borrows* the serial. The entries themselves stay separate.
//
// ⚠️ `[GCController controllers]` is filled in by the private `_GCControllerManager` once it is
// told that the application became active, which a process that never activates -- a command line
// tool -- never tells it. SDL records the symptom for macOS 11.0.1 ("this always returns an empty
// array"). The array may therefore legitimately be empty here, so the IOHID pass only skips a
// device when the framework actually contributed something: with an empty array there is no
// duplicate to avoid, and skipping would drop the device from the output altogether.
//
// The array also does not fill on its own. Measured on this machine with a Pro Controller attached:
// it reads 0 at process start, is *still* 0 after 20 ms of unrelated work with no run loop, and
// only becomes non-empty ~10.7 ms after the first `CFRunLoopRunInMode` call -- the request is sent
// by the run loop turn, not by the property read. Since nothing else in this file pumps, waiting for
// the framework has to be explicit, which is what `GAMEPAD_GAMECONTROLLER_TIMEOUT_SECONDS` and the
// pump loop in `findGameControllerDevices()` are for. Without them this whole pass contributes
// nothing and macOS never shows a battery.
//
// Merely reading `GCController.controllers` is what starts the framework -- it connects to
// `gamecontrollerd` on first touch -- so that read is *not* hoisted to the top of
// `ffDetectGamepad()`. It is therefore paid only on the path where the framework has something to
// answer for. Measured on this machine with no controller attached, module time via `--stat`:
//
// nothing hoisted ................. 0.3 ms
// the read only ................... 8.3 ms
// the read plus the run loop turn . 14.3 ms
//
// Both halves are expensive and neither is optional: the read is the ignition and the turn is what
// carries the request. The turn is nominally bounded by 0.5 ms, but `returnAfterSourceHandled` does
// not bound the handler, so it adds ~6 ms of its own. Paying 14 ms on a machine with no pad at all
// -- to learn what `supportsHIDDevice:` below already answered -- is why the kick is gone rather
// than merely deferred.
#define GAMEPAD_GAMECONTROLLER_TIMEOUT_SECONDS 0.05
@interface GCController()
// Not in the public header, and the only identity the framework exposes at all: the public
// `GCDevice` surface has just the two display strings, so two pads of the same model are
// indistinguishable through it -- no vendor id, no product id, no serial.
//
// On the Bluetooth pad this was measured with, the description is the transport address, which is
// the very string IOKit reports as `kIOHIDSerialNumberKey`:
//
// GCController.identifier LOGICAL_DEVICE(5c-52-1e-88-7e-70)
// IOHIDDevice serial 5C:52:1E:88:7E:70
//
// `persistentIdentifier` answered the same string on that pad and is deliberately not used: its
// name promises stability across launches, which is a different contract from "the address this
// pad has right now". `physicalDeviceUniqueID` is nil there, and `deviceHash` is a per-process
// hash, so neither is something that could be looked up in the IOKit set.
@property (readonly, nonatomic) NSString* identifier;
@end
// ---------------------------------------------------------------------------------------------
// Joining the two views
// ---------------------------------------------------------------------------------------------
//
// The serial is borrowed rather than guessed. The framework's own spelling of the address is the join
// key, and both sides are normalised before they are compared.
//
// Only that one pad has been measured: the wrapper format is private, so a pad that reports its
// identity in some other shape -- a wired pad with a real serial number, say -- fails to match. That
// is the intended failure mode, and it is why this only ever fills in a field that would otherwise be
// empty: no match means an empty serial, never a wrong one.
// `LOGICAL_DEVICE(5c-52-1e-88-7e-70)` -> `5c-52-1e-88-7e-70`. Anything without a parenthesised part
// is rejected outright rather than guessed at, because the wrapper is private formatting that can
// change without notice.
static bool extractIdentityToken(NSString* identifier, FFstrbuf* result) {
// The property is declared opaque in the framework's metadata -- `NSObject<NSCopying,
// NSSecureCoding>` -- but the value is a plain string, so it is used as-is: measured on this pad
// the object is an `__NSCFString` inheriting `NSMutableString`, and `description` returns the
// same 33 characters. Anything that is not a string is rejected rather than stringified, because
// the wrapper format is private either way and a non-string value would mean it changed.
if (![identifier isKindOfClass: NSString.class]) {
return false;
}
NSRange open = [identifier rangeOfString: @"("];
if (open.location == NSNotFound) {
return false;
}
NSRange close = [identifier rangeOfString: @")" options: 0 range: NSMakeRange(open.location + 1, identifier.length - open.location - 1)];
if (close.location == NSNotFound) {
return false;
}
NSString* token = [identifier substringWithRange: NSMakeRange(open.location + 1, close.location - open.location - 1)];
ffStrbufSetS(result, token.UTF8String);
return result->length > 0;
}
// Drop every separator and fold to upper case, so `5c-52-1e-88-7e-70` and `5C:52:1E:88:7E:70` -- the
// two spellings of one address above -- compare equal.
static void stripIdentitySeparators(FFstrbuf* strbuf) {
uint32_t out = 0;
for (uint32_t i = 0; i < strbuf->length; ++i) {
char c = strbuf->chars[i];
if (c >= '0' && c <= '9') {
strbuf->chars[out++] = c;
} else if (c >= 'a' && c <= 'f') {
strbuf->chars[out++] = (char) (c - 'a' + 'A');
} else if (c >= 'A' && c <= 'F') {
strbuf->chars[out++] = c;
}
}
strbuf->chars[out] = '\0';
strbuf->length = out;
}
// The IOKit device the framework is describing, or `nullptr` when the two cannot be matched. The
// returned ref is owned by `set`, which the caller holds for the rest of the call.
static IOHIDDeviceRef findHIDDeviceForController(GCController* controller, NSSet* set) {
if (set == nil || ![controller respondsToSelector: @selector(identifier)]) {
return nullptr;
}
FF_STRBUF_AUTO_DESTROY identity = ffStrbufCreate();
if (!extractIdentityToken(controller.identifier, &identity)) {
return nullptr;
}
stripIdentitySeparators(&identity);
for (id value in set) {
IOHIDDeviceRef device = (__bridge IOHIDDeviceRef) value;
FF_STRBUF_AUTO_DESTROY serial = ffStrbufCreate();
ffCfStrGetString(IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey)), &serial);
stripIdentitySeparators(&serial);
if (serial.length > 0 && ffStrbufEqual(&serial, &identity)) {
return device;
}
}
return nullptr;
}
typedef struct FFFindGamepadContext {
FFlist* devices;
uint32_t gameControllerCount;
} 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) {
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
// caller checks `supportsHIDDevice:` first, so a pad the framework does not claim never
// reaches this wait.
//
// The turn is handed the time that is left rather than 0, so it blocks until the framework's
// source fires or the deadline passes. A zero timeout returns as soon as the run loop has
// nothing pending, which turns this into a busy spin: measured with no pad and the loop
// forced to run its full timeout, `CFRunLoopRunInMode(mode, 0, true)` burned 50.4 ms of CPU
// in 50.0 ms of wall time (30k-40k turns), where passing the remaining time burned 0.08 ms
// in a single turn. The deadline is a soft bound either way -- the blocking turn overshot it
// by 3-5 ms in the same measurement.
NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow: GAMEPAD_GAMECONTROLLER_TIMEOUT_SECONDS];
while (GCController.controllers.count == 0 && deadline.timeIntervalSinceNow > 0) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, deadline.timeIntervalSinceNow, true);
}
uint32_t count = 0;
@autoreleasepool {
for (GCController* controller in GCController.controllers) {
FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices);
ffStrbufInit(&device->serial);
ffStrbufInit(&device->name);
device->battery = 0;
// The framework knows the pad but not its serial -- the public surface has no such
// field -- so it is borrowed from the IOKit device its private identity points at.
// Both passes then spell the same serial the same way, which is what makes the two
// entries comparable in the output.
IOHIDDeviceRef hidDevice = findHIDDeviceForController(controller, set);
if (hidDevice) {
ffCfStrGetString(IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDSerialNumberKey)), &device->serial);
}
if (controller.productCategory) {
ffStrbufSetS(&device->name, controller.productCategory.UTF8String);
} else if (controller.vendorName) {
ffStrbufSetS(&device->name, controller.vendorName.UTF8String);
} else {
ffStrbufSetS(&device->name, "MFi Gamepad");
}
// A wired pad has no battery object at all, and `batteryLevel` is `0.0` by default,
// so an unknown level cannot be told apart from a drained one. Both become the
// module's "0 means unknown", which hides the number rather than printing a wrong 0%.
GCDeviceBattery* battery = controller.battery;
if (battery) {
int percent = (int) lroundf(battery.batteryLevel * 100.f);
device->battery = (uint8_t) (percent < 0 ? 0 : (percent > 100 ? 100 : percent));
}
++count;
}
}
return count;
}
return 0;
}
// Whether the framework would claim any of these devices. Unlike the list itself this is a local
// predicate on the HID descriptor, not a daemon round trip, so it is the right thing to ask *before*
// paying for the wait above. Measured on this machine: 0.3 ms warm, 0.9 ms as the very first
// GameController call in a process -- and reading `GCController.controllers` immediately afterwards
// still cost 6 ms. That is the point: the gate answers without starting the framework, so the
// ~14 ms above is only ever paid by a pad Apple actually claims. A Logitech pad, or any pad outside
// 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) {
if (@available(macOS 11.0, *)) {
for (id value in set) {
if ([GCController supportsHIDDevice: (__bridge IOHIDDeviceRef) value]) {
return true;
}
}
}
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.
if (ctx->gameControllerCount > 0) {
if (@available(macOS 11.0, *)) {
if ([GCController supportsHIDDevice:value]) {
return;
}
}
}
FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *ctx->devices);
ffStrbufInit(&device->serial);
ffStrbufInit(&device->name);
device->battery = 0;
CFStringRef manufacturer = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDManufacturerKey));
ffCfStrGetString(manufacturer, &device->name);
CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey));
if (device->name.length) {
ffCfStrGetString(product, &device->serial);
ffStrbufAppendC(&device->name, ' ');
ffStrbufAppend(&device->name, &device->serial);
} else {
ffCfStrGetString(product, &device->name);
}
CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey));
ffCfStrGetString(serialNumber, &device->serial);
}
const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
FF_CFTYPE_AUTO_RELEASE IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
return "IOHIDManagerOpen() failed";
}
// Built as Foundation collections and bridged for the call. The C spelling of this is four lines
// of `CFDictionaryCreate` with inline array casts around two numbers each; a literal says the same
// thing in two. `kIOHIDDevice*Key` are C string macros -- the old spelling had to wrap them in
// `CFSTR()` -- and `@()` boxes them straight to `NSString`.
//
// The old spelling was *not* leaking, tempting as that is to assume: the four `ffCfCreateInt()`
// numbers do hand a +1 reference to a dictionary that retains them and never give it back, but
// `CFNumberCreate()` answers with a tagged pointer for every int32 -- measured, all seven test
// values came back pointer-identical and 200k stranded references grew RSS by 0 KB. So this is a
// readability change, not a fix.
NSArray* matchings = @[
@{ @(kIOHIDDeviceUsagePageKey): @(kHIDPage_GenericDesktop), @(kIOHIDDeviceUsageKey): @(kHIDUsage_GD_Joystick) },
@{ @(kIOHIDDeviceUsagePageKey): @(kHIDPage_GenericDesktop), @(kIOHIDDeviceUsageKey): @(kHIDUsage_GD_GamePad) },
];
IOHIDManagerSetDeviceMatchingMultiple(manager, (__bridge CFArrayRef) matchings);
// `IOHIDManagerCopyDevices()` follows the Create rule, so the set is handed to ARC rather than
// tracked with a cleanup attribute. It may also return NULL, which bridging turns into nil.
NSSet* set = CFBridgingRelease(IOHIDManagerCopyDevices(manager));
// The framework's entries come first, and the wait for them happens only when the framework has
// a reason to answer -- see `anyDeviceIsClaimedByGameController()`.
FFFindGamepadContext ctx = {
.devices = devices,
.gameControllerCount = anyDeviceIsClaimedByGameController(set) ? findGameControllerDevices(devices, set) : 0,
};
for (id value in set) {
enumSet((__bridge IOHIDDeviceRef) value, &ctx);
}
IOHIDManagerClose(manager, kIOHIDOptionsTypeNone);
return nullptr;
}
+279 -29
View File
@@ -1,6 +1,7 @@
#include "gamepad.h"
#include "common/io.h"
#include "common/mallocHelper.h"
#include "common/time.h"
#include "common/windows/unicode.h"
#include <windows.h>
@@ -15,6 +16,8 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
return "Nintendo Switch Joycon L";
case 0x2007:
return "Nintendo Switch Joycon R";
case 0x2008:
return "Nintendo Switch Joycon L+R";
case 0x2009:
return "Nintendo Switch Pro Controller";
case 0x200E:
@@ -22,6 +25,15 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
case 0x2017:
return "Nintendo Switch SNES Controller";
case 0x2066:
return "Nintendo Switch 2 Joycon R";
case 0x2067:
return "Nintendo Switch 2 Joycon L";
case 0x2068:
return "Nintendo Switch 2 Joycon L+R";
case 0x2069:
return "Nintendo Switch 2 Pro Controller";
default:
return nullptr;
}
@@ -44,6 +56,8 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
return "Sony DualSense";
case 0x0DF2:
return "Sony DualSense Edge";
case 0x0E5F:
return "Sony Access Controller";
default:
return nullptr;
@@ -77,6 +91,251 @@ static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId)
}
}
// A HID read on Windows hands back as many bytes as the descriptor declares for the largest report,
// so the number of bytes a read returns says nothing about which report actually arrived. The report
// id, the first byte of the buffer, is what identifies it.
static bool readHidReport(HANDLE hHidFile, uint8_t* buffer, uint32_t length, DWORD timeoutMs, DWORD* nBytes) {
OVERLAPPED overlapped = {};
DWORD read;
if (!ReadFile(hHidFile, buffer, length, &read, &overlapped)) {
// A synchronous failure -- the device was unplugged, the handle went stale, access was
// denied -- does not set up `overlapped` at all, so it still looks like `STATUS_SUCCESS`
// and asking for the result below would report success with zero bytes read.
if (GetLastError() != ERROR_IO_PENDING) {
return false;
}
if (!GetOverlappedResultEx(hHidFile, &overlapped, &read, timeoutMs, TRUE)) {
// A timeout leaves the read pending, and the driver writes into `buffer` and `overlapped` when
// it eventually completes -- both of which are gone by then: the buffer belongs to the caller
// and the OVERLAPPED sits on this frame. So the read is cancelled by its own OVERLAPPED rather
// than by handle (CancelIo would take every other read on the same handle with it), and the
// cancellation is waited for before returning.
CancelIoEx(hHidFile, &overlapped);
GetOverlappedResult(hHidFile, &overlapped, &read, TRUE);
return false;
}
}
*nBytes = read;
return true;
}
// The DualShock 4 sends a battery level in its state packet, but only in the extended report it
// switches to when a program asks it to. Until then it sends the plain HID gamepad report, which
// ends after the buttons.
typedef enum FFDs4ReportId: uint8_t {
FF_DS4_REPORT_ID_USB_STATE = 0x01,
FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST = 0x11,
FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST = 0x19,
} FFDs4ReportId;
// Offset of the battery level in the state packet, which is what the DualShock 4 documents and
// what SDL reads: five pad bytes after the six IMU axes.
#define FF_DS4_STATE_PACKET_BATTERY_OFFSET 29
// The plain HID gamepad report is ten bytes long, report id included
#define FF_DS4_NON_EXTENDED_REPORT_LENGTH 10
// Reads the battery level out of one input report of a DualShock 4, and returns 0 when the report
// carries none. `report[0]` is the report id and `reportLength` the size of the buffer.
static uint8_t parseDs4Battery(const uint8_t* report, uint32_t reportLength) {
uint32_t batteryOffset;
if (report[0] == FF_DS4_REPORT_ID_USB_STATE) {
// A non-extended report stops after the buttons, so a non-zero byte past that can only come
// from an extended packet. A wired controller always sends the extended one, a Bluetooth one
// only once another program has enabled it.
bool extended = false;
for (uint32_t i = FF_DS4_NON_EXTENDED_REPORT_LENGTH; i < reportLength; ++i) {
if (report[i]) {
extended = true;
break;
}
}
if (!extended) {
return 0;
}
// On USB the state packet follows the report id directly
batteryOffset = 1 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
} else if (report[0] >= FF_DS4_REPORT_ID_BLUETOOTH_STATE_FIRST && report[0] <= FF_DS4_REPORT_ID_BLUETOOTH_STATE_LAST) {
// A Bluetooth state packet has two extra bytes in front, the first of which says whether the
// packet carries HID state at all
if (reportLength <= 1 || !(report[1] & 0x80)) {
return 0;
}
batteryOffset = 3 + FF_DS4_STATE_PACKET_BATTERY_OFFSET;
} else {
return 0;
}
if (batteryOffset >= reportLength) {
return 0;
}
uint8_t batteryInfo = report[batteryOffset];
uint8_t level = batteryInfo & 0x0f;
if ((batteryInfo & 0x10 /*charging?*/) && level > 11) {
return 0; // A charging controller reports 11 for a full battery; anything above is not a level
}
uint8_t battery = (uint8_t) (level * 10 + 5); // 0..11 maps onto the middle of eleven 10% steps
return battery > 100 ? 100 : battery;
}
// The DualSense uses one report id over USB and another over Bluetooth, and the state packet follows
// the id directly in the first case and one byte later in the second. The official controller sends
// SDL's PS5StatePacket_t; the shorter PS5StatePacketAlt_t belongs to third-party pads and is not read.
typedef enum FFDualSenseReportId: uint8_t {
FF_DUALSENSE_REPORT_ID_USB_STATE = 0x01,
FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE = 0x31,
} FFDualSenseReportId;
// Offset of the battery level in PS5StatePacket_t, and the length of a whole USB state report
#define FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET 52
#define FF_DUALSENSE_USB_REPORT_LENGTH 64
static uint8_t parseDualSenseBattery(const uint8_t* report, uint32_t reportLength) {
uint32_t batteryOffset;
if (report[0] == FF_DUALSENSE_REPORT_ID_USB_STATE) {
// Over Bluetooth the controller answers the same id with a short report that stops before the
// battery, so the USB report length is what tells the two apart
if (reportLength != FF_DUALSENSE_USB_REPORT_LENGTH) {
return 0;
}
batteryOffset = 1 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
} else if (report[0] == FF_DUALSENSE_REPORT_ID_BLUETOOTH_STATE) {
// The Bluetooth report has one extra byte in front of the state packet
batteryOffset = 2 + FF_DUALSENSE_STATE_PACKET_BATTERY_OFFSET;
} else {
return 0;
}
if (batteryOffset >= reportLength) {
return 0;
}
uint8_t batteryInfo = report[batteryOffset];
uint32_t status = (batteryInfo >> 4) & 0x0f;
if (status == 2) {
return 100; // Charged
}
if (status > 1) {
return 0; // Not a status the controller documents
}
uint32_t battery = (batteryInfo & 0x0f) * 10 + 5; // Discharging and charging share one scale
return (uint8_t) (battery > 100 ? 100 : battery);
}
// A Switch controller interleaves its full state report with subcommand replies, which carry no
// battery at all. SDL skips those replies and waits for the next full report, and so does this,
// inside the same one-second budget a single read of any other controller gets.
typedef enum FFSwitchReportId: uint8_t {
FF_SWITCH_REPORT_ID_FULL_STATE_FIRST = 0x30,
FF_SWITCH_REPORT_ID_FULL_STATE_LAST = 0x31,
} FFSwitchReportId;
// The controller state packet starts one byte after the report id, and its second byte carries the
// battery level in its top three bits and the charging flag in bit 4. Anything that is not a full
// state report carries no battery and answers 0.
#define FF_SWITCH_STATE_PACKET_BATTERY_OFFSET 2
static uint8_t parseSwitchBattery(const uint8_t* report, uint32_t reportLength) {
if (report[0] < FF_SWITCH_REPORT_ID_FULL_STATE_FIRST || report[0] > FF_SWITCH_REPORT_ID_FULL_STATE_LAST) {
return 0;
}
if (reportLength <= FF_SWITCH_STATE_PACKET_BATTERY_OFFSET) {
return 0;
}
uint8_t batteryInfo = report[FF_SWITCH_STATE_PACKET_BATTERY_OFFSET];
uint32_t battery = ((batteryInfo & 0xE0) >> 4) * 100 / 8; // A level of 8 is a full battery
if (battery == 0) {
return 1; // Keep a drained controller apart from the "unknown" 0 the module uses
}
return (uint8_t) (battery > 100 ? 100 : battery);
}
// The longest run of reports without a full state report measured on a Pro Controller was eleven
#define FF_SWITCH_BATTERY_MAX_READS 16
static uint8_t detectSwitchBattery(HANDLE hHidFile, uint8_t* reportBuffer, uint32_t reportLength) {
const double deadline = ffTimeGetTick() + (double) FF_IO_TERM_RESP_WAIT_MS;
for (uint32_t i = 0; i < FF_SWITCH_BATTERY_MAX_READS; ++i) {
double remaining = deadline - ffTimeGetTick();
if (remaining < 1.) {
break;
}
DWORD nBytes;
// A report never arrives empty, so zero bytes means the read did not deliver one
if (!readHidReport(hHidFile, reportBuffer, reportLength, (DWORD) remaining, &nBytes) || !nBytes) {
break;
}
uint8_t battery = parseSwitchBattery(reportBuffer, reportLength);
if (battery) {
return battery;
}
}
return 0;
}
// Which of the battery layouts a controller uses, if any. A controller that is not listed here keeps
// the module's "unknown" 0 without a single read being issued.
typedef enum FFGamepadBatteryKind: uint8_t {
FF_GAMEPAD_BATTERY_NONE,
FF_GAMEPAD_BATTERY_DS4,
FF_GAMEPAD_BATTERY_DUALSENSE,
FF_GAMEPAD_BATTERY_SWITCH,
} FFGamepadBatteryKind;
static FFGamepadBatteryKind detectBatteryKind(uint32_t vendorId, uint32_t productId) {
switch (vendorId) {
case 0x054C: // Sony
switch (productId) {
case 0x0CE6: // DualSense
case 0x0DF2: // DualSense Edge
case 0x0E5F: // Access Controller
return FF_GAMEPAD_BATTERY_DUALSENSE;
case 0x05C4: // DualShock 4 Gen1
case 0x09CC: // DualShock 4 Gen2
case 0x0BA0: // DualShock 4 USB receiver
return FF_GAMEPAD_BATTERY_DS4;
default:
return FF_GAMEPAD_BATTERY_NONE;
}
// The Nintendo list is deliberately shorter than the name table above. The Charging Grip
// (0x200E) is a rail that charges Joy-Con and has no battery of its own, and the SNES
// online controller (0x2017) and the Switch 2 controllers (0x2066-0x2069) are ones SDL --
// which is what this parsing follows -- reports no battery for: `SDL_hidapi_switch2.c` has
// no power reporting at all, and the SNES entry is commented out of `controller_list.h`.
// Reading the Switch state report from them would be a guess, so they keep the module's
// "unknown".
case 0x057E: // Nintendo
switch (productId) {
case 0x2006: // Joy-Con L
case 0x2007: // Joy-Con R
case 0x2008: // Joy-Con pair
case 0x2009: // Pro Controller
return FF_GAMEPAD_BATTERY_SWITCH;
default:
return FF_GAMEPAD_BATTERY_NONE;
}
default:
return FF_GAMEPAD_BATTERY_NONE;
}
}
const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
UINT nDevices = 0;
if (GetRawInputDeviceList(nullptr, &nDevices, sizeof(RAWINPUTDEVICELIST))) {
@@ -159,39 +418,30 @@ const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) {
continue;
}
if (
(rdi.hid.dwVendorId == 0x054C && (rdi.hid.dwProductId == 0x05C4 || // PS4 Gen1
rdi.hid.dwProductId == 0x09CC // PS4 Gen2
)) ||
(rdi.hid.dwVendorId == 0x057E && (rdi.hid.dwProductId == 0x2009 // NS Pro
))) {
// Controller must be connected by other programs
// Sony and Nintendo controllers report a battery level only in a report a program has to
// ask the controller for, so a controller that nothing else has opened reports none.
FFGamepadBatteryKind batteryKind = detectBatteryKind(rdi.hid.dwVendorId, rdi.hid.dwProductId);
if (batteryKind != FF_GAMEPAD_BATTERY_NONE) {
FF_AUTO_FREE uint8_t* reportBuffer = malloc(caps.InputReportByteLength);
OVERLAPPED overlapped = {};
DWORD nBytes;
if (ReadFile(hHidFile, reportBuffer, caps.InputReportByteLength, &nBytes, &overlapped) ||
GetOverlappedResultEx(hHidFile, &overlapped, &nBytes, FF_IO_TERM_RESP_WAIT_MS, TRUE)) {
if (rdi.hid.dwVendorId == 0x054C) {
if (nBytes > 31) {
uint8_t batteryInfo = reportBuffer[caps.InputReportByteLength == 64 /*USB?*/ ? 30 : 32];
device->battery = (uint8_t) ((batteryInfo & 0x0f) * 100 / (batteryInfo & 0x10 /*charging?*/ ? 11 /*BATTERY_MAX_USB*/ : 8 /*BATTERY_MAX*/));
if (device->battery > 100) {
device->battery = 100;
}
switch (batteryKind) {
case FF_GAMEPAD_BATTERY_DS4:
// `reportBuffer` is uninitialised malloc() memory, so a read that delivered
// nothing must not be parsed
if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes) && nBytes) {
device->battery = parseDs4Battery(reportBuffer, caps.InputReportByteLength);
}
} else {
if (nBytes > 3 && reportBuffer[0] == 0x30) {
uint8_t batteryInfo = reportBuffer[2];
device->battery = (uint8_t) (((batteryInfo & 0xE0) >> 4) * 100 / 8);
if (device->battery == 0) {
device->battery = 1;
} else if (device->battery > 100) {
device->battery = 100;
}
break;
case FF_GAMEPAD_BATTERY_DUALSENSE:
if (readHidReport(hHidFile, reportBuffer, caps.InputReportByteLength, FF_IO_TERM_RESP_WAIT_MS, &nBytes) && nBytes) {
device->battery = parseDualSenseBattery(reportBuffer, caps.InputReportByteLength);
}
}
} else {
CancelIo(hHidFile);
break;
case FF_GAMEPAD_BATTERY_SWITCH:
device->battery = detectSwitchBattery(hHidFile, reportBuffer, caps.InputReportByteLength);
break;
case FF_GAMEPAD_BATTERY_NONE:
break;
}
}
}
+1 -1
View File
@@ -124,7 +124,7 @@ const char* detectByOpenGL(FFlist* gpus) {
gpu->coreCount = FF_GPU_CORE_COUNT_UNSET;
gpu->frequency = FF_GPU_FREQUENCY_UNSET;
gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET;
gpu->dedicated = gpu->shared = (FFGPUMemory) { 0, 0 };
gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET;
gpu->deviceId = 0;
gpu->pcieSpeed = FF_GPU_PCIE_SPEED_UNSET;
-6
View File
@@ -1,6 +0,0 @@
#include "gpu.h"
const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) {
FF_UNUSED(options, gpus);
return "Not supported on this platform";
}
+43 -2
View File
@@ -1,4 +1,5 @@
#include "gpu.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/path.h"
#include "common/properties.h"
@@ -22,35 +23,45 @@ static const FFstrbuf* loadPciIds() {
#ifdef FF_CUSTOM_PCI_IDS_PATH
FF_DEBUG("Loading PCI IDs from custom path: %s", FF_STR(FF_CUSTOM_PCI_IDS_PATH));
ffReadFileBuffer(FF_STR(FF_CUSTOM_PCI_IDS_PATH), &pciids);
#else // FF_CUSTOM_PCI_IDS_PATH
#if __linux__
FF_DEBUG("Loading PCI IDs from %s", FASTFETCH_TARGET_DIR_USR "/share/hwdata/pci.ids");
ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/hwdata/pci.ids", &pciids);
if (pciids.length == 0) {
FF_DEBUG("PCI IDs not found, trying %s", FASTFETCH_TARGET_DIR_USR "/share/misc/pci.ids");
ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/misc/pci.ids", &pciids); // debian?
if (pciids.length == 0) {
FF_DEBUG("PCI IDs not found, trying %s", FASTFETCH_TARGET_DIR_USR "/local/share/hwdata/pci.ids");
ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/local/share/hwdata/pci.ids", &pciids);
}
}
#elif __OpenBSD__ || __FreeBSD__ || __NetBSD__
FF_DEBUG("Loading PCI IDs from %s", FF_PATH_PKG_BASE "/share/hwdata/pci.ids");
ffReadFileBuffer(FF_PATH_PKG_BASE "/share/hwdata/pci.ids", &pciids);
if (pciids.length == 0) {
FF_DEBUG("PCI IDs not found, trying %s", FF_PATH_PKG_BASE "/share/pciids/pci.ids");
ffReadFileBuffer(FF_PATH_PKG_BASE "/share/pciids/pci.ids", &pciids);
}
#elif __sun
FF_DEBUG("Loading PCI IDs from %s", FASTFETCH_TARGET_DIR_ROOT "/usr/share/hwdata/pci.ids");
ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/usr/share/hwdata/pci.ids", &pciids);
#elif __HAIKU__
FF_DEBUG("Loading PCI IDs from %s", FASTFETCH_TARGET_DIR_ROOT "/system/data/hwdata/pci.ids");
ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/system/data/hwdata/pci.ids", &pciids);
#endif
#endif // FF_CUSTOM_PCI_IDS_PATH
FF_DEBUG("PCI IDs data loaded: %u bytes", pciids.length);
return &pciids;
}
static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
FF_DEBUG("Searching pci.ids for vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass);
if (content->length) {
char buffer[32];
@@ -59,6 +70,7 @@ static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t
char* start = (char*) memmem(content->chars, content->length, buffer, len);
char* end = content->chars + content->length;
if (start) {
FF_DEBUG("Found PCI vendor entry for 0x%04x", vendor);
start += len;
end = memchr(start, '\n', (uint32_t) (end - start));
if (!end) {
@@ -85,6 +97,7 @@ static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t
len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x ", device);
start = memmem(start, (size_t) (end - start), buffer, len);
if (start) {
FF_DEBUG("Found PCI device entry for 0x%04x:0x%04x", vendor, device);
start += len;
end = memchr(start, '\n', (uint32_t) (end - start));
if (!end) {
@@ -102,8 +115,14 @@ static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t
if (!gpu->name.length) {
ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start);
}
} else {
FF_DEBUG("PCI device entry 0x%04x:0x%04x was not found in pci.ids", vendor, device);
}
} else {
FF_DEBUG("PCI vendor entry 0x%04x was not found in pci.ids", vendor);
}
} else {
FF_DEBUG("PCI IDs data is empty; using a synthesized device name");
}
if (!gpu->name.length) {
@@ -124,6 +143,9 @@ static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t
}
ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr);
FF_DEBUG("PCI device name not found; synthesized name: %s", gpu->name.chars);
} else {
FF_DEBUG("Resolved PCI device name: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars);
}
}
@@ -133,6 +155,7 @@ static inline int pciDeviceCmp(const uint16_t* key, const FFPciDevice* element)
}
static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
FF_DEBUG("Searching embedded PCI IDs for vendor=0x%04x device=0x%04x", vendor, device);
for (const FFPciVendor* pvendor = ffPciVendors; pvendor->name; pvendor++) {
if (pvendor->id != vendor) {
continue;
@@ -146,6 +169,10 @@ static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FF
if (pdevice) {
uint32_t nameLen = (uint32_t) strlen(pdevice->name);
if (nameLen == 0) {
FF_DEBUG("Embedded PCI device entry for 0x%04x:0x%04x has an empty name", vendor, device);
return false;
}
const char* closingBracket = pdevice->name + nameLen - 1;
if (*closingBracket == ']') {
const char* openingBracket = memrchr(pdevice->name, '[', nameLen - 1);
@@ -157,9 +184,11 @@ static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FF
if (!gpu->name.length) {
ffStrbufSetNS(&gpu->name, nameLen, pdevice->name);
}
FF_DEBUG("Embedded PCI ID matched: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars);
return true;
}
FF_DEBUG("Embedded PCI vendor matched ('%s'), but device 0x%04x was not found", pvendor->name, device);
if (!gpu->name.length) {
const char* subclassStr;
switch (subclass) {
@@ -179,25 +208,31 @@ static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FF
ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr);
}
FF_DEBUG("Using synthesized PCI device name: %s", gpu->name.chars);
return true;
}
FF_DEBUG("Embedded PCI vendor 0x%04x was not found", vendor);
return false;
}
#endif
void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) {
FF_DEBUG("Resolving PCI GPU name: vendor=0x%04x device=0x%04x subclass=0x%02x", vendor, device, subclass);
if (vendor == 0x1234 && device == 0x1111 && subclass == 0) { // Not exist in pci.ids
ffStrbufSetStatic(&gpu->name, "Virtual Video Controller");
FF_DEBUG("Matched special virtual video controller ID");
return;
}
#if FF_HAVE_EMBEDDED_PCIIDS
bool ok = loadPciidsInc(subclass, vendor, device, gpu);
if (ok) {
FF_DEBUG("Resolved PCI GPU name from embedded IDs: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars);
return;
}
#endif
return parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu);
parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu);
FF_DEBUG("Resolved PCI GPU name from pci.ids: vendor='%s', name='%s'", gpu->vendor.chars, gpu->name.chars);
}
#if FF_HAVE_EMBEDDED_AMDGPUIDS
@@ -211,8 +246,10 @@ static bool loadAmdGpuIdsInc(uint16_t deviceId, uint8_t revision, FFGPUResult* g
FFArmGpuProduct* product = bsearch(&key, ffAmdGpuProducts, ARRAY_SIZE(ffAmdGpuProducts), sizeof(*ffAmdGpuProducts), (void*) amdGpuCmp);
if (product) {
ffStrbufSetS(&gpu->name, product->name);
FF_DEBUG("Embedded AMD GPU ID matched: device=0x%04x revision=0x%02x name='%s'", deviceId, revision, gpu->name.chars);
return true;
}
FF_DEBUG("Embedded AMD GPU ID not found: device=0x%04x revision=0x%02x", deviceId, revision);
return false;
}
#endif
@@ -221,18 +258,22 @@ static void parseAmdGpuIdsFile(uint16_t deviceId, uint8_t revision, FFGPUResult*
char query[32];
snprintf(query, ARRAY_SIZE(query), "%X,\t%X,", (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);
#else
FF_DEBUG("Searching libdrm/amdgpu.ids for device=0x%04x revision=0x%02x", deviceId, revision);
ffParsePropFileData("libdrm/amdgpu.ids", query, &gpu->name);
#endif
FF_DEBUG("AMD GPU IDs file lookup %s: device=0x%04x revision=0x%02x name='%s'", gpu->name.length ? "matched" : "not matched", deviceId, revision, gpu->name.chars);
}
void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu) {
FF_DEBUG("Resolving AMD GPU name: device=0x%04x revision=0x%02x", deviceId, revisionId);
#if FF_HAVE_EMBEDDED_AMDGPUIDS
bool ok = loadAmdGpuIdsInc(deviceId, revisionId, gpu);
if (ok) {
return;
}
#endif
return parseAmdGpuIdsFile(deviceId, revisionId, gpu);
parseAmdGpuIdsFile(deviceId, revisionId, gpu);
}
+117 -47
View File
@@ -1,7 +1,7 @@
#include "fastfetch.h"
#include "common/properties.h"
#include "common/thread.h"
#include "common/settings.h"
#include "common/FFcache.h"
#include "detection/gtk_qt/gtk_qt.h"
#include "detection/displayserver/displayserver.h"
@@ -11,29 +11,42 @@ static inline bool allPropertiesSet(FFGTKResult* result) {
result->font.length > 0;
}
static inline void applyGTKSettings(FFGTKResult* result, const char* themeName, const char* iconsName, const char* fontName, const char* cursorTheme, int cursorSize, const char* wallpaper) {
// The values read from the desktop settings daemons, shared by the GTK2/3/4 results. They are
// owned copies on purpose: `settings.c` hands out strings it leaks deliberately (see the
// "Leaks value.chars" notes there), so borrowing them past a `--dynamic-interval` round would
// leak one set per round. Copying them also lets `destroy` release them.
typedef struct FFGTKSettings {
FFstrbuf theme;
FFstrbuf icons;
FFstrbuf font;
FFstrbuf cursor;
FFstrbuf wallpaper;
int32_t cursorSize;
} FFGTKSettings;
static inline void applyGTKSettings(FFGTKResult* result, const FFGTKSettings* settings) {
if (result->theme.length == 0) {
ffStrbufAppendS(&result->theme, themeName);
ffStrbufAppend(&result->theme, &settings->theme);
}
if (result->icons.length == 0) {
ffStrbufAppendS(&result->icons, iconsName);
ffStrbufAppend(&result->icons, &settings->icons);
}
if (result->font.length == 0) {
ffStrbufAppendS(&result->font, fontName);
ffStrbufAppend(&result->font, &settings->font);
}
if (result->cursor.length == 0) {
ffStrbufAppendS(&result->cursor, cursorTheme);
ffStrbufAppend(&result->cursor, &settings->cursor);
}
if (result->cursorSize.length == 0 && cursorSize > 0) {
ffStrbufAppendF(&result->cursorSize, "%i", cursorSize);
if (result->cursorSize.length == 0 && settings->cursorSize > 0) {
ffStrbufAppendSInt(&result->cursorSize, settings->cursorSize);
}
if (result->wallpaper.length == 0) {
ffStrbufAppendS(&result->wallpaper, wallpaper);
ffStrbufAppend(&result->wallpaper, &settings->wallpaper);
}
}
@@ -43,22 +56,23 @@ static bool testXfconfWallpaperPropKey([[maybe_unused]] void* data, const char*
return count == 0;
}
static void detectGTKFromSettings(FFGTKResult* result) {
static const char* themeName = nullptr;
static const char* iconsName = nullptr;
static const char* fontName = nullptr;
static const char* cursorTheme = nullptr;
static int cursorSize = 0;
static const char* wallpaper = nullptr;
static FFGTKSettings gtkSettings;
static bool init = false;
static void initGTKSettings(void* storage) {
FFGTKSettings* settings = storage;
if (init) {
applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper);
return;
}
ffStrbufInit(&settings->theme);
ffStrbufInit(&settings->icons);
ffStrbufInit(&settings->font);
ffStrbufInit(&settings->cursor);
ffStrbufInit(&settings->wallpaper);
init = true;
const char* themeName = nullptr;
const char* iconsName = nullptr;
const char* fontName = nullptr;
const char* cursorTheme = nullptr;
int cursorSize = 0;
const char* wallpaper = nullptr;
const FFDisplayServerResult* wmde = ffConnectDisplayServer();
@@ -98,14 +112,14 @@ static void detectGTKFromSettings(FFGTKResult* result) {
wallpaper = ffSettingsGetGnome("/org/gnome/desktop/background/picture-uri", "org.gnome.desktop.background", nullptr, "picture-uri", FF_VARIANT_TYPE_STRING).strValue;
} else if (
ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_ENLIGHTENMENT)) {
ffEnlightenmentSettings settings = {};
if (ffSettingsGetEnlightenmentProperty(&settings)) {
themeName = settings.theme;
iconsName = settings.icon_theme;
fontName = settings.font;
cursorTheme = settings.use_e_cursor ? "Enlightenment" : "Application";
cursorSize = settings.cursor_size;
wallpaper = settings.desktop_default_background;
ffEnlightenmentSettings enlightenmentSettings = {};
if (ffSettingsGetEnlightenmentProperty(&enlightenmentSettings)) {
themeName = enlightenmentSettings.theme;
iconsName = enlightenmentSettings.icon_theme;
fontName = enlightenmentSettings.font;
cursorTheme = enlightenmentSettings.use_e_cursor ? "Enlightenment" : "Application";
cursorSize = enlightenmentSettings.cursor_size;
wallpaper = enlightenmentSettings.desktop_default_background;
}
}
} else {
@@ -121,9 +135,34 @@ static void detectGTKFromSettings(FFGTKResult* result) {
cursorSize = ffSettingsGetDConf("/org/gnome/desktop/interface/cursor-size", FF_VARIANT_TYPE_INT).intValue;
}
applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper);
// The strings above are owned by `settings.c`, which leaks them on purpose, so copy them into
// the cache instead of borrowing them: this runs again on every `--dynamic-interval` round.
ffStrbufSetS(&settings->theme, themeName);
ffStrbufSetS(&settings->icons, iconsName);
ffStrbufSetS(&settings->font, fontName);
ffStrbufSetS(&settings->cursor, cursorTheme);
ffStrbufSetS(&settings->wallpaper, wallpaper);
settings->cursorSize = cursorSize;
}
static void destroyGTKSettings(void* storage) {
FFGTKSettings* settings = storage;
ffStrbufDestroy(&settings->theme);
ffStrbufDestroy(&settings->icons);
ffStrbufDestroy(&settings->font);
ffStrbufDestroy(&settings->cursor);
ffStrbufDestroy(&settings->wallpaper);
settings->cursorSize = 0;
}
static FFcacheEntry ffCacheEntryGTKSettings = {
.name = "gtk-settings",
.storage = &gtkSettings,
.init = initGTKSettings,
.destroy = destroyGTKSettings,
};
static void detectGTKFromConfigFile(const char* filename, FFGTKResult* result) {
ffParsePropFileValues(filename, 5, (FFpropquery[]) { { "gtk-theme-name =", &result->theme }, { "gtk-icon-theme-name =", &result->icons }, { "gtk-font-name =", &result->font }, { "gtk-cursor-theme-name =", &result->cursor }, { "gtk-cursor-theme-size =", &result->cursorSize } });
}
@@ -172,7 +211,7 @@ static void detectGTKFromConfigDir(FFstrbuf* configDir, const char* version, FFG
static void detectGTK(const char* version, FFGTKResult* result) {
// Mate, Cinnamon, GNOME, Unity, Budgie use dconf to save theme config
// On other DEs, this will do nothing
detectGTKFromSettings(result);
applyGTKSettings(result, ffCacheGet(&ffCacheEntryGTKSettings));
if (allPropertiesSet(result)) {
return;
}
@@ -189,20 +228,51 @@ static void detectGTK(const char* version, FFGTKResult* result) {
}
}
#define FF_DETECT_GTK_IMPL(version) \
static FFGTKResult result; \
static bool init = false; \
if (init) \
return &result; \
init = true; \
ffStrbufInit(&result.theme); \
ffStrbufInit(&result.icons); \
ffStrbufInit(&result.font); \
ffStrbufInit(&result.cursor); \
ffStrbufInit(&result.cursorSize); \
ffStrbufInit(&result.wallpaper); \
detectGTK(#version, &result); \
return &result;
static void initGTKResult(const char* version, FFGTKResult* result) {
ffStrbufInit(&result->theme);
ffStrbufInit(&result->icons);
ffStrbufInit(&result->font);
ffStrbufInit(&result->cursor);
ffStrbufInit(&result->cursorSize);
ffStrbufInit(&result->wallpaper);
detectGTK(version, result);
}
static void destroyGTKResult(void* storage) {
FFGTKResult* result = storage;
ffStrbufDestroy(&result->theme);
ffStrbufDestroy(&result->icons);
ffStrbufDestroy(&result->font);
ffStrbufDestroy(&result->cursor);
ffStrbufDestroy(&result->cursorSize);
ffStrbufDestroy(&result->wallpaper);
}
static void initGTK2Result(void* storage) {
initGTKResult("2", storage);
}
static void initGTK3Result(void* storage) {
initGTKResult("3", storage);
}
static void initGTK4Result(void* storage) {
initGTKResult("4", storage);
}
// Each version gets its own result and its own entry, so a refresh drops all three (each one is
// then rebuilt on demand, in the same lazy order as a single-shot run). They share the single
// `gtk-settings` entry above, which keeps a round at one settings query instead of three.
#define FF_DETECT_GTK_IMPL(version) \
static FFGTKResult result; \
static FFcacheEntry entry = { \
.name = "gtk" #version, \
.storage = &result, \
.init = initGTK##version##Result, \
.destroy = destroyGTKResult, \
}; \
return ffCacheGet(&entry);
const FFGTKResult* ffDetectGTK2(void) {
FF_DETECT_GTK_IMPL(2)
@@ -216,4 +286,4 @@ const FFGTKResult* ffDetectGTK4(void) {
FF_DETECT_GTK_IMPL(4)
}
#undef FF_CALCULATE_GTK_IMPL
#undef FF_DETECT_GTK_IMPL
+36 -21
View File
@@ -1,7 +1,7 @@
#include "fastfetch.h"
#include "common/properties.h"
#include "common/thread.h"
#include "common/strutil.h"
#include "common/FFcache.h"
#include "detection/gtk_qt/gtk_qt.h"
#include "detection/displayserver/displayserver.h"
@@ -172,38 +172,53 @@ static void detectKvantum(FFQtResult* result) {
});
}
const FFQtResult* ffDetectQt(void) {
static FFQtResult result;
static FFQtResult result;
static bool init = false;
if (init) {
return &result;
}
init = true;
static void initQtResult(void* storage) {
FFQtResult* qt = storage;
ffStrbufInit(&result.widgetStyle);
ffStrbufInit(&result.colorScheme);
ffStrbufInit(&result.icons);
ffStrbufInit(&result.font);
ffStrbufInit(&result.wallpaper);
ffStrbufInit(&qt->widgetStyle);
ffStrbufInit(&qt->colorScheme);
ffStrbufInit(&qt->icons);
ffStrbufInit(&qt->font);
ffStrbufInit(&qt->wallpaper);
const FFDisplayServerResult* wmde = ffConnectDisplayServer();
if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_PLASMA)) {
detectPlasma(&result);
detectPlasma(qt);
} else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_LXQT)) {
detectLXQt(&result);
detectLXQt(qt);
} else {
const char* qPlatformTheme = getenv("QT_QPA_PLATFORMTHEME");
if (qPlatformTheme && (ffStrEquals(qPlatformTheme, "qt5ct") || ffStrEquals(qPlatformTheme, "qt6ct"))) {
detectQtCt(qPlatformTheme[2], &result);
detectQtCt(qPlatformTheme[2], qt);
}
}
if (ffStrbufEqualS(&result.widgetStyle, "kvantum") || ffStrbufEqualS(&result.widgetStyle, "kvantum-dark")) {
ffStrbufClear(&result.widgetStyle);
detectKvantum(&result);
if (ffStrbufEqualS(&qt->widgetStyle, "kvantum") || ffStrbufEqualS(&qt->widgetStyle, "kvantum-dark")) {
ffStrbufClear(&qt->widgetStyle);
detectKvantum(qt);
}
return &result;
}
static void destroyQtResult(void* storage) {
FFQtResult* qt = storage;
ffStrbufDestroy(&qt->widgetStyle);
ffStrbufDestroy(&qt->colorScheme);
ffStrbufDestroy(&qt->icons);
ffStrbufDestroy(&qt->font);
ffStrbufDestroy(&qt->wallpaper);
}
static FFcacheEntry ffCacheEntryQt = {
.name = "qt",
.storage = &result,
.init = initQtResult,
.destroy = destroyQtResult,
};
const FFQtResult* ffDetectQt(void) {
return ffCacheGet(&ffCacheEntryQt);
}
+12
View File
@@ -229,6 +229,18 @@ const char* ffHostGetMacProductNameWithHwModel(const FFstrbuf* hwModel) {
}
} else if (ffStrbufStartsWithS(hwModel, "Mac")) {
const char* version = hwModel->chars + strlen("Mac");
if (ffStrEquals(version, "18,5")) {
return "Mac Mini (M6, 2026)";
}
if (ffStrEquals(version, "17,16")) {
return "Mac Mini (M5 Pro, 2026)";
}
if (ffStrEquals(version, "17,15")) {
return "Mac Studio (M5 Ultra, 2026)";
}
if (ffStrEquals(version, "17,14")) {
return "Mac Studio (M5 Max, 2026)";
}
if (ffStrEquals(version, "17,9")) {
return "MacBook Pro (14-inch, M5 Pro, 2026)";
}
@@ -1,5 +0,0 @@
#include "initsystem.h"
const char* ffDetectInitSystem([[maybe_unused]] FFInitSystemResult* result) {
return "Not supported on this platform";
}
@@ -25,7 +25,7 @@ const char* ffDetectInitSystem(FFInitSystemResult* result) {
if (ffIsSystemBasicProcessInfoAvailable()) {
// SYSTEM_BASICPROCESS_INFORMATION entries are much smaller than SYSTEM_PROCESS_INFORMATION ones,
// so a modest buffer should be enough to contain all processes
SYSTEM_BASICPROCESS_INFORMATION buffer[1024];
SYSTEM_BASICPROCESS_INFORMATION buffer[1024] = {};
NTSTATUS status = NtQuerySystemInformation(SystemBasicProcessInformation, buffer, sizeof(buffer), NULL);
if (status != STATUS_INFO_LENGTH_MISMATCH && !NT_SUCCESS(status)) {
goto fallback;
+1 -1
View File
@@ -1,5 +1,5 @@
#include "keyboard.h"
const char* ffDetectKeyboard([[maybe_unused]] FFlist* devices /* List of FFKeyboardDevice */) {
return "No mouse support on this platform";
return "No keyboard support on this platform";
}
+2 -3
View File
@@ -18,16 +18,15 @@ const char* ffDetectLoadavg(double result[3]) {
}
#endif
#ifndef __GNU__
// getloadavg requires higher ANDROID_API version
struct sysinfo si;
if (sysinfo(&si) < 0) {
return "sysinfo() failed";
return "both /proc/loadavg and sysinfo() failed";
}
for (int i = 0; i < 3; i++) {
result[i] = (double) si.loads[i] / (1 << SI_LOAD_SHIFT);
}
#endif
return nullptr;
}
+50 -5
View File
@@ -1,19 +1,54 @@
#include "media.h"
#include "common/FFcache.h"
#include "common/io.h"
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover);
static FFMediaResult result;
// The cover file of this run, if there is one. It is tracked here rather than in `result` because the
// cached result is dropped between `--dynamic-interval` rounds, and `result.cover` — the only other
// copy of the path — is destroyed with it. The file itself has to outlive that, since the
// `kitty-direct` logo type hands its path to the terminal, so it is kept for the whole run and
// removed at exit.
static FFstrbuf coverFile;
static bool coverFileRegistered = false;
static void removeMediaCoverFile(void) {
if (result.cover.length > 0) {
ffRemoveFile(result.cover.chars);
ffStrbufDestroy(&result.cover);
if (coverFile.length > 0) {
ffRemoveFile(coverFile.chars);
ffStrbufDestroy(&coverFile);
}
}
static void destroyMediaResult(void* storage) {
FFMediaResult* media = storage;
ffStrbufDestroy(&media->error);
ffStrbufDestroy(&media->playerId);
ffStrbufDestroy(&media->player);
ffStrbufDestroy(&media->song);
ffStrbufDestroy(&media->artist);
ffStrbufDestroy(&media->album);
ffStrbufDestroy(&media->url);
ffStrbufDestroy(&media->status);
ffStrbufDestroy(&media->cover);
media->length = 0;
media->position = 0;
media->removeCoverAfterUse = false;
}
// `saveCover` asks for a side effect and is not part of the cache identity, so the entry cannot be
// initialized through `init(void*)`: the first caller of a generation decides, and the callers that
// follow reuse its result, as they did when the result was cached for the whole run.
static FFcacheEntry ffCacheEntryMedia = {
.name = "media",
.storage = &result,
.destroy = destroyMediaResult,
};
const FFMediaResult* ffDetectMedia(bool saveCover) {
if (result.error.chars == nullptr) {
if (ffCacheBeginInit(&ffCacheEntryMedia)) {
ffStrbufInit(&result.error);
ffStrbufInit(&result.playerId);
ffStrbufInit(&result.player);
@@ -37,7 +72,17 @@ const FFMediaResult* ffDetectMedia(bool saveCover) {
ffStrbufTrimRightSpace(&result.player);
if (saveCover && result.removeCoverAfterUse) {
atexit(removeMediaCoverFile);
// A new round can produce a new cover while the previous file is still on disk, and
// `coverFile` is about to forget its path. Remove it here, otherwise only the last one
// would ever be cleaned up by `atexit`.
if (coverFile.length > 0 && !ffStrbufEqual(&coverFile, &result.cover)) {
ffRemoveFile(coverFile.chars);
}
ffStrbufSet(&coverFile, &result.cover);
if (!coverFileRegistered) {
coverFileRegistered = true;
atexit(removeMediaCoverFile);
}
}
}
+968
View File
@@ -0,0 +1,968 @@
#include "media.h"
#include "common/android/api.h"
#include "common/android/binder.h"
#include "common/debug.h"
#include "common/io.h"
#include "common/mallocHelper.h"
#include "detection/displayserver/displayserver.h"
#include <android/bitmap.h>
#include <android/data_space.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
// Android's "now playing" belongs to the media_session system service. There is no C API for it --
// the NDK exposes neither the service nor a Java bridge -- and `/system/bin/dumpsys media_session`
// costs a fork/exec (measured at ~70 ms on this device) while answering with less. This file speaks
// to the service over /dev/binder through common/android/binder.h instead, the same way the wallpaper
// and wifi modules do, and pays ~0.1 ms per call.
//
// The gate is a single method. `ISessionManager.getSessions(ComponentName, int)` is the only entry
// point that checks a permission: MediaSessionService.enforceMediaPermissions() admits the system UI,
// a holder of android.permission.MEDIA_CONTENT_CONTROL and an enabled notification listener, and
// refuses everything else -- measured on an Android 16 device, an app UID opens binder, finds the
// service and reaches onTransact, and is then answered with a SecurityException carrying the server's
// own stack (`enforceMediaPermissions` <- `verifySessionsRequest` <- `getSessions`), while the shell
// UID gets the session list from the same transaction code. Nothing in this file tests the uid: the
// service is asked and its answer is believed, so a device whose policy is looser, or one where a
// notification listener is registered, works without a change here.
//
// Only one of the two routes is ever asked, and which one it is depends on the display server. The
// binder route is Android's, and the session it reads is the one the phone is playing only while
// SurfaceFlinger is the display server; a Termux:X11 or Wayland session runs a real compositor on top
// of the device, and the players running there are not Android media sessions at all -- so that case
// is handed to the MPRIS implementation in media_linux.c, which is compiled alongside this file for
// exactly this reason.
//
// Under SurfaceFlinger the Android session is still not the whole story. When it has nothing to say --
// an app UID, or a phone with nothing registered -- the module falls back to that same MPRIS
// implementation, over the session bus. That bus is open to every process in the session, so the
// fallback is the only route an app UID has and a real one for the shell UID too.
//
// What the service answers with is worth the trouble, and more than the dump carries. The reply to
// getSessions is a list of session tokens, and each token *is* an ISessionController binder -- there
// is no getController() hop -- and every argument-less getter on that interface was measured to
// answer without a permission check, so the one gate above is the whole story. getMetadata yields the
// title, the artist, the album and, unlike the dump, the track length; getPlaybackState yields the
// state, the position and the playback speed; and the artwork comes across as a Bitmap whose pixels
// arrive as an ashmem descriptor, which is what makes a real cover possible here.
//
// Two traps, both measured:
//
// * A request must ask with TF_ACCEPT_FDS, or a reply that carries a descriptor is refused with
// BR_FAILED_REPLY -- which reads exactly like a service that is not running. The failure is
// asymmetric and that is what makes it expensive: only metadata carrying artwork has a
// descriptor, so a probe tried against one player passes while the same probe fails against the
// next one.
// * A Bundle value is written by Parcel.writeValue, and for a Parcelable that means an int32 byte
// length in front of the class name (Parcel.isLengthPrefixed). Reading the class name without
// accounting for it, or reading a Bitmap as if it were a string, desynchronises every field after
// it. The length is also the one thing that makes an unknown Parcelable skippable.
//
#define FF_MEDIA_ANDROID_SERVICE "media_session"
#define FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR "android.media.session.ISessionManager"
#define FF_MEDIA_ANDROID_CONTROLLER_DESCRIPTOR "android.media.session.ISessionController"
// Transaction codes as this device's own framework.jar declares them, read out of its dex rather than
// transcribed from AOSP -- a vendor ROM adds and drops methods as it likes, and a wrong code reaches
// a different method or none at all. getSessions is the second declared method of ISessionManager;
// the numbers on ISessionController are sparse because most of that interface is setters this module
// never calls.
#define FF_MEDIA_ANDROID_GET_SESSIONS 2u
#define FF_MEDIA_ANDROID_GET_PACKAGE_NAME 5u
#define FF_MEDIA_ANDROID_GET_METADATA 32u
#define FF_MEDIA_ANDROID_GET_PLAYBACK_STATE 33u
// The largest reply any of these getters produced is a metadata Bundle at ~1.6 KB, and the artwork
// never lands here -- it arrives as a descriptor. 16 KB leaves room for a Bundle of unusually long
// strings; a reply that did not fit would be reported rather than misread.
#define FF_MEDIA_ANDROID_REPLY_SIZE (16 * 1024)
// An interface token plus, for getSessions, two int32s.
#define FF_MEDIA_ANDROID_REQUEST_SIZE 128
// Lossless WebP reads quality as effort rather than as fidelity: 0 is "compress quickly and accept a
// larger file", 100 is "spend the time". Measured on a 363x363 cover, the whole range is worth 1.2%
// of the file for 2.4 times the time, so there is nothing at the top to buy.
#define FF_MEDIA_ANDROID_COVER_QUALITY 0
// One reply buffer for every call in this file: the calls are sequential, and the metadata of the
// winning session is the last one to need it.
static uint8_t s_reply[FF_MEDIA_ANDROID_REPLY_SIZE];
// ---------------------------------------------------------------------------------------------
// Reading a Parcel
// ---------------------------------------------------------------------------------------------
// android.os.Parcel value types, from Parcel.VAL_*. A MediaMetadata Bundle only ever holds the four
// below; anything else stops the walk there rather than being guessed at, and the caller is told.
typedef enum FFMediaAndroidValueType : int32_t {
FF_MEDIA_ANDROID_VAL_NULL = -1,
FF_MEDIA_ANDROID_VAL_STRING = 0,
FF_MEDIA_ANDROID_VAL_INTEGER = 1,
FF_MEDIA_ANDROID_VAL_PARCELABLE = 4,
FF_MEDIA_ANDROID_VAL_LONG = 6,
FF_MEDIA_ANDROID_VAL_CHAR_SEQUENCE = 10,
} FFMediaAndroidValueType;
// Bundle.writeToParcel writes this in front of the entry count. It is the byte pattern 'BNDL', and
// checking it is what tells a bundle apart from the length word of something else.
#define FF_MEDIA_ANDROID_BUNDLE_MAGIC 0x4C444E42u
#define FF_MEDIA_ANDROID_CLASS_BITMAP "android.graphics.Bitmap"
#define FF_MEDIA_ANDROID_KEY_PREFIX "android.media.metadata."
#define FF_MEDIA_ANDROID_KEY_TITLE FF_MEDIA_ANDROID_KEY_PREFIX "TITLE"
#define FF_MEDIA_ANDROID_KEY_DISPLAY_TITLE FF_MEDIA_ANDROID_KEY_PREFIX "DISPLAY_TITLE"
#define FF_MEDIA_ANDROID_KEY_ARTIST FF_MEDIA_ANDROID_KEY_PREFIX "ARTIST"
#define FF_MEDIA_ANDROID_KEY_DISPLAY_SUBTITLE FF_MEDIA_ANDROID_KEY_PREFIX "DISPLAY_SUBTITLE"
#define FF_MEDIA_ANDROID_KEY_ALBUM FF_MEDIA_ANDROID_KEY_PREFIX "ALBUM"
#define FF_MEDIA_ANDROID_KEY_DURATION FF_MEDIA_ANDROID_KEY_PREFIX "DURATION"
#define FF_MEDIA_ANDROID_KEY_MEDIA_URI FF_MEDIA_ANDROID_KEY_PREFIX "MEDIA_URI"
#define FF_MEDIA_ANDROID_KEY_ART FF_MEDIA_ANDROID_KEY_PREFIX "ART"
#define FF_MEDIA_ANDROID_KEY_ALBUM_ART FF_MEDIA_ANDROID_KEY_PREFIX "ALBUM_ART"
#define FF_MEDIA_ANDROID_KEY_DISPLAY_ICON FF_MEDIA_ANDROID_KEY_PREFIX "DISPLAY_ICON"
// MediaMetadata carries the artwork under three keys, and Android documents them as: ART is "the
// artwork for the media", ALBUM_ART "the artwork for the album it is from", DISPLAY_ICON "a small
// icon representing the media". They are usually the same image; ART is the one the system UI puts on
// the lock screen, so it wins when more than one is present.
typedef enum FFMediaAndroidCoverPriority : uint32_t {
FF_MEDIA_ANDROID_COVER_DISPLAY_ICON = 1,
FF_MEDIA_ANDROID_COVER_ALBUM_ART = 2,
FF_MEDIA_ANDROID_COVER_ART = 3,
} FFMediaAndroidCoverPriority;
typedef struct FFMediaAndroidReader {
const uint8_t* data;
size_t size;
size_t position;
} FFMediaAndroidReader;
[[gnu::nonnull(1, 2)]] static bool androidReadI32(FFMediaAndroidReader* reader, int32_t* value) {
if (reader->position + sizeof(int32_t) > reader->size) {
return false;
}
memcpy(value, reader->data + reader->position, sizeof(int32_t));
reader->position += sizeof(int32_t);
return true;
}
[[gnu::nonnull(1, 2)]] static bool androidReadI64(FFMediaAndroidReader* reader, int64_t* value) {
if (reader->position + sizeof(int64_t) > reader->size) {
return false;
}
memcpy(value, reader->data + reader->position, sizeof(int64_t));
reader->position += sizeof(int64_t);
return true;
}
// AIDL string16: an int32 code-unit count, that many UTF-16LE code units, a UTF-16 NUL terminator,
// and padding to four bytes. The terminator is walked over even though it is not part of the string,
// and so is the padding -- getting either wrong shifts every following field. A count of -1 is how
// the same field spells null.
[[gnu::nonnull(1, 2)]] static bool androidReadString16(FFMediaAndroidReader* reader, FFstrbuf* result) {
int32_t length = 0;
if (!androidReadI32(reader, &length)) {
return false;
}
if (length < 0) {
ffStrbufClear(result);
return true;
}
const size_t units = (size_t) length * 2;
const size_t padded = (units + 2 + 3) & ~(size_t) 3; // the code units, the two byte NUL, then padding
if (reader->position + padded > reader->size) {
return false;
}
const uint8_t* codeUnits = reader->data + reader->position;
reader->position += padded;
ffStrbufClear(result);
for (size_t i = 0; i < (size_t) length; i++) {
uint32_t codepoint = (uint32_t) codeUnits[i * 2] | ((uint32_t) codeUnits[i * 2 + 1] << 8);
if (codepoint >= 0xD800 && codepoint <= 0xDBFF && i + 1 < (size_t) length) {
const uint32_t low = (uint32_t) codeUnits[(i + 1) * 2] | ((uint32_t) codeUnits[(i + 1) * 2 + 1] << 8);
if (low >= 0xDC00 && low <= 0xDFFF) {
codepoint = 0x10000u + ((codepoint - 0xD800u) << 10) + (low - 0xDC00u);
++i;
}
}
ffStrbufAppendUtf32CodePoint(result, codepoint);
}
return true;
}
// ---------------------------------------------------------------------------------------------
// The artwork
// ---------------------------------------------------------------------------------------------
typedef struct FFMediaAndroidCover {
int fd; // a descriptor the reply handed over; whoever read the reply closes it
uint32_t width;
uint32_t height;
uint32_t priority;
bool found;
} FFMediaAndroidCover;
// The pixels of a Bitmap cross the transaction as an ashmem descriptor, and the descriptor's length is
// the one thing that ties the blob back to its Bitmap: Parcel::writeBlob writes the byte count in
// front of the descriptor, so that count and the descriptor's own size must agree -- and the same
// identity (`width * height * 4`) is what makes the two dimensions recoverable without knowing the
// rest of Bitmap's parcel layout, which is written by native code and has changed between releases.
//
// Nothing is assumed about where in the body the dimensions sit: the walk is over the whole body and
// the arithmetic is the filter, so a wrong candidate is rejected rather than believed. A Bitmap whose
// pixels were small enough to be written inline has no descriptor at all and answers nothing here.
[[gnu::nonnull(1, 3, 4)]] static bool androidTakeBitmap(const uint8_t* body, size_t length, const FFBinderReply* reply, FFMediaAndroidCover* cover) {
for (uint32_t i = 0; i < reply->fdCount; i++) {
const off_t size = lseek(reply->fds[i], 0, SEEK_END);
if (size <= 0 || size > (off_t) UINT32_MAX || (size % 4) != 0) {
continue;
}
size_t blobOffset = SIZE_MAX;
for (size_t offset = 0; offset + sizeof(int32_t) <= length; offset += sizeof(int32_t)) {
int32_t word = 0;
memcpy(&word, body + offset, sizeof(word));
if (word > 0 && (uint32_t) word == (uint32_t) size) {
blobOffset = offset;
break;
}
}
if (blobOffset == SIZE_MAX) {
continue;
}
for (size_t offset = 0; offset + 2 * sizeof(int32_t) <= blobOffset; offset += sizeof(int32_t)) {
int32_t width = 0;
int32_t height = 0;
memcpy(&width, body + offset, sizeof(width));
memcpy(&height, body + offset + sizeof(width), sizeof(height));
if (width < 1 || height < 1 || width > 8192 || height > 8192) {
continue;
}
if ((uint64_t) width * (uint64_t) height * 4 != (uint64_t) size) {
continue;
}
cover->fd = reply->fds[i];
cover->width = (uint32_t) width;
cover->height = (uint32_t) height;
cover->found = true;
FF_DEBUG("The cover is %dx%d, %lld bytes of pixels", width, height, (long long) size);
return true;
}
}
return false;
}
// ---------------------------------------------------------------------------------------------
// Writing the cover out
// ---------------------------------------------------------------------------------------------
[[gnu::nonnull(2)]]
static bool androidWriteAll(FFNativeFD fd, const void* data, size_t length) {
const uint8_t* bytes = data;
while (length > 0) {
const ssize_t written = write(fd, bytes, length);
if (written < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
bytes += (size_t) written;
length -= (size_t) written;
}
return true;
}
// The callback the encoder writes through. Its contract is that returning false stops it, which is
// what makes a full disk come out as no file rather than a truncated one.
typedef struct FFMediaAndroidCoverSink {
FFNativeFD fd;
size_t written;
bool failed;
} FFMediaAndroidCoverSink;
[[gnu::nonnull(1)]]
static bool androidCoverSinkWrite(void* userContext, const void* data, size_t size) {
FFMediaAndroidCoverSink* sink = userContext;
if (!androidWriteAll(sink->fd, data, size)) {
sink->failed = true;
return false;
}
sink->written += size;
return true;
}
// A Bitmap is premultiplied, and artwork without transparency is the same bytes either way -- so
// saying which it is costs one pass over the mapping and saves the encoder a bigger one. Declared
// premultiplied, AndroidBitmap_compress makes a pass over all 527 KB to unpremultiply pixels it then
// finds no alpha in: 41.1 ms against 37.6, for a file that came out byte for byte identical
// (sha256 943a6215... on all three of premultiplied, opaque and unpremultiplied). A cover that does
// carry transparency keeps the premultiplied description, and its alpha survives.
[[gnu::nonnull(1)]] static bool androidCoverIsOpaque(const uint8_t* pixels, size_t length) {
for (size_t i = 3; i < length; i += 4) {
if (pixels[i] != 0xFF) {
return false;
}
}
return true;
}
// API 30. The pixels are handed over as the mapping itself: the encoder reads them strided, so the
// cover never takes a second copy on the way in.
FF_ANDROID_REQUIRES_API(30)
static bool androidCompressCover(FFNativeFD fd, const void* pixels, uint32_t width, uint32_t height, size_t* outLength) {
// Everything the description needs was already proved by the descriptor's own size, bar one
// thing: the pixels are RGBA8888 and tightly packed, which is what a Bitmap carries, while
// whether there is an alpha worth keeping is not in the size and has to be looked for.
const AndroidBitmapInfo info = {
.width = width,
.height = height,
.stride = width * 4,
.format = ANDROID_BITMAP_FORMAT_RGBA_8888,
.flags = androidCoverIsOpaque(pixels, (size_t) width * height * 4)
? ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE
: ANDROID_BITMAP_FLAGS_ALPHA_PREMUL,
};
FFMediaAndroidCoverSink sink = { .fd = fd };
const int result = AndroidBitmap_compress(&info, ADATASPACE_SRGB, pixels, ANDROID_BITMAP_COMPRESS_FORMAT_WEBP_LOSSLESS, FF_MEDIA_ANDROID_COVER_QUALITY, &sink, androidCoverSinkWrite);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
FF_DEBUG("AndroidBitmap_compress returned %d", result);
return false;
}
*outLength = sink.written;
return !sink.failed;
}
// The descriptor holds the Bitmap's pixels and nothing else: no header, no pixel format and no
// length beyond the region's own size. Encoding them is the one thing the session service cannot do
// for this module, and the platform has the encoder -- see androidCompressCover above.
//
// What that encoder costs and produces, measured on this device with a real 363x363 cover (527076
// bytes of premultiplied RGBA) and the clock around the call alone:
//
// format quality output time
// --------------- -------- ---------- -------
// PNG any 203675 B ~370 ms quality is ignored, and all four settings
// produced byte-identical files
// WebP lossless 0 146884 B 41 ms
// WebP lossless 100 145064 B 98 ms the whole effort knob is worth 1.2%
// WebP lossy 80 22060 B 125 ms
// JPEG 90 39973 B 11 ms
//
// PNG is what the artwork usually arrives as and the worst choice here: the header documents that
// quality is ignored for it, and Skia pays for an adaptive filter pass a single image cannot
// amortise. Compressing by hand instead -- a deflate stream of stored blocks, which needs no library
// at all -- costs 2 ms but writes 527490 bytes, so the platform encoder trades 39 ms for a file 3.6
// times smaller.
//
// Declaring the pixels opaque rather than premultiplied, which the scan in androidCompressCover
// makes a truthful thing to say, is worth another 3.5 ms of the 41; the two files are identical, so
// what is being bought is only the encoder's pass in search of an alpha that is not there.
//
// Lossless WebP is the format, for three measured reasons: it is nine times faster than PNG, it is
// 3.6 times smaller than what the hand-written encoder produced, and being lossless it keeps alpha --
// which matters, because a Bitmap is premultiplied and an encoder that dropped the alpha without
// compositing would leave the premultiplied colour behind and turn what was transparent black. Every
// reader here takes it as well: the iTerm and kitty logo paths hand the file over as it is for the
// terminal to decode, and AImageDecoder, which reads it back inside fastfetch, handles it too.
//
// TMPDIR is what Termux exports, and it is the one directory the terminal that will draw the logo
// can also read; /data/local/tmp is the answer when fastfetch was started by something that exports
// nothing. That second one belongs to the shell UID and root, and Termux runs this module as an app
// UID for which it is not writable, so the candidates are tested rather than trusted.
[[gnu::nonnull(1)]] static const char* androidCoverPath(char* buffer, size_t size) {
const char* directories[] = { getenv("TMPDIR"), "/data/local/tmp" };
for (size_t i = 0; i < sizeof(directories) / sizeof(directories[0]); i++) {
const char* directory = directories[i];
if (directory == nullptr || directory[0] != '/') {
continue;
}
if (access(directory, W_OK) != 0) {
continue;
}
const int length = snprintf(buffer, size, "%s/fastfetch-media-cover-%d.webp", directory, (int) getpid());
if (length <= 0 || (size_t) length >= size) {
continue;
}
return buffer;
}
return nullptr;
}
// The file belongs to this process, and this is the only place that knows its name: media.c removes
// it once the logo is drawn, which is what removeCoverAfterUse asks for, and a half-written file
// would otherwise never be removed by anyone.
[[gnu::nonnull(1)]] static void androidWriteCover(FFMediaResult* result, int sourceFD, uint32_t width, uint32_t height) {
const off_t size = lseek(sourceFD, 0, SEEK_END);
if (size <= 0 || (uint64_t) size != (uint64_t) width * (uint64_t) height * 4) {
return;
}
// AndroidBitmap_compress is API 30, and the build reaches it through a weak reference, so this
// is a run-time branch rather than a compile-time constant. There is no second encoder behind
// it: AImageDecoder, the only thing on Android that can read a cover back, is API 30 as well, so
// a file written below that would be one whose reader does not exist.
if (FF_ANDROID_API_AT_LEAST(30)) {
void* map = mmap(nullptr, (size_t) size, PROT_READ, MAP_SHARED, sourceFD, 0);
if (map == MAP_FAILED) {
return;
}
char path[256];
const char* file = androidCoverPath(path, sizeof(path));
FF_AUTO_CLOSE_FD FFNativeFD fd = file != nullptr ? open(file, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) : -1;
size_t length = 0;
const bool written = ffIsValidNativeFD(fd) && androidCompressCover(fd, map, width, height, &length);
munmap(map, (size_t) size);
if (written) {
ffStrbufSetS(&result->cover, file);
result->removeCoverAfterUse = true;
FF_DEBUG("The %ux%u cover was written to %s, %zu bytes", width, height, file, (size_t) length);
} else if (file != nullptr) {
ffRemoveFile(file);
}
}
}
// ---------------------------------------------------------------------------------------------
// The session
// ---------------------------------------------------------------------------------------------
// Every getter on ISessionController takes no argument, so a call is the interface token and nothing
// else; `flags` is passed through because one of them needs TF_ACCEPT_FDS.
[[gnu::nonnull(1, 5)]] static const char* androidCall(FFBinder* binder, uint32_t handle, uint32_t code, uint32_t flags, FFBinderReply* reply) {
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_CONTROLLER_DESCRIPTOR);
const char* error = ffBinderTransact(binder, handle, code, flags, &parcel, reply);
if (error != nullptr) {
return error;
}
if (ffBinderReplyIsStatus(reply)) {
// onTransact() answered with a raw status_t instead of a parcel. -74 is UNKNOWN_TRANSACTION,
// which is what a code this build of the interface does not have comes back as.
return ffBinderReadI32(reply->data, reply->size, 0) == -74
? "The session does not have that method"
: "The session rejected the request";
}
// Every reply starts with the exception code AIDL writes, so nothing past it is read before this
// has been checked.
const int32_t exception = ffBinderReadI32(reply->data, reply->size, 0);
if (exception != 0) {
FF_DEBUG("Transaction %u came back as exception %d over %zu bytes", code, exception, reply->size);
return "The session raised an exception";
}
return nullptr;
}
[[gnu::nonnull(1, 4, 5, 6, 7)]]
static const char* androidReadMetadataEntry(FFMediaAndroidReader* reader, size_t bundleEnd, int32_t type, const FFstrbuf* key, FFMediaResult* result, const FFBinderReply* reply, FFMediaAndroidCover* cover) {
switch (type) {
case FF_MEDIA_ANDROID_VAL_NULL:
return nullptr;
case FF_MEDIA_ANDROID_VAL_STRING:
case FF_MEDIA_ANDROID_VAL_CHAR_SEQUENCE: {
FF_STRBUF_AUTO_DESTROY value = ffStrbufCreate();
if (!androidReadString16(reader, &value)) {
return "The metadata carries a truncated string";
}
if (value.length == 0) {
return nullptr;
}
// DISPLAY_TITLE and DISPLAY_SUBTITLE are the fallbacks Android documents for a session
// whose structured metadata is incomplete -- a radio stream, typically -- and an empty
// TITLE is exactly the case they exist for.
if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_TITLE)) {
ffStrbufSet(&result->song, &value);
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_DISPLAY_TITLE)) {
if (result->song.length == 0) {
ffStrbufSet(&result->song, &value);
}
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_ARTIST)) {
ffStrbufSet(&result->artist, &value);
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_DISPLAY_SUBTITLE)) {
if (result->artist.length == 0) {
ffStrbufSet(&result->artist, &value);
}
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_ALBUM)) {
ffStrbufSet(&result->album, &value);
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_MEDIA_URI)) {
ffStrbufSet(&result->url, &value);
}
return nullptr;
}
case FF_MEDIA_ANDROID_VAL_LONG: {
int64_t value = 0;
if (!androidReadI64(reader, &value)) {
return "The metadata carries a truncated long";
}
// METADATA_KEY_DURATION is milliseconds, where MPRIS answers microseconds -- the one
// field that has to be read differently on each platform.
if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_DURATION) && value > 0 && value <= (int64_t) UINT32_MAX) {
result->length = (uint32_t) value;
}
return nullptr;
}
case FF_MEDIA_ANDROID_VAL_INTEGER: {
int32_t value = 0;
if (!androidReadI32(reader, &value)) {
return "The metadata carries a truncated int";
}
return nullptr;
}
case FF_MEDIA_ANDROID_VAL_PARCELABLE: {
// Parcel.writeValue length-prefixes a Parcelable with the byte size of everything that
// follows, and that size is what makes an unknown class skippable: the walk resumes at the
// end of the payload instead of having to understand it.
int32_t payloadLength = 0;
if (!androidReadI32(reader, &payloadLength) || payloadLength < 0) {
return "The metadata carries a malformed parcelable";
}
const size_t payloadEnd = reader->position + (size_t) payloadLength;
if (payloadEnd > bundleEnd) {
return "The metadata carries a parcelable past the end of the bundle";
}
FF_STRBUF_AUTO_DESTROY className = ffStrbufCreate();
if (!androidReadString16(reader, &className)) {
return "The metadata carries a truncated parcelable name";
}
if (ffStrbufEqualS(&className, FF_MEDIA_ANDROID_CLASS_BITMAP)) {
uint32_t priority = 0;
if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_ART)) {
priority = FF_MEDIA_ANDROID_COVER_ART;
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_ALBUM_ART)) {
priority = FF_MEDIA_ANDROID_COVER_ALBUM_ART;
} else if (ffStrbufEqualS(key, FF_MEDIA_ANDROID_KEY_DISPLAY_ICON)) {
priority = FF_MEDIA_ANDROID_COVER_DISPLAY_ICON;
}
if (priority > cover->priority) {
FFMediaAndroidCover candidate = { .fd = -1 };
if (androidTakeBitmap(reader->data + reader->position, payloadEnd - reader->position, reply, &candidate)) {
candidate.priority = priority;
*cover = candidate;
}
}
}
reader->position = payloadEnd;
return nullptr;
}
default:
// Anything else -- a map, a list, an array -- would have to be walked by shape to be
// skipped, and a MediaMetadata Bundle does not carry one. Stopping here is what keeps the
// walk from reading a wrong number of bytes and carrying on with nonsense.
FF_DEBUG("The metadata carries an unsupported value type %d", type);
return "The metadata carries a value this build cannot read";
}
}
[[gnu::nonnull(1, 2, 3, 4)]]
static const char* androidReadMetadataBundle(FFMediaAndroidReader* reader, FFMediaResult* result, const FFBinderReply* reply, FFMediaAndroidCover* cover) {
int32_t bundleLength = 0;
if (!androidReadI32(reader, &bundleLength) || bundleLength <= 0) {
return "The metadata bundle is empty";
}
const size_t bundleEnd = reader->position + (size_t) bundleLength;
if (bundleEnd > reader->size) {
return "The metadata bundle runs past the end of the reply";
}
int32_t magic = 0;
if (!androidReadI32(reader, &magic) || (uint32_t) magic != FF_MEDIA_ANDROID_BUNDLE_MAGIC) {
return "The metadata is not a bundle";
}
int32_t count = 0;
if (!androidReadI32(reader, &count)) {
return "The metadata bundle has no entry count";
}
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
int32_t read = 0;
for (; read < count && reader->position < bundleEnd; read++) {
if (!androidReadString16(reader, &key)) {
return "The metadata carries a truncated key";
}
int32_t type = 0;
if (!androidReadI32(reader, &type)) {
return "The metadata carries a value without a type";
}
const char* error = androidReadMetadataEntry(reader, bundleEnd, type, &key, result, reply, cover);
if (error != nullptr) {
return error;
}
}
// The count in the header is the check on the walk: a discrepancy means a type this build reads the
// wrong width for, and a partially read title is worse than none.
if (read != count) {
FF_DEBUG("The metadata bundle declared %d entries but only %d could be read", count, read);
return "The metadata bundle did not read back in full";
}
return nullptr;
}
// PlaybackState.STATE_*, which is what getPlaybackState answers with. The three names MPRIS uses are
// kept identical -- Playing, Paused, Stopped -- so that a format string written for the Linux or the
// Windows implementation keeps working.
static const char* androidPlaybackStateName(int32_t state) {
switch (state) {
case 0: return "None";
case 1: return "Stopped";
case 2: return "Paused";
case 3: return "Playing";
case 4: return "FastForwarding";
case 5: return "Rewinding";
case 6: return "Buffering";
case 7: return "Error";
case 8: return "Connecting";
case 9: return "SkippingToPrevious";
case 10: return "SkippingToNext";
case 11: return "SkippingToQueueItem";
default: return "Unknown";
}
}
static uint32_t androidPlaybackStateScore(int32_t state) {
// Playing first, then the states that are on their way to it, then paused. A session that is
// stopped or has no state at all is only reported when nothing else is there.
switch (state) {
case 3: return 4; // PLAYING
case 4: case 5: case 6: case 8: case 9: case 10: case 11: return 3; // moving, buffering, connecting
case 2: return 2; // PAUSED
case 1: case 7: return 1; // STOPPED, ERROR
default: return 0;
}
}
// SystemClock.elapsedRealtime(), which is the clock PlaybackState.updateTime is stamped with. It is
// CLOCK_BOOTTIME and not CLOCK_MONOTONIC: a phone suspends with music playing often enough that the
// two differ by hours, and the difference would be read as a position that never advances.
static int64_t androidElapsedRealtime(void) {
struct timespec now = {};
if (clock_gettime(CLOCK_BOOTTIME, &now) != 0) {
return 0;
}
return (int64_t) now.tv_sec * 1000 + (int64_t) (now.tv_nsec / 1000000);
}
// The reply is walked only as far as the speed: everything past it is a typed list of custom actions,
// an item id, a CharSequence and a Bundle, none of which this module reports. `length` therefore has
// to have been read already -- the clamping below needs it -- which is why the metadata pass runs
// first.
[[gnu::nonnull(1, 2)]] static const char* androidReadPlaybackState(FFMediaAndroidReader* reader, FFMediaResult* result) {
int32_t state = 0;
if (!androidReadI32(reader, &state) || state == 0) {
return nullptr; // the session has no state yet
}
int64_t position = 0;
if (!androidReadI64(reader, &position)) {
return "The playback state is truncated";
}
float speed = 0;
if (reader->position + sizeof(float) > reader->size) {
return "The playback state is truncated";
}
memcpy(&speed, reader->data + reader->position, sizeof(speed));
reader->position += sizeof(speed);
int64_t updateTime = 0;
if (!androidReadI64(reader, &updateTime)) {
return "The playback state is truncated";
}
// `position` is the position *at* updateTime, so a snapshot taken now has to be advanced by the
// time that has passed since -- otherwise a player that only refreshes its state once per track
// would report the same position for the whole of it. Only a playing session advances: a paused
// one is exactly where it says it is.
int64_t current = position;
if (state == 3 && speed > 0) {
const int64_t now = androidElapsedRealtime();
if (now > updateTime) {
current += (int64_t) ((double) (now - updateTime) * (double) speed);
}
}
if (current < 0) {
current = 0;
}
if (result->length > 0 && current > (int64_t) result->length) {
current = result->length;
}
if (current > 0 && current <= (int64_t) UINT32_MAX) {
result->position = (uint32_t) current;
}
ffStrbufSetS(&result->status, androidPlaybackStateName(state));
return nullptr;
}
static void androidReadPackageName(FFBinder* binder, uint32_t token, FFMediaResult* result) {
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PACKAGE_NAME, 0, &reply);
if (error != nullptr) {
FF_DEBUG("getPackageName failed: %s", error);
return;
}
FFMediaAndroidReader reader = { .data = reply.data, .size = reply.size, .position = 4 };
if (!androidReadString16(&reader, &result->playerId)) {
return;
}
// `player` wants the name the app shows, and there is no way to get one from here: `cmd package
// dump` for this app is 1.2 MB and carries `labelRes`, a resource id, rather than a name, and
// resolving it would mean reading the app's resource table. The package name is what the
// notification and the settings screen fall back to as well, so both fields get it -- the same
// fallback media_linux.c uses when MPRIS answers without an Identity.
ffStrbufSet(&result->player, &result->playerId);
}
static void androidReadMetadata(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) {
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
// TF_ACCEPT_FDS is not a hint and not optional here: the artwork arrives as a descriptor, and a
// request that did not ask for one is answered with BR_FAILED_REPLY -- which reads like a service
// that is not running rather than like a missing flag. It is passed unconditionally, because the
// alternative is a failure that depends on whether the player happened to publish artwork.
const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_METADATA, TF_ACCEPT_FDS, &reply);
FFMediaAndroidCover cover = { .fd = -1 };
if (error != nullptr) {
FF_DEBUG("getMetadata failed: %s", error);
} else if (ffBinderReadI32(reply.data, reply.size, 4) != 0) {
FFMediaAndroidReader reader = { .data = reply.data, .size = reply.size, .position = 8 };
const char* parseError = androidReadMetadataBundle(&reader, result, &reply, &cover);
if (parseError != nullptr) {
FF_DEBUG("The metadata bundle could not be read: %s", parseError);
}
}
if (cover.found && saveCover) {
androidWriteCover(result, cover.fd, cover.width, cover.height);
}
// Every descriptor the reply carried is this side's to close, the cover's included: the pixels
// have been read out by now, and the one no cover came from would otherwise leak.
for (uint32_t i = 0; i < reply.fdCount; i++) {
close(reply.fds[i]);
}
}
static void androidReadSession(FFBinder* binder, uint32_t token, FFMediaResult* result, bool saveCover) {
androidReadPackageName(binder, token, result);
// The length has to be known before the position is placed, so the metadata comes first.
androidReadMetadata(binder, token, result, saveCover);
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
const char* error = androidCall(binder, token, FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply);
if (error == nullptr && ffBinderReadI32(reply.data, reply.size, 4) != 0) {
FFMediaAndroidReader reader = { .data = reply.data, .size = reply.size, .position = 8 };
error = androidReadPlaybackState(&reader, result);
}
if (error != nullptr) {
FF_DEBUG("getPlaybackState failed: %s", error);
}
}
// ---------------------------------------------------------------------------------------------
// Everything that goes through the media_session service. A reason is returned when it came up empty;
// a song in `media` means it did not.
//
// There is deliberately no uid test in front of this. `getSessions` is refused for an app UID, but that
// is the service's decision to make and it is not the only way in -- a notification listener is let
// through too, an OEM can loosen the check, a release can change it -- so the module asks and reads the
// answer. That costs one transaction, and it keeps the module working wherever the check happens to
// pass, instead of hard-coding today's policy into the client.
static const char* androidDetectMediaSession(FFMediaResult* media, bool saveCover) {
[[gnu::cleanup(ffBinderClose)]] FFBinder binder = { .fd = -1 };
const char* error = ffBinderOpen(&binder);
if (error != nullptr) {
return error;
}
[[gnu::cleanup(ffBinderServiceHandleRelease)]] FFBinderServiceHandle service = { .binder = &binder };
error = ffBinderLookupService(&binder, FF_MEDIA_ANDROID_SERVICE, FF_BINDER_SM_GET_SERVICE, &service.handle);
if (error != nullptr) {
FF_DEBUG("Looking up \"%s\" failed: %s", FF_MEDIA_ANDROID_SERVICE, error);
return error;
}
// A phone can have several sessions at once -- a music player and a video one, say -- and only one
// of them is the answer. Which one is decided first, by state alone, so that the metadata and the
// artwork are fetched exactly once, for the winner.
uint32_t tokens[FF_BINDER_MAX_HANDLES];
uint32_t tokenCount = 0;
// Raised as soon as ffBinderTransact() has replied, because from that point on it is holding a
// strong reference to every handle it collected and the kernel has installed every descriptor it
// delivered into our own fd table. Any early return past that line has to give them back, so one
// cleanup attribute does it rather than a release call repeated at each exit.
//
// `replied` is what decides: ffBinderTransact() acquires the handles before it can fail -- it
// returns an error when the reply did not fit the caller buffer, and the tail that carries
// BC_ACQUIRE runs regardless -- so releasing without the flag would drop one reference too many.
bool replied = false;
[[gnu::cleanup(ffBinderAcquiredRelease)]] FFBinderAcquired acquired = { .binder = &binder, .replied = &replied };
{
uint8_t request[FF_MEDIA_ANDROID_REQUEST_SIZE];
FFBinderParcel parcel = ffBinderParcelCreate(request, sizeof(request));
ffBinderParcelPutInterfaceToken(&parcel, FF_MEDIA_ANDROID_SESSIONS_DESCRIPTOR);
ffBinderParcelPutI32(&parcel, 0); // ComponentName: null asks for every session
ffBinderParcelPutI32(&parcel, 0); // userId: the single user a phone has
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
error = ffBinderTransact(&binder, service.handle, FF_MEDIA_ANDROID_GET_SESSIONS, 0, &parcel, &reply);
replied = true;
if (error != nullptr) {
return error;
}
if (ffBinderReplyIsStatus(&reply)) {
return "The media session service rejected the request";
}
const int32_t exception = ffBinderReadI32(reply.data, reply.size, 0);
if (exception != 0) {
// -1 is EX_SECURITY, which is what a UID the service will not read sessions for gets. The
// number goes to the debug log rather than into the message: what tells the two cases apart
// for the user is which UID is asking.
FF_DEBUG("getSessions came back as exception %d%s", exception, exception == -1 ? " (EX_SECURITY)" : "");
return ffAndroidIsRootOrShell(instance.state.platform.uid)
? "The media session service refused the request"
: "Reading the media session needs the shell UID or root: an app UID is refused by the media_session service";
}
// The tokens arrive as flat binder objects, which ffBinderTransact() collects in payload order
// -- the same order the descriptors of a metadata reply arrive in, which is what makes the two
// line up without walking the list itself.
tokenCount = reply.handleCount;
if (tokenCount > FF_BINDER_MAX_HANDLES) {
tokenCount = FF_BINDER_MAX_HANDLES;
}
for (uint32_t i = 0; i < tokenCount; i++) {
tokens[i] = reply.handles[i];
}
acquired.handles = tokens;
acquired.handleCount = tokenCount;
// Every descriptor the reply delivered is ours to close. The art of a session is read through
// one of them, and reading it happens over the fd number rather than over the file, so closing
// it on the way out does not cut the read short.
acquired.fds = reply.fds;
acquired.fdCount = reply.fdCount;
}
if (tokenCount == 0) {
return "No active media session";
}
uint32_t best = 0;
uint32_t bestScore = 0;
for (uint32_t i = 0; i < tokenCount; i++) {
FFBinderReply reply = ffBinderReplyCreate(s_reply, sizeof(s_reply));
if (androidCall(&binder, tokens[i], FF_MEDIA_ANDROID_GET_PLAYBACK_STATE, 0, &reply) != nullptr) {
continue;
}
// The presence marker first: a session with no state yet answers a null parcelable, and its
// state field is not there to be read.
const int32_t state = ffBinderReadI32(reply.data, reply.size, 4) != 0 ? ffBinderReadI32(reply.data, reply.size, 8) : 0;
const uint32_t score = androidPlaybackStateScore(state);
if (score > bestScore) {
bestScore = score;
best = i;
}
}
androidReadSession(&binder, tokens[best], media, saveCover);
// The tokens were acquired by ffBinderTransact() and are given back by the cleanup attribute above.
return nullptr;
}
// ---------------------------------------------------------------------------------------------
// The MPRIS implementation in media_linux.c, whose entry point is renamed on Android because this file
// owns ffDetectMediaImpl -- the same arrangement as wallpaper_linux.c and terminalfont_linux.c.
void ffDetectMediaLinuxImpl(FFMediaResult* media, bool saveCover);
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
// The binder route below reads Android's own now-playing session, which is the one the phone is
// playing only while SurfaceFlinger is the display server. A Termux:X11 or Wayland session runs a
// real compositor on top of the device, and what is playing there belongs to that desktop, not to
// Android -- so that case is handed to the Linux implementation, which knows the MPRIS bus. Only
// one of the two answers, never merged.
const FFDisplayServerResult* wm = ffConnectDisplayServer();
if (!ffStrbufIgnCaseEqualS(&wm->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) {
FF_DEBUG("The display server is \"%s\", so the Linux implementation answers", wm->wmProtocolName.chars);
ffDetectMediaLinuxImpl(media, saveCover);
return;
}
const char* error = androidDetectMediaSession(media, saveCover);
if (media->song.length > 0) {
return; // the phone's own now-playing session answered
}
// Nothing came out of the media_session service, so try the session bus. That is worth trying even
// with SurfaceFlinger on screen: the service refuses an app UID outright, and a player inside a
// chroot is not an Android media session at all, so nothing else here would ever see it.
//
// The reason is copied out first, because the fallback writes to media->error itself.
char sessionError[160] = "";
if (error != nullptr) {
snprintf(sessionError, sizeof(sessionError), "%s", error);
}
FF_DEBUG("The media session gave nothing (%s), falling back to MPRIS", sessionError[0] != '\0' ? sessionError : "no session");
// Artwork without a song to go with it: the file belongs to the track MPRIS is about to report, so
// it is removed here rather than at exit, where media.c would by then have forgotten the path.
if (media->cover.length > 0) {
ffRemoveFile(media->cover.chars);
ffStrbufClear(&media->cover);
media->removeCoverAfterUse = false;
}
ffStrbufClear(&media->error);
ffDetectMediaLinuxImpl(media, saveCover);
// The phone is what was asked about, so its reason is the one to print: it names something a user
// can act on, while "Failed to connect to DBus" only explains why the second opinion was never
// taken -- and most phones have no session bus at all, which would make that the usual answer.
// MPRIS never reports anything when it merely finds no player, so a silent fallback here means
// nothing is playing anywhere.
if (media->song.length == 0) {
ffStrbufSetS(&media->error, sessionError[0] != '\0' ? sessionError : "No active media session");
}
}
+27 -11
View File
@@ -111,7 +111,14 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
}
#if !FF_MODULE_DISABLE_MEDIA
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(int saveCover) {
// Called from the Perl helper process through `DynaLoader::dl_install_xsub`. That mechanism installs a
// plain C function as an XSUB, so Perl calls it with the `CV*` in the first parameter slot and no Perl
// argument ever reaches a parameter — a `bool saveCover` parameter read as a pointer and was always
// truthy. The flag has to travel in the environment instead; see `getMediaByAuthorizedProcess`.
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(void) {
const char* saveCoverEnv = getenv("FF_MEDIA_REMOTE_SAVE_COVER");
bool saveCover = saveCoverEnv && saveCoverEnv[0] == '1';
FFMediaResult media = {
.status = ffStrbufCreate(),
.song = ffStrbufCreate(),
@@ -121,7 +128,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
.player = ffStrbufCreate(),
.cover = ffStrbufCreate(),
};
if (getMediaByMediaRemote(&media, !!saveCover) != nullptr) {
if (getMediaByMediaRemote(&media, saveCover) != nullptr) {
return 1;
}
ffStrbufAppendC(&media.status, '\n');
@@ -156,13 +163,17 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
// #1737
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("require DynaLoader;\
// `DynaLoader::dl_install_xsub` hands the C function the `CV*` as its first parameter, so the flag
// cannot be passed as a Perl argument — `ffPrintMediaByMediaRemote` reads it from the environment,
// which the child process inherits.
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("$ENV{'FF_MEDIA_REMOTE_SAVE_COVER'}='%c';\
require DynaLoader;\
my $so = DynaLoader::dl_load_file('%s', 0);\
my $sym = DynaLoader::dl_find_symbol($so, 'ffPrintMediaByMediaRemote');\
DynaLoader::dl_install_xsub('fn', $sym, __FILE__);\
exit(fn(%c))",
instance.state.platform.exePath.chars,
saveCover ? '1' : '0');
exit(fn())",
saveCover ? '1' : '0',
instance.state.platform.exePath.chars);
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
const char* error = ffProcessAppendStdOut(
@@ -201,12 +212,17 @@ void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
}
if (error) {
ffStrbufAppendS(&media->error, error);
} else if (media->player.length == 0 && media->playerId.length > 0) {
ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
} else {
if (media->player.length == 0 && media->playerId.length > 0) {
ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
}
ffStrbufReplaceAllC(&media->player, '.', ' ');
}
ffStrbufReplaceAllC(&media->player, '.', ' ');
// The cover is a temporary file that this process created, so it is ours to delete. This used
// to be flagged only in the branch above, which meant the file was left behind on every run
// whose player name was already known — that is, almost all of them.
if (media->cover.length > 0) {
media->removeCoverAfterUse = true;
}
+11 -1
View File
@@ -277,7 +277,17 @@ static const char* getMedia(FFMediaResult* result) {
#endif
void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
// Android compiles this file next to media_android.c, which owns ffDetectMediaImpl and calls this one
// when the media_session service has nothing to say -- an app UID is refused by that service, while the
// session bus a desktop environment brings up is open to every process in it. Same arrangement as
// wallpaper_linux.c and terminalfont_linux.c.
void
#ifdef __ANDROID__
ffDetectMediaLinuxImpl
#else
ffDetectMediaImpl
#endif
(FFMediaResult* media, bool saveCover) {
FF_UNUSED(saveCover); // We don't save the cover to a file for Mpris implementation
#ifdef FF_HAVE_DBUS
const char* error = getMedia(media);
-5
View File
@@ -1,5 +0,0 @@
#include "media.h"
void ffDetectMediaImpl(FFMediaResult* media) {
ffStrbufAppendS(&media->error, "Not supported on this platform");
}
+11 -100
View File
@@ -6,10 +6,9 @@ extern "C" {
}
#if FF_HAVE_WINRT
#include <roapi.h>
#include "common/windows/winrt.hpp"
#include <robuffer.h>
#include <winstring.h>
#include <asyncinfo.h>
#include <shobjidl.h>
#include <shlobj.h>
@@ -21,94 +20,6 @@ extern "C" {
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.Storage.Streams.h>
using winrt::impl::abi_t;
using winrt::Windows::Foundation::IAsyncOperation;
using winrt::Windows::Foundation::IAsyncOperationWithProgress;
static inline void deleteHstring(HSTRING* pstr) {
if (*pstr) {
WindowsDeleteString(*pstr);
}
}
static inline void ffStrbufSetHstring(FFstrbuf* destination, HSTRING value) {
uint32_t length;
const wchar_t* raw = WindowsGetStringRawBuffer(value, &length);
ffStrbufSetNWS(destination, length, raw);
}
template <typename Interface>
static inline HRESULT ffGetActivationFactory(const wchar_t* className, REFIID iid, Interface** factory) {
HSTRING_HEADER header;
HSTRING runtimeClass;
HRESULT hr = WindowsCreateStringReference(className, (UINT32)::wcslen(className), &header, &runtimeClass);
if (FAILED(hr)) {
return hr;
}
return RoGetActivationFactory(runtimeClass, iid, reinterpret_cast<void**>(factory));
}
template <typename TargetProjection, typename SourceAbi>
static inline HRESULT ffQueryInterface(SourceAbi* source, abi_t<TargetProjection>** target) {
return source->QueryInterface(winrt::guid_of<TargetProjection>(), reinterpret_cast<void**>(target));
}
template <typename TOperationAbi, typename TResultAbi>
static HRESULT ffWaitForAsyncOperation(TOperationAbi* operation, TResultAbi** result) {
FF_AUTO_RELEASE_COM_OBJECT IAsyncInfo* asyncInfo = nullptr;
HRESULT hr = ffQueryInterface<IAsyncInfo>(operation, &asyncInfo);
if (FAILED(hr)) {
return hr;
}
AsyncStatus status = AsyncStatus::Started;
for (;;) {
hr = asyncInfo->get_Status(&status);
if (FAILED(hr)) {
return hr;
}
if (status == AsyncStatus::Started) {
ffTimeSleep(0);
} else {
break;
}
}
if (status != AsyncStatus::Completed) {
HRESULT errorCode = E_FAIL;
asyncInfo->get_ErrorCode(&errorCode);
return FAILED(errorCode) ? errorCode : E_FAIL;
}
return operation->GetResults((void**) result);
}
template <typename TResultProjection, typename TOperation>
static HRESULT ffRunAndWait(TOperation&& operation, abi_t<TResultProjection>** result) {
FF_AUTO_RELEASE_COM_OBJECT abi_t<IAsyncOperation<TResultProjection>>* opResult = nullptr;
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
if (FAILED(hr) || !opResult) {
return hr;
}
return ffWaitForAsyncOperation(opResult, result);
}
template <typename TResultProjection, typename TOperation>
static HRESULT ffRunAndWait2(TOperation&& operation, abi_t<TResultProjection>** result) {
*result = nullptr;
FF_AUTO_RELEASE_COM_OBJECT abi_t<IAsyncOperationWithProgress<TResultProjection, int32_t>>* opResult = nullptr;
HRESULT hr = operation(reinterpret_cast<void**>(&opResult));
if (FAILED(hr) || !opResult) {
return hr;
}
return ffWaitForAsyncOperation(opResult, result);
}
static HRESULT ffSaveThumbnailToTempPath(
abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail,
FFstrbuf* destination) {
@@ -138,7 +49,7 @@ static HRESULT ffSaveThumbnailToTempPath(
}
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Storage::Streams::IBufferFactory>* bufferFactory = nullptr;
hr = ffGetActivationFactory(L"Windows.Storage.Streams.Buffer", winrt::guid_of<winrt::Windows::Storage::Streams::IBufferFactory>(), &bufferFactory);
hr = ffGetActivationFactory<winrt::Windows::Storage::Streams::IBufferFactory>(L"Windows.Storage.Streams.Buffer", &bufferFactory);
if (FAILED(hr)) {
return hr;
}
@@ -223,7 +134,7 @@ static HRESULT ffSaveThumbnailToTempPath(
// are actually packaged -- the caller gates it, see `resolveAppUserModelId`.
static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result) {
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::ApplicationModel::IAppInfoStatics>* statics = nullptr;
if (FAILED(ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", winrt::guid_of<winrt::Windows::ApplicationModel::IAppInfoStatics>(), &statics)) || !statics) {
if (FAILED(ffGetActivationFactory<winrt::Windows::ApplicationModel::IAppInfoStatics>(L"Windows.ApplicationModel.AppInfo", &statics)) || !statics) {
return false;
}
@@ -243,7 +154,7 @@ static bool resolvePackagedAppUserModelId(const wchar_t* aumid, FFstrbuf* result
return false;
}
[[gnu::cleanup(deleteHstring)]] HSTRING displayName = nullptr;
[[gnu::cleanup(ffDeleteHstring)]] HSTRING displayName = nullptr;
if (FAILED(displayInfo->get_DisplayName(reinterpret_cast<void**>(&displayName))) || !displayName) {
return false;
}
@@ -316,7 +227,7 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) {
do {
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>* managerStatics = nullptr;
HRESULT hr = ffGetActivationFactory(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", winrt::guid_of<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(), &managerStatics);
HRESULT hr = ffGetActivationFactory<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", &managerStatics);
if (FAILED(hr) || !managerStatics) {
error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed";
break;
@@ -332,7 +243,7 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) {
break;
}
[[gnu::cleanup(deleteHstring)]] HSTRING playerId = nullptr;
[[gnu::cleanup(ffDeleteHstring)]] HSTRING playerId = nullptr;
FF_AUTO_RELEASE_COM_OBJECT abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* session = nullptr;
if (instance.config.general.playerName.length) {
@@ -367,7 +278,7 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) {
currentSession = nullptr; // Don't release the session object
break;
}
deleteHstring(&playerId);
ffDeleteHstring(&playerId);
ffStrbufClear(&result->playerId);
}
@@ -433,17 +344,17 @@ static const char* getMedia(FFMediaResult* result, bool saveCover) {
}
}
[[gnu::cleanup(deleteHstring)]] HSTRING title = nullptr;
[[gnu::cleanup(ffDeleteHstring)]] HSTRING title = nullptr;
if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast<void**>(&title)))) {
ffStrbufSetHstring(&result->song, title);
}
[[gnu::cleanup(deleteHstring)]] HSTRING artist = nullptr;
[[gnu::cleanup(ffDeleteHstring)]] HSTRING artist = nullptr;
if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast<void**>(&artist)))) {
ffStrbufSetHstring(&result->artist, artist);
}
[[gnu::cleanup(deleteHstring)]] HSTRING album = nullptr;
[[gnu::cleanup(ffDeleteHstring)]] HSTRING album = nullptr;
if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast<void**>(&album)))) {
ffStrbufSetHstring(&result->album, album);
}
-5
View File
@@ -1,5 +0,0 @@
#include "memory.h"
const char* ffDetectMemory([[maybe_unused]] FFMemoryResult* ram) {
return "Not supported on this platform";
}
+43 -16
View File
@@ -1,21 +1,48 @@
#include "os.h"
#include "common/FFcache.h"
void ffDetectOSImpl(FFOSResult* os);
const FFOSResult* ffDetectOS(void) {
static FFOSResult result;
if (result.name.chars == nullptr) {
ffStrbufInit(&result.name);
ffStrbufInit(&result.prettyName);
ffStrbufInit(&result.id);
ffStrbufInit(&result.version);
ffStrbufInit(&result.versionID);
ffStrbufInit(&result.codename);
ffStrbufInit(&result.buildID);
ffStrbufInit(&result.idLike);
ffStrbufInit(&result.variant);
ffStrbufInit(&result.variantID);
ffDetectOSImpl(&result);
}
return &result;
static FFOSResult result;
static void initOSResult(void* storage) {
FFOSResult* os = storage;
ffStrbufInit(&os->name);
ffStrbufInit(&os->prettyName);
ffStrbufInit(&os->id);
ffStrbufInit(&os->version);
ffStrbufInit(&os->versionID);
ffStrbufInit(&os->codename);
ffStrbufInit(&os->buildID);
ffStrbufInit(&os->idLike);
ffStrbufInit(&os->variant);
ffStrbufInit(&os->variantID);
ffDetectOSImpl(os);
}
static void destroyOSResult(void* storage) {
FFOSResult* os = storage;
ffStrbufDestroy(&os->name);
ffStrbufDestroy(&os->prettyName);
ffStrbufDestroy(&os->id);
ffStrbufDestroy(&os->version);
ffStrbufDestroy(&os->versionID);
ffStrbufDestroy(&os->codename);
ffStrbufDestroy(&os->buildID);
ffStrbufDestroy(&os->idLike);
ffStrbufDestroy(&os->variant);
ffStrbufDestroy(&os->variantID);
}
static FFcacheEntry ffCacheEntryOS = {
.name = "os",
.storage = &result,
.init = initOSResult,
.destroy = destroyOSResult,
};
const FFOSResult* ffDetectOS(void) {
return ffCacheGet(&ffCacheEntryOS);
}
+1
View File
@@ -44,6 +44,7 @@ typedef struct FFPackagesResult {
uint32_t pkgtool;
uint32_t porg;
uint32_t rpm;
uint32_t rum;
uint32_t scoopGlobal;
uint32_t scoopUser;
uint32_t snap;
+3
View File
@@ -528,6 +528,9 @@ static void getPackageCounts(FFstrbuf* baseDir, FFPackagesResult* packageCounts,
// `Sigmd5` is the only table that doesn't contain the virtual `gpg-pubkey` package
packageCounts->rpm += getSQLite3Int(baseDir, "/var/lib/rpm/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rpm");
}
if (FF_PACKAGES_IS_ENABLED(options, RUM)) {
packageCounts->rum += getSQLite3Int(baseDir, "/var/lib/rakuos/rum-rpmdb/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rum");
}
if (FF_PACKAGES_IS_ENABLED(options, SNAP)) {
packageCounts->snap += getSnap(baseDir);
}
@@ -1,4 +0,0 @@
#include "packages.h"
void ffDetectPackagesImpl([[maybe_unused]] FFPackagesResult* result, [[maybe_unused]] FFPackagesOptions* options) {
}
@@ -1,5 +0,0 @@
#include "processes.h"
const char* ffDetectProcesses([[maybe_unused]] const FFProcessesOptions* options, [[maybe_unused]] FFProcessesResult* result) {
return "Not supported on this platform";
}
+5 -2
View File
@@ -103,13 +103,16 @@ const char* ffDetectPublicIp(FFPublicIPOptions* options, FFPublicIpResult* resul
}
if (*status != nullptr) {
return *status;
const char* error = *status;
ffNetworkingResetState(state);
*status = FF_UNINITIALIZED;
return error;
}
FF_STRBUF_AUTO_DESTROY response = ffStrbufCreateA(4096);
const char* error = ffNetworkingRecvHttpResponse(state, &response);
*state = (FFNetworkingState) {};
ffNetworkingResetState(state);
*status = FF_UNINITIALIZED;
if (error == nullptr) {
-5
View File
@@ -1,5 +0,0 @@
#include "swap.h"
const char* ffDetectSwap(FFSwapResult* swap) {
return "Not supported on this platform";
}
@@ -80,6 +80,10 @@ static void applyWTProfile(yyjson_val* profile, uint8_t priority, FFTerminalFont
// A fragment either defines a profile with "guid" or updates an existing one with "updates",
// the latter being more important, so it is looked up first.
// Note that "guid" and "updates" may be missing: yyjson_get_str() returns nullptr then.
// `fromUpdates` may be null: it is only useful to a caller that ranks the two apart, which a
// fragment does. settings.json does not -- an "updates" entry and a "guid" entry in profiles.list
// both sit at the same point of the inheritance chain, and the two passes above are what pick the
// right one between them.
static yyjson_val* findWTProfileInArray(yyjson_val* profiles, const FFstrbuf* wtProfileId, bool* fromUpdates) {
if (!yyjson_is_arr(profiles)) {
return nullptr;
@@ -91,7 +95,9 @@ static yyjson_val* findWTProfileInArray(yyjson_val* profiles, const FFstrbuf* wt
yyjson_arr_foreach (profiles, idx, max, profile) {
const char* id = yyjson_get_str(yyjson_obj_get(profile, pass == 0 ? "updates" : "guid"));
if (id && ffStrbufEqualS(wtProfileId, id)) {
*fromUpdates = pass == 0;
if (fromUpdates) {
*fromUpdates = pass == 0;
}
return profile;
}
}
@@ -122,8 +128,7 @@ static const char* detectFromWTSettings(FFstrbuf* content, const FFstrbuf* wtPro
}
if (wtProfileId->length > 0) {
bool fromUpdates = false;
yyjson_val* profile = findWTProfileInArray(yyjson_obj_get(profiles, "list"), wtProfileId, &fromUpdates);
yyjson_val* profile = findWTProfileInArray(yyjson_obj_get(profiles, "list"), wtProfileId, nullptr);
if (profile) {
applyWTProfile(profile, WT_FONT_PRIORITY_PROFILE, result);
}
@@ -167,25 +172,40 @@ static void detectWTProfileFromFragmentsIn(const FFstrbuf* fragmentDir, const FF
const uint32_t baseLength = path.length;
ffStrbufAppendC(&path, '*');
WIN32_FIND_DATAA entry;
FF_AUTO_CLOSE_DIR HANDLE hFind = FindFirstFileA(path.chars, &entry);
// The wide enumeration, because the directory above came from SHGetKnownFolderPath(): a profile
// whose name is not ASCII would otherwise hand FindFirstFileA a UTF-8 path that it reads as ANSI,
// and no fragment would be found at all.
wchar_t pathW[MAX_PATH];
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), nullptr, path.chars, (ULONG) path.length + 1))) {
return;
}
WIN32_FIND_DATAW entry;
FF_AUTO_CLOSE_DIR HANDLE hFind = FindFirstFileW(pathW, &entry);
if (hFind == INVALID_HANDLE_VALUE) {
return;
}
do {
if (!(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || entry.cFileName[0] == '.') {
if (!(entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || entry.cFileName[0] == L'.') {
continue;
}
ffStrbufSubstrBefore(&path, baseLength);
ffStrbufAppendS(&path, entry.cFileName);
ffStrbufAppendWS(&path, entry.cFileName);
ffStrbufAppendC(&path, '\\');
const uint32_t appLength = path.length;
ffStrbufAppendS(&path, "*.json");
WIN32_FIND_DATAA fileEntry;
FF_AUTO_CLOSE_DIR HANDLE hFile = FindFirstFileA(path.chars, &fileEntry);
// The path changed, so the pattern has to be converted again. A name that cannot be converted
// is skipped rather than failing the whole scan: the next iteration rebuilds `path` from
// `baseLength` anyway.
if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), nullptr, path.chars, (ULONG) path.length + 1))) {
continue;
}
WIN32_FIND_DATAW fileEntry;
FF_AUTO_CLOSE_DIR HANDLE hFile = FindFirstFileW(pathW, &fileEntry);
if (hFile != INVALID_HANDLE_VALUE) {
do {
if (fileEntry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
@@ -193,13 +213,13 @@ static void detectWTProfileFromFragmentsIn(const FFstrbuf* fragmentDir, const FF
}
ffStrbufSubstrBefore(&path, appLength);
ffStrbufAppendS(&path, fileEntry.cFileName);
ffStrbufAppendWS(&path, fileEntry.cFileName);
applyWTFragmentFile(path.chars, wtProfileId, result);
} while (FindNextFileA(hFile, &fileEntry));
} while (FindNextFileW(hFile, &fileEntry));
}
ffStrbufSubstrBefore(&path, baseLength);
} while (FindNextFileA(hFind, &entry));
} while (FindNextFileW(hFind, &entry));
}
static void detectFromWTFragments(const FFstrbuf* wtProfileId, FFTerminalFontWT* result) {
+232 -65
View File
@@ -29,7 +29,7 @@ static bool getFileVersion(const FFstrbuf* exePath, const wchar_t* stringName, F
#endif
static bool getExeVersionRaw(FFstrbuf* exe, FFstrbuf* version) {
return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) == nullptr;
return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr }) == nullptr && version->length > 0;
}
static bool getExeVersionGeneral(FFstrbuf* exe, FFstrbuf* version) {
@@ -39,7 +39,7 @@ static bool getExeVersionGeneral(FFstrbuf* exe, FFstrbuf* version) {
ffStrbufSubstrAfterFirstC(version, ' ');
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
return version->length > 0;
}
static bool extractBashVersion(const char* line, [[maybe_unused]] uint32_t len, void* userdata) {
@@ -118,22 +118,162 @@ static bool getShellVersionPwsh(FFstrbuf* exe, FFstrbuf* version) {
}
static bool getShellVersionKsh(FFstrbuf* exe, FFstrbuf* version) {
if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", nullptr }) == nullptr && ffStrbufSubstrAfterFirstS(version, " (AT&T Research) ")) {
// version sh (AT&T Research) 93u+ 2012-08-01
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
ffStrbufClear(version);
// ksh93 has used both stderr and stdout for --version, and the exit
// status is not consistent across releases. Parse any captured output
// before trying the next probe.
for (int attempt = 0; attempt < 4; ++attempt) {
ffStrbufClear(version);
if (attempt < 2 && (ffStrbufEndsWithS(exe, "mksh") || ffStrbufEndsWithS(exe, "pdksh") || ffStrbufEndsWithS(exe, "oksh")))
continue;
if (attempt == 0) {
ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", nullptr });
} else if (attempt == 1) {
ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", nullptr });
} else if (attempt == 2) {
ffProcessAppendStdOut(version, (char* const[]) {
exe->chars,
"-c",
"printf '%s\\n' \"${KSH_VERSION:-${SH_VERSION:-}}\"",
nullptr
});
} else {
ffProcessAppendStdOut(version, (char* const[]) {
exe->chars,
"-c",
"printf '%s\\n' \"${.sh.version}\"",
nullptr
});
}
const char* p = version->chars;
while (*p && isspace((unsigned char) *p))
++p;
if (!*p)
continue;
// @(#)PD KSH v5.2.14, @(#)MIRBSD KSH R59, @(#)LEGACY KSH R59
if (ffStrStartsWith(p, "@(#)")) {
const char* marker = strstr(p, " KSH ");
if (!marker)
continue;
const char* start = marker + strlen(" KSH ");
while (*start && isspace((unsigned char) *start))
++start;
const char* end = start;
while (*end && !isspace((unsigned char) *end))
++end;
uint32_t length = (uint32_t) (end - start);
if (length > 1 && start[0] == 'v' && ffCharIsDigit(start[1])) {
++start;
--length;
}
if (length == 0 || (!ffCharIsDigit(start[0]) && !(start[0] == 'R' && length > 1 && ffCharIsDigit(start[1]))))
continue;
ffStrbufSetNS(version, length, start);
return true;
}
const char* core = nullptr;
if (ffStrStartsWith(p, "version")) {
// ksh93 self-doc: version sh (AT&T [Labs] Research) ...
const char* selfDoc = strstr(p, " sh (");
const char* end = selfDoc ? strchr(selfDoc, ')') : nullptr;
if (!end)
continue;
core = end + 1;
} else if (ffStrStartsWith(p, "Version ")) {
// $KSH_VERSION / ${.sh.version}: Version <features> <version> <date>
core = p + strlen("Version ");
} else {
continue;
}
while (*core && isspace((unsigned char) *core))
++core;
const char* token = core;
while (*token) {
while (*token && isspace((unsigned char) *token))
++token;
if (!*token)
break;
const char* end = token;
while (*end && !isspace((unsigned char) *end))
++end;
uint32_t length = (uint32_t) (end - token);
bool valid = false;
if (length >= 2 && token[0] == '9' && token[1] == '3') {
valid = true;
for (uint32_t i = 2; i < length; ++i) {
char c = token[i];
if (!(ffCharIsDigit(c) || ffCharIsEnglishAlphabet(c) || c == '+' || c == '.' || c == '_' || c == '/' || c == '-')) {
valid = false;
break;
}
}
} else if (length >= 5 && token[0] == 'M' && token[1] == '-') {
for (uint32_t i = 2; i + 2 < length; ++i) {
if (token[i] == '/' && token[i + 1] == '9' && token[i + 2] == '3') {
valid = true;
break;
}
}
} else if (length > 0 && ffCharIsDigit(token[0]) && (memchr(token, '.', length) || memchr(token, '-', length))) {
valid = true;
} else if (length > 1 && token[0] == 'v' && ffCharIsDigit(token[1])) {
valid = true;
}
if (valid) {
uint32_t resultLength = length;
bool date = length >= 10 &&
ffCharIsDigit(token[0]) && ffCharIsDigit(token[1]) &&
ffCharIsDigit(token[2]) && ffCharIsDigit(token[3]) && token[4] == '-' &&
ffCharIsDigit(token[5]) && ffCharIsDigit(token[6]) && token[7] == '-' &&
ffCharIsDigit(token[8]) && ffCharIsDigit(token[9]);
if (date) {
const char* next = end;
while (*next && isspace((unsigned char) *next))
++next;
const char* nextEnd = next;
while (*nextEnd && !isspace((unsigned char) *nextEnd))
++nextEnd;
uint32_t nextLength = (uint32_t) (nextEnd - next);
if (nextLength > 0 && nextLength <= 2) {
bool suffix = true;
for (uint32_t i = 0; i < nextLength; ++i) {
if (!ffCharIsEnglishAlphabet(next[i]) && next[i] != '+') {
suffix = false;
break;
}
}
if (suffix)
resultLength = (uint32_t) (nextEnd - token);
}
}
const char* start = token;
if (resultLength > 1 && start[0] == 'v' && ffCharIsDigit(start[1])) {
++start;
--resultLength;
}
ffStrbufSetNS(version, resultLength, start);
return version->length > 0;
}
token = end;
}
}
ffStrbufClear(version);
if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $KSH_VERSION", nullptr }) == nullptr && ffStrbufSubstrAfterFirstS(version, " KSH ")) {
// OKSH: @(#)PD KSH v5.2.14 99/07/13.2
// MKSH: @(#)MIRBSD KSH R59 2025/04/26 +Debian
// $OKSH_VERSION doesn't exist on OpenBSD
ffStrbufSubstrBeforeFirstC(version, ' ');
ffStrbufTrimLeft(version, 'v');
return true;
}
return false;
}
@@ -283,7 +423,7 @@ bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version)
if (ffStrEqualsIgnCase(exeName, "nu")) {
return getShellVersionNushell(exe, version);
}
if (ffStrEqualsIgnCase(exeName, "ksh") || ffStrEqualsIgnCase(exeName, "mksh")) {
if (ffStrEqualsIgnCase(exeName, "ksh") || ffStrEqualsIgnCase(exeName, "ksh93") || ffStrEqualsIgnCase(exeName, "mksh") || ffStrEqualsIgnCase(exeName, "pdksh")) {
return getShellVersionKsh(exe, version);
}
if (ffStrEqualsIgnCase(exeName, "oksh")) {
@@ -347,9 +487,10 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// # GNOME Terminal 3.46.7 using VTE 0.70.2 +BIDI +GNUTLS +ICU +SYSTEMD
ffStrbufSubstrAfterFirstS(version, "Terminal ");
if (!ffStrbufSubstrAfterFirstS(version, "Terminal "))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionXfce4Terminal(FFstrbuf* exe, FFstrbuf* version) {
@@ -369,17 +510,19 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// # KGX 45.0 using VTE 0.74.0 +BIDI +GNUTLS +ICU +SYSTEMD
ffStrbufSubstrAfterFirstS(version, "KGX ");
if (!ffStrbufSubstrAfterFirstS(version, "KGX "))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionKonsole(FFstrbuf* exe, FFstrbuf* version) {
const char* konsoleVersion = getenv("KONSOLE_VERSION");
if (konsoleVersion) {
// 221201
long major = strtol(konsoleVersion, nullptr, 10);
if (major >= 0) {
char* end;
long major = strtol(konsoleVersion, &end, 10);
if (end != konsoleVersion && *end == '\0' && major >= 0) {
long patch = major % 100;
major /= 100;
long minor = major % 100;
@@ -408,9 +551,10 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// foot version: 1.13.1 -pgo +ime -graphemes -assertions
ffStrbufSubstrAfterFirstS(version, "version: ");
if (!ffStrbufSubstrAfterFirstS(version, "version: "))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionMateTerminal(FFstrbuf* exe, FFstrbuf* version) {
@@ -424,7 +568,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// MATE Terminal 1.26.1
ffStrbufSubstrAfterLastC(version, ' ');
if (!ffStrbufSubstrAfterLastC(version, ' '))
return false;
return version->length > 0;
}
@@ -435,7 +580,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
// Version: 295\n...
ffStrbufSubstrBeforeFirstC(version, '\n');
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -449,7 +595,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
// xterm(273)
ffStrbufTrimRight(version, ')');
ffStrbufSubstrAfterFirstC(version, '(');
if (!ffStrbufSubstrAfterFirstC(version, '('))
return false;
return version->length > 0;
}
@@ -459,7 +606,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// BlackBox version 0.14.0 (flatpak)
ffStrbufSubstrAfterFirstS(version, "version ");
if (!ffStrbufSubstrAfterFirstS(version, "version "))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return version->length > 0;
}
@@ -473,7 +621,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
// urxvt: "invalid": unknown or malformed option.
// rxvt-unicode (urxvt) v9.31 - released: 2023-01-02
ffStrbufSubstrAfterFirstS(version, "(urxvt) v");
if (!ffStrbufSubstrAfterFirstS(version, "(urxvt) v"))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return version->length > 0;
@@ -485,7 +634,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// st 0.9
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -495,7 +645,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
return false;
}
// lxterminal 0.3.2
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -506,7 +657,8 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// weston 8.0.0
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -537,21 +689,23 @@ static bool extractGeneralVersion(const char* str, [[maybe_unused]] uint32_t len
}
// kmscon version v10.0.0
ffStrbufSubstrAfterLastC(version, ' ');
if (!ffStrbufSubstrAfterLastC(version, ' '))
return false;
return version->length > 0;
}
static bool getTerminalVersionContour(FFstrbuf* exe, FFstrbuf* version) {
const char* env = getenv("TERMINAL_VERSION_STRING");
if (env) {
if (env && *env) {
ffStrbufAppendS(version, env);
return true;
return version->length > 0;
}
if (!getExeVersionRaw(exe, version)) {
return false;
}
// Contour Terminal Emulator 0.3.12.262
ffStrbufSubstrAfterLastC(version, ' ');
if (!ffStrbufSubstrAfterLastC(version, ' '))
return false;
return version->length > 0;
}
@@ -560,7 +714,8 @@ static bool getTerminalVersionScreen(FFstrbuf* exe, FFstrbuf* version) {
return false;
}
// Screen version 4.09.01 (GNU) 20-Aug-23
ffStrbufSubstrAfter(version, (uint32_t) strlen("Screen version ") - 1);
if (!ffStrbufSubstrAfterFirstS(version, "Screen version "))
return false;
ffStrbufSubstrBeforeFirstC(version, ' ');
return version->length > 0;
}
@@ -571,7 +726,8 @@ static bool getTerminalVersionTmux(FFstrbuf* exe, FFstrbuf* version) {
}
// tmux 3.4
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -581,7 +737,8 @@ static bool getTerminalVersionZellij(FFstrbuf* exe, FFstrbuf* version) {
}
// zellij 0.39.2
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
@@ -599,9 +756,11 @@ static bool getTerminalVersionZed(FFstrbuf* exe, FFstrbuf* version) {
}
// Zed 0.142.6 – /Applications/Zed.app
ffStrbufSubstrAfterFirstC(version, ' ');
if (!ffStrbufStartsWithS(version, "Zed "))
return false;
ffStrbufSubstrAfter(version, (uint32_t) strlen("Zed ") - 1);
ffStrbufSubstrBeforeFirstC(version, ' ');
return true;
return version->length > 0;
}
static bool extractSshdVersion(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
@@ -631,18 +790,22 @@ static bool getTerminalVersionSshd(FFstrbuf* exe, FFstrbuf* version) {
return true;
}
if (ffProcessAppendStdOut(version, (char* const[]) { exePath.chars, "-V", nullptr }) != nullptr) {
if (ffProcessAppendStdErr(version, (char* const[]) { exePath.chars, "-V", nullptr }) != nullptr) {
return false;
}
if (ffStrbufStartsWithS(version, "unknown ")) { // `unknown option -- V` (ancient OpenSSH version)
ffStrbufSubstrAfterFirstC(version, '\n');
if (!ffStrbufSubstrAfterFirstC(version, '\n'))
return false;
}
if (!ffStrbufStartsWithS(version, "OpenSSH_"))
return false;
ffStrbufSubstrAfter(version, (uint32_t) strlen("OpenSSH_") - 1);
// OpenSSH_10.0p2 Ubuntu-5ubuntu5, OpenSSL 3.5.3 16 Sep 2025
ffStrbufSubstrAfterFirstC(version, '_');
ffStrbufSubstrBeforeFirstC(version, ',');
return true;
return version->length > 0;
}
#ifndef _WIN32
@@ -670,7 +833,7 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
}
}
#elif __APPLE__
if (ffGetAppNameAndVersion(exe->chars, nullptr, version)) {
if (ffGetAppNameAndVersion(exe->chars, nullptr, version) && version->length > 0) {
return true;
}
#endif
@@ -689,7 +852,8 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
ffStrbufAppendC(version, (char) value);
}
}
return true;
if (version->length > 0)
return true;
}
// kitty 0.21.2 created by Kovid Goyal
@@ -702,8 +866,9 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
}
ffStrbufSubstrBeforeFirstC(version, '\n');
ffStrbufSubstrAfterFirstC(version, ' ');
return true;
if (!ffStrbufSubstrAfterFirstC(version, ' '))
return false;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionTilix(FFstrbuf* exe, FFstrbuf* version) {
@@ -728,7 +893,7 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
ffStrbufSubstrBefore(version, end);
ffStrbufSubstrAfter(version, index);
return true;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionSakura(FFstrbuf* exe, FFstrbuf* version) {
@@ -736,8 +901,9 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
return false;
}
ffStrbufSubstrAfterLastC(version, ' ');
return true;
if (!ffStrbufSubstrAfterLastC(version, ' '))
return false;
return version->length > 0;
}
[[maybe_unused]] static bool getTerminalVersionTermite(FFstrbuf* exe, FFstrbuf* version) {
@@ -746,8 +912,9 @@ static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) {
}
ffStrbufSubstrBeforeFirstC(version, '\n');
ffStrbufSubstrAfterLastC(version, 'v');
return true;
if (!ffStrbufSubstrAfterLastC(version, 'v'))
return false;
return version->length > 0;
}
#endif
@@ -758,12 +925,12 @@ static bool getTerminalVersionWindowsTerminal(FFstrbuf* exe, FFstrbuf* version)
ffStrbufInitNS(&buildInfoPath, ffStrbufLastIndexC(exe, '\\') + 1, exe->chars);
ffStrbufAppendS(&buildInfoPath, "BuildInfo.xml");
if (ffParsePropFile(buildInfoPath.chars, "StoreVersion=\"", version)) {
if (ffParsePropFile(buildInfoPath.chars, "StoreVersion=\"", version) && version->length > 0) {
ffStrbufTrimRight(version, '"');
return true;
return version->length > 0;
}
return getFileVersion(exe, nullptr, version);
return getFileVersion(exe, nullptr, version) && version->length > 0;
}
static bool getTerminalVersionConEmu(FFstrbuf* exe, FFstrbuf* version) {
@@ -773,7 +940,7 @@ static bool getTerminalVersionConEmu(FFstrbuf* exe, FFstrbuf* version) {
return true;
}
return getFileVersion(exe, nullptr, version);
return getFileVersion(exe, nullptr, version) && version->length > 0;
}
#endif
@@ -897,7 +1064,7 @@ bool fftsGetTerminalVersion(FFstrbuf* processName, [[maybe_unused]] FFstrbuf* ex
}
if (ffStrbufIgnCaseEqualS(processName, "warp.exe")) {
return getFileVersion(exe, L"ProductVersion", version);
return getFileVersion(exe, L"ProductVersion", version) && version->length > 0;
}
#endif
@@ -941,7 +1108,7 @@ bool fftsGetTerminalVersion(FFstrbuf* processName, [[maybe_unused]] FFstrbuf* ex
#endif
const char* termProgramVersion = getenv("TERM_PROGRAM_VERSION");
if (termProgramVersion) {
if (termProgramVersion && *termProgramVersion) {
const char* termProgram = getenv("TERM_PROGRAM");
if (termProgram) {
if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows
@@ -953,20 +1120,20 @@ bool fftsGetTerminalVersion(FFstrbuf* processName, [[maybe_unused]] FFstrbuf* ex
#endif
false) {
ffStrbufSetS(version, termProgramVersion);
return true;
return version->length > 0;
}
}
}
termProgramVersion = getenv("LC_TERMINAL_VERSION");
if (termProgramVersion) {
if (termProgramVersion && *termProgramVersion) {
const char* termProgram = getenv("LC_TERMINAL");
if (termProgram) {
if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows
(ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) ||
(ffStrStartsWith(termProgram, "iTerm") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-"))) {
ffStrbufSetS(version, termProgramVersion);
return true;
return version->length > 0;
}
}
}
@@ -981,11 +1148,11 @@ bool fftsGetTerminalVersion(FFstrbuf* processName, [[maybe_unused]] FFstrbuf* ex
#ifdef _WIN32
return getFileVersion(exe, nullptr, version);
return getFileVersion(exe, nullptr, version) && version->length > 0;
#elif __APPLE__
return ffGetAppNameAndVersion(exe->chars, nullptr, version);
return ffGetAppNameAndVersion(exe->chars, nullptr, version) && version->length > 0;
#else
@@ -1,4 +1,5 @@
#include "terminalshell.h"
#include "common/FFcache.h"
#include "common/io.h"
#include "common/parsing.h"
#include "common/processing.h"
@@ -90,7 +91,9 @@ static pid_t getTerminalInfo(FFTerminalResult* result, pid_t pid) {
ffStrbufEqualS(&result->processName, "bash") ||
ffStrbufEqualS(&result->processName, "zsh") ||
ffStrbufEqualS(&result->processName, "ksh") ||
ffStrbufEqualS(&result->processName, "ksh93") ||
ffStrbufEqualS(&result->processName, "mksh") ||
ffStrbufEqualS(&result->processName, "pdksh") ||
ffStrbufEqualS(&result->processName, "oksh") ||
ffStrbufEqualS(&result->processName, "csh") ||
ffStrbufEqualS(&result->processName, "tcsh") ||
@@ -376,22 +379,43 @@ static void setTerminalInfoDetails(FFTerminalResult* result) {
#define FF_EXE_PATH_LEN 260
#endif
const FFShellResult* ffDetectShell() {
static FFShellResult result;
static bool init = false;
if (init) {
return &result;
}
init = true;
static FFShellResult shellResult;
ffStrbufInit(&result.processName);
ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN);
result.exeName = result.exe.chars;
ffStrbufInit(&result.exePath);
ffStrbufInit(&result.version);
result.pid = 0;
result.ppid = 0;
result.tty = -1;
static void destroyShellResult(void* storage) {
FFShellResult* result = storage;
ffStrbufDestroy(&result->processName);
ffStrbufDestroy(&result->exe);
result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again
ffStrbufDestroy(&result->exePath);
ffStrbufDestroy(&result->prettyName);
ffStrbufDestroy(&result->version);
result->pid = 0;
result->ppid = 0;
result->tty = -1;
}
static FFcacheEntry ffCacheEntryShell = {
.name = "shell",
.storage = &shellResult,
.destroy = destroyShellResult,
};
const FFShellResult* ffDetectShell() {
FFShellResult* result = &shellResult;
if (!ffCacheBeginInit(&ffCacheEntryShell)) {
return result;
}
ffStrbufInit(&result->processName);
ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN);
result->exeName = result->exe.chars;
ffStrbufInit(&result->exePath);
ffStrbufInit(&result->version);
result->pid = 0;
result->ppid = 0;
result->tty = -1;
pid_t ppid = getppid();
@@ -401,49 +425,74 @@ const FFShellResult* ffDetectShell() {
ffProcessGetBasicInfoLinux(ppid, &_, &ppid, nullptr);
}
ppid = getShellInfo(&result, ppid);
getUserShellFromEnv(&result);
ppid = getShellInfo(result, ppid);
getUserShellFromEnv(result);
if (result.processName.length > 0) {
setShellInfoDetails(&result);
if (result->processName.length > 0) {
setShellInfoDetails(result);
if (instance.config.general.detectVersion) {
fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, result.exeName, &result.version);
if (!fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, result->exeName, &result->version)) {
ffStrbufClear(&result->version);
}
}
}
return &result;
return result;
}
const FFTerminalResult* ffDetectTerminal() {
static FFTerminalResult result;
static bool init = false;
if (init) {
return &result;
}
init = true;
static FFTerminalResult terminalResult;
ffStrbufInit(&result.processName);
ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN);
result.exeName = result.exe.chars;
ffStrbufInit(&result.exePath);
ffStrbufInit(&result.version);
ffStrbufInitS(&result.tty, ttyname(STDOUT_FILENO));
result.pid = 0;
result.ppid = 0;
static void destroyTerminalResult(void* storage) {
FFTerminalResult* result = storage;
ffStrbufDestroy(&result->processName);
ffStrbufDestroy(&result->exe);
result->exeName = result->exe.chars; // `exeName` points into `exe`, which is empty again
ffStrbufDestroy(&result->exePath);
ffStrbufDestroy(&result->prettyName);
ffStrbufDestroy(&result->version);
ffStrbufDestroy(&result->tty);
result->pid = 0;
result->ppid = 0;
}
static FFcacheEntry ffCacheEntryTerminal = {
.name = "terminal",
.storage = &terminalResult,
.destroy = destroyTerminalResult,
};
const FFTerminalResult* ffDetectTerminal() {
FFTerminalResult* result = &terminalResult;
if (!ffCacheBeginInit(&ffCacheEntryTerminal)) {
return result;
}
ffStrbufInit(&result->processName);
ffStrbufInitA(&result->exe, FF_EXE_PATH_LEN);
result->exeName = result->exe.chars;
ffStrbufInit(&result->exePath);
ffStrbufInit(&result->version);
ffStrbufInitS(&result->tty, ttyname(STDOUT_FILENO));
result->pid = 0;
result->ppid = 0;
pid_t ppid = (pid_t) ffDetectShell()->ppid;
if (ppid) {
ppid = getTerminalInfo(&result, ppid);
ppid = getTerminalInfo(result, ppid);
}
getTerminalFromEnv(&result);
getTerminalFromEnv(result);
if (result.processName.length > 0) {
setTerminalInfoDetails(&result);
if (result->processName.length > 0) {
setTerminalInfoDetails(result);
if (instance.config.general.detectVersion) {
fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version);
if (!fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version)) {
ffStrbufClear(&result->version);
}
}
}
return &result;
return result;
}
@@ -1,4 +1,5 @@
#include "terminalshell.h"
#include "common/FFcache.h"
#include "common/io.h"
#include "common/processing.h"
#include "common/thread.h"
@@ -293,27 +294,48 @@ static void setTerminalInfoDetails(FFTerminalResult* result) {
}
}
const FFShellResult* ffDetectShell(void) {
static FFShellResult result;
static bool init = false;
if (init) {
return &result;
}
init = true;
static FFShellResult shellResult;
ffStrbufInit(&result.processName);
ffStrbufInitA(&result.exe, MAX_PATH);
result.exeName = "";
ffStrbufInit(&result.exePath);
ffStrbufInit(&result.prettyName);
ffStrbufInit(&result.version);
result.pid = 0;
result.ppid = 0;
result.tty = -1;
static void destroyShellResult(void* storage) {
FFShellResult* result = storage;
ffStrbufDestroy(&result->processName);
ffStrbufDestroy(&result->exe);
result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it
ffStrbufDestroy(&result->exePath);
ffStrbufDestroy(&result->prettyName);
ffStrbufDestroy(&result->version);
result->pid = 0;
result->ppid = 0;
result->tty = -1;
}
static FFcacheEntry ffCacheEntryShell = {
.name = "shell",
.storage = &shellResult,
.destroy = destroyShellResult,
};
const FFShellResult* ffDetectShell(void) {
FFShellResult* result = &shellResult;
if (!ffCacheBeginInit(&ffCacheEntryShell)) {
return result;
}
ffStrbufInit(&result->processName);
ffStrbufInitA(&result->exe, MAX_PATH);
result->exeName = "";
ffStrbufInit(&result->exePath);
ffStrbufInit(&result->prettyName);
ffStrbufInit(&result->version);
result->pid = 0;
result->ppid = 0;
result->tty = -1;
uint32_t ppid;
if (!ffProcessGetInfoWindows(0, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr)) {
return &result;
return result;
}
const char* ignoreParent = getenv("FFTS_IGNORE_PARENT");
@@ -321,60 +343,85 @@ const FFShellResult* ffDetectShell(void) {
ffProcessGetInfoWindows(ppid, &ppid, nullptr, nullptr, nullptr, nullptr, nullptr);
}
ppid = getShellInfo(&result, ppid);
ppid = getShellInfo(result, ppid);
if (result.processName.length > 0) {
setShellInfoDetails(&result);
if (result->processName.length > 0) {
setShellInfoDetails(result);
char tmp[MAX_PATH];
strcpy(tmp, result.exeName);
char* ext = strrchr(tmp, '.');
char* end = ffStrCopy(tmp, result->exeName, sizeof(tmp));
char* ext = (char*) memrchr(tmp, '.', (size_t) (end - tmp));
if (ext) {
*ext = '\0';
}
if (instance.config.general.detectVersion) {
fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, tmp, &result.version);
if (!fftsGetShellVersion(result->exePath.length > 0 ? &result->exePath : &result->exe, tmp, &result->version)) {
ffStrbufClear(&result->version);
}
}
}
return &result;
return result;
}
const FFTerminalResult* ffDetectTerminal(void) {
static FFTerminalResult result;
static bool init = false;
if (init) {
return &result;
}
init = true;
static FFTerminalResult terminalResult;
ffStrbufInit(&result.processName);
ffStrbufInitA(&result.exe, MAX_PATH);
result.exeName = "";
ffStrbufInit(&result.exePath);
ffStrbufInit(&result.prettyName);
ffStrbufInit(&result.version);
ffStrbufInit(&result.tty);
result.pid = 0;
result.ppid = 0;
static void destroyTerminalResult(void* storage) {
FFTerminalResult* result = storage;
ffStrbufDestroy(&result->processName);
ffStrbufDestroy(&result->exe);
result->exeName = ""; // The `exe` buffer is empty again, so nothing points into it
ffStrbufDestroy(&result->exePath);
ffStrbufDestroy(&result->prettyName);
ffStrbufDestroy(&result->version);
ffStrbufDestroy(&result->tty);
result->pid = 0;
result->ppid = 0;
}
static FFcacheEntry ffCacheEntryTerminal = {
.name = "terminal",
.storage = &terminalResult,
.destroy = destroyTerminalResult,
};
const FFTerminalResult* ffDetectTerminal(void) {
FFTerminalResult* result = &terminalResult;
if (!ffCacheBeginInit(&ffCacheEntryTerminal)) {
return result;
}
ffStrbufInit(&result->processName);
ffStrbufInitA(&result->exe, MAX_PATH);
result->exeName = "";
ffStrbufInit(&result->exePath);
ffStrbufInit(&result->prettyName);
ffStrbufInit(&result->version);
ffStrbufInit(&result->tty);
result->pid = 0;
result->ppid = 0;
uint32_t ppid = ffDetectShell()->ppid;
if (ppid) {
getTerminalInfo(&result, ppid);
getTerminalInfo(result, ppid);
}
if (result.processName.length == 0) {
getTerminalFromEnv(&result);
if (result->processName.length == 0) {
getTerminalFromEnv(result);
}
if (result.processName.length == 0) {
detectDefaultTerminal(&result);
if (result->processName.length == 0) {
detectDefaultTerminal(result);
}
if (result.processName.length > 0) {
setTerminalInfoDetails(&result);
if (result->processName.length > 0) {
setTerminalInfoDetails(result);
if (instance.config.general.detectVersion) {
fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version);
if (!fftsGetTerminalVersion(&result->processName, result->exePath.length > 0 ? &result->exePath : &result->exe, &result->version)) {
ffStrbufClear(&result->version);
}
}
}
return &result;
return result;
}
+2 -1
View File
@@ -10,4 +10,5 @@ typedef struct FFTerminalSizeResult {
uint16_t height;
} FFTerminalSizeResult;
bool ffDetectTerminalSize(FFTerminalSizeResult* result);
// `fastOnly` avoids terminal query/response sequences and is intended for startup layout decisions.
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly);
+12 -11
View File
@@ -9,7 +9,9 @@
#include <sys/termios.h>
#endif
bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly) {
*result = (FFTerminalSizeResult){};
struct winsize winsize = {};
static int ttyfd = STDOUT_FILENO;
if (!isatty(ttyfd)) {
@@ -18,21 +20,20 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
ioctl(ttyfd, TIOCGWINSZ, &winsize);
if (winsize.ws_row == 0 || winsize.ws_col == 0) {
ffGetTerminalResponse("\e[18t", 2, "\e[8;%hu;%hut", &winsize.ws_row, &winsize.ws_col);
}
if (!fastOnly) {
if (winsize.ws_row == 0 || winsize.ws_col == 0) {
ffGetTerminalResponse("\e[18t", 2, "\e[8;%hu;%hut", &winsize.ws_row, &winsize.ws_col);
}
if (winsize.ws_ypixel == 0 || winsize.ws_xpixel == 0) {
ffGetTerminalResponse("\e[14t", 2, "\e[4;%hu;%hut", &winsize.ws_ypixel, &winsize.ws_xpixel);
}
if (winsize.ws_row == 0 && winsize.ws_col == 0) {
return false;
if (winsize.ws_ypixel == 0 || winsize.ws_xpixel == 0) {
ffGetTerminalResponse("\e[14t", 2, "\e[4;%hu;%hut", &winsize.ws_ypixel, &winsize.ws_xpixel);
}
}
result->rows = winsize.ws_row;
result->columns = winsize.ws_col;
result->width = winsize.ws_xpixel;
result->height = winsize.ws_ypixel;
return true;
return result->rows > 0 && result->columns > 0;
}
@@ -3,7 +3,9 @@
#include <windows.h>
bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
bool ffDetectTerminalSize(FFTerminalSizeResult* result, bool fastOnly) {
*result = (FFTerminalSizeResult){};
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE;
{
@@ -18,16 +20,20 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
if (GetConsoleScreenBufferInfo(hOutput, &csbi)) {
result->columns = (uint16_t) (csbi.srWindow.Right - csbi.srWindow.Left + 1);
result->rows = (uint16_t) (csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
} else {
} else if (!fastOnly) {
// Windows Terminal doesn't report `\e` for some reason
ffGetTerminalResponse("\e[18t", 2, "%*[^;];%hu;%hut", &result->rows, &result->columns);
}
}
if (result->columns == 0 && result->rows == 0) {
if (result->columns == 0 || result->rows == 0) {
return false;
}
if (fastOnly) {
return true;
}
{
CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) };
if (GetCurrentConsoleFontEx(hOutput, FALSE, &cfi)) // Only works for ConHost
@@ -41,5 +47,5 @@ bool ffDetectTerminalSize(FFTerminalSizeResult* result) {
}
}
return result->columns > 0 && result->rows > 0;
return true;
}
+17 -8
View File
@@ -4,20 +4,22 @@
static FFlist first;
static double startTick;
static FFTopTypes preparedTypes;
void ffPrepareTopProcesses(FFTopTypes showTypes) {
if ((showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0) {
return; // Memory usage is instantaneous; no baseline snapshot is needed
}
if (startTick != 0) {
return; // Already prepared
}
// `showTypes` cannot change between this call and `ffDetectTopProcesses`: `ffPrepareCommandOption`
// and `parseStructureCommand` both build the options through `initStructureModuleOptions`, which
// merges the module object from the JSON config. So the baseline always matches what the second
// snapshot collects and needs no re-validation.
if ((showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0) {
return; // Memory usage is instantaneous; no baseline snapshot is needed
}
// Within one module `showTypes` cannot change between this call and `ffDetectTopProcesses`:
// `ffPrepareCommandOption` and `parseStructureCommand` both build the options through
// `initStructureModuleOptions`, which merges the module object from the JSON config, so the
// baseline always matches what the second snapshot collects.
preparedTypes = showTypes;
ffListInit(&first);
startTick = ffTimeGetTick();
ffTopGetProcessSnapshot(&first, showTypes);
@@ -67,6 +69,13 @@ const char* ffDetectTopProcesses(FFTopOptions* options, FFlist* result) {
return nullptr;
}
// A baseline collected for a different set of counters is answered here rather than in
// ffPrepareTopProcesses, which has no way to report an error to the caller. The module that
// triggered the mismatch is the one that sees it; see ffPrepareTopProcesses.
if (options->showTypes != preparedTypes && (options->showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) != 0) {
return "`top` modules with different `showTypes` cannot share a run";
}
// Memory usage and thread count are instantaneous; when neither CPU time nor disk IO
// counters are requested, a single snapshot suffices and no sampling wait is needed.
const bool sampleOnce = (options->showTypes & (FF_TOP_TYPE_CPU | FF_TOP_TYPE_DISK)) == 0;

Some files were not shown because too many files have changed in this diff Show More