-
-
Notifications
You must be signed in to change notification settings - Fork 907
Modules Gamepad
List connected gamepads
| Module type | gamepad |
| Default order | 63 (only used by --gen-config) |
| Module source | src/modules/gamepad/gamepad.c |
| Detection source | src/detection/gamepad/ |
Prints one line per connected controller. The value is the device name, optionally preceded by a battery bar and optionally followed by a battery percentage in parentheses:
Gamepad: Sony DualSense (100%)
The bar and the number are both governed by percent.type; with the default (the global
display.percent.type, i.e. num plus num-color) the line is exactly the name, a space and
(N%). Without hide-others the name is always printed, so turning the number off does not leave an
empty line. The battery number is added only for a device whose battery is in (0, 100] — 0 is
the module's "unknown" sentinel, and a device that reports nothing shows its name alone:
Gamepad: 8BitDo Pro 2
Two or more devices get a numbered key (Gamepad 1, Gamepad 2); a single device is printed
without a number. With no controller attached there is no line at all unless
display.showErrors is on, in which case the message is No devices detected.
| Platform | Implementation | Notes |
|---|---|---|
| Linux | gamepad_linux.c |
/sys/class/input/js* |
| Android | gamepad_nosupport.c |
Not supported |
| FreeBSD | gamepad_bsd.c |
/dev/uhid0 … /dev/uhid63
|
| NetBSD | gamepad_nosupport.c |
Not supported |
| OpenBSD | gamepad_nosupport.c |
Not supported |
| Solaris / illumos | gamepad_nosupport.c |
Not supported |
| Haiku | gamepad_haiku.cpp |
BJoystick |
| GNU/Hurd | gamepad_nosupport.c |
Not supported |
| macOS | gamepad_apple.m |
IOHIDManager + GameController |
| Windows | gamepad_windows.c |
Raw Input plus hidsdi.h
|
The five stub platforms keep the module registered and report the standard
Not supported on this platform message, which is silent unless display.showErrors is on.
| Key | Type | Default | Description |
|---|---|---|---|
ignores |
array of strings | – | Case-insensitive device-name prefixes to leave out of the text output |
percent.green |
integer 0–100 | 50 |
Green threshold |
percent.yellow |
integer 0–100 | 20 |
Yellow threshold |
percent.type |
number or array of flags |
0 (use display.percent.type) |
num, bar, hide-others, num-color, bar-monochrome
|
key |
string | module name | Module key. A single space hides the key and the separator. |
keyColor |
color | – | Overrides display.color.keys
|
keyIcon |
string | built-in glyph | Printed when display.key.type includes the icon bit. Any glyph works; "" prints none. |
keyWidth |
integer | – | Overrides display.key.width
|
outputColor |
color | – | Overrides display.color.output
|
format |
string | – | Custom output format (see below) |
condition |
object | – | Show the module only if the conditions match |
Two notes on the thresholds. The module's default is green: 50, yellow: 20 — green greater than
yellow, which inverts the direction the schema text describes: with green > yellow a high
battery is green, a battery in [20, 50) is yellow and anything below 20 is red. The schema's
"values below this threshold are shown in green" only holds for the other ordering
(green <= yellow), which is what the battery-style modules use. And because the default
percent.type is 0, the module follows the global display.percent.type unless you set its own —
so --percent-type and display.percent.type change this module too.
ignores is applied with ffStrbufStartsWithIgnCase(), so an entry hides every device whose name
starts with it: "Sony" hides all Sony controllers, and an empty string would hide everything.
Run fastfetch -h gamepad-format for the authoritative list.
| Variable | Description |
|---|---|
{name} |
Device name |
{serial} |
Serial number, empty when the backend has none |
{battery-percentage} |
The number, e.g. 100%, empty unless percent.type includes num
|
{battery-percentage-bar} |
The bar, empty unless percent.type includes bar
|
None of the four is available in the key format: ffPrintLogoAndKey() parses key with {index},
{icon} and {module-name} only. {index} is available in the output format and follows the
printed line number — 0 for a single controller, 1 for the first of several.
Two details of the shared format engine are easy to get wrong:
-
An unknown placeholder is copied verbatim, conditional syntax included. A key of
"[{?index}I{index}{?}][{?name}N{name}{?}]"prints[][{?name}N{name}{?}]. -
Conditional blocks treat
0and""as unset.formatArgSet()counts an integer as set only when it is> 0and a string only when its length is non-zero.{?index}…{?}therefore stays silent for a single controller, and{?serial}…{?}stays silent for a device without a serial.
The array is always present — "result": [] when nothing is connected, with no error field even
though the print path would have said No devices detected. ignored is only a flag; the JSON path
does not remove ignored devices, unlike the text path.
battery is the raw value of FFGamepadDevice.battery, so it carries the same 0-means-unknown
convention as the text path: a DualSense in that sample reports no battery at all rather than 0%.
JSON is the only place where the ignored flag and the battery of an ignored device are visible —
the text path drops ignored devices before printing, so their battery never reaches the terminal.
// Hide everything whose name starts with "Xbox" or "8BitDo"
{ "type": "gamepad", "ignores": ["Xbox", "8BitDo"] }// Battery bar instead of the name, number included
{ "type": "gamepad", "percent": { "type": ["bar", "num", "hide-others"] }, "key": "PAD {index}" }// Name only: the number is suppressed by dropping the "num" flag
{ "type": "gamepad", "percent": { "type": ["bar-monochrome"] } }Each of these is accepted by the parser; the line they produce depends on a controller being attached, and on the model, because only some models report a battery (see the Pitfalls below).
-
The JSON list and the text list can have different lengths. The text path filters
ignoresout, the JSON path keeps every device and marks it withignored: true. A config that hides a controller therefore prints fewer lines than the JSON array has objects, and index-based comparisons between the two drift apart. -
{battery-percentage-bar}is empty by default. The bar string is only built whenpercent.typeincludes thebarflag, and the default type does not. A format like{battery-percentage-bar} {name}prints a leading space and no bar unless you add the flag, whereas the default line is unaffected because it appends the bar inline rather than through the variable. -
A battery of
0is hidden, not printed as0%.0means "unknown" in this module, so a controller that is genuinely empty and one the backend could not read look identical: both print the bare name. Negating the value is not possible from a format string. -
On Windows only three controller models ever report a battery. The battery read is guarded by
a vendor/product check — Sony
0x054C/0x05C4and0x09CC(DualShock 4), and Nintendo0x057E/0x2009(Switch Pro). Every other controller, Xbox pads included, keepsbattery = 0and never shows a percentage. -
On Windows the battery only appears after another program has opened the controller.
Immediately after pairing, fastfetch usually reports no battery even for a DualShock 4 or a Switch
Pro; starting a program that actually uses the pad (Steam, DS4Windows, a browser gamepad tester)
and re-running fastfetch then shows it. The source comment at that read is
// Controller must be connected by other programs, and the read is a singleReadFile()on an overlapped handle that is waited for at most one second (FF_IO_TERM_RESP_WAIT_MS, a constant that otherwise belongs to terminal-response handling). The module does no handshake of its own, so a controller that stays silent until a client unlocks it yields nothing. (Tracked as an open item in the project'sbug.md.) -
On Windows a fully drained Switch Pro reports 1%. The Switch path maps a battery level of
0to1on purpose, because0would otherwise mean "unknown" and the number would disappear. -
On macOS the battery comes from GameController, not from the HID report. The two views of a
controller cannot be joined — see the macOS implementation section — so the framework's own devices
are appended as whole entries and the HID pass skips them. A controller the framework does not
claim therefore reports no battery, even though its HID report may well carry one: only Apple's
allow list, the 37 vendor/product pairs in
AppleGameControllerPersonality.kext, and HID devices that "look like an MFi gamepad" are claimed. A controller that is claimed but whose driver implements no battery service still reports nothing. -
On macOS the battery may be missing entirely for a command line run.
[GCController controllers]is filled by the private_GCControllerManagerwhen it is told the application became active, which a CLI process never tells it. Measured on this machine: the array is empty at process start, stays empty through 20 ms of unrelated work without pumping the run loop, and only fills once the run loop is pumped (~10.7 ms). The module pumps for it explicitly, in a loop bounded by 50 ms, but only once+[GCController supportsHIDDevice:]has confirmed the framework claims a connected device — so the wait is never entered on a machine with no pad, or with one Apple does not claim. If the array is still empty after that, the HID pass is not skipped and every pad is listed by its IOHID name with no battery; the fallback is deliberate, but it means no percentage appears. -
On macOS the framework's start-up cost is paid only when it can pay off. Measured with
--staton-s Gamepad, no controller attached: 0.3 ms with nothing hoisted, 8.3 ms for a bareGCController.controllersread, 14.3 ms once a singleCFRunLoopRunInMode()turn follows it. The read is what connects togamecontrollerdand the turn is what carries the request, so both are needed before the list is usable. The gate costs 0.3 ms warm and does not start the framework, which is what makes it safe to ask first. -
On macOS the battery is rounded, and
batteryLevelcannot distinguish "unknown" from "drained". The value islroundf(batteryLevel * 100)clamped to0..100. A wired pad has noGCDeviceBatteryobject at all and a pad that reports0.0looks the same, so both end up as the module's0— "unknown" — and the number is hidden rather than printed as0%. -
On macOS the device order is not stable. The GameController entries come first, in the
framework's own order, but the HID entries come from a set (
IOHIDManagerCopyDevices()), which has no defined order, so with two controllers attached the lines — and theGamepad 1/Gamepad 2numbers — can swap between runs. Linux and Windows enumerate in a stable OS order. -
On FreeBSD the same controller can be listed twice. The report descriptor is walked and an
entry is appended for every matching top-level usage (page 1, usage 1, 4 or 5) without a
duplicate check or a
break, so a device that advertises two of those usages produces two identical lines with two different index numbers. -
On Linux the battery probe stops at the first directory that answers. The scan of
.../power_supply/*readscapacityand, failing that,<name>_level, andbreaks as soon as one of them returns data. A device whosepower_supplydirectory holds more than one entry therefore reports whichever entry the kernel happened to list first. -
On Linux the Switch Pro level is a five-step ladder, not a percentage.
_levelis a word —Critical→ 1,Low→ 25,Normal→ 50,High→ 75,Full→ 100 — so the bar can only ever show five values on that controller. -
macOS and Linux disagree about the Switch Pro percentage. Linux maps the kernel's five-step
_levelladder (1 / 25 / 50 / 75 / 100), whereas macOS asks Apple's framework, which reported30%for a controller Linux showed asNormal. The raw field is a coarse band, not a percentage, so no two of the three platforms agree on a Switch Pro. -
Nothing at all is printed when
ignoresmatches every device, unlessdisplay.showErrorsis on, where the message isAll devices are ignored. The JSON path still lists the devices. -
ffPrintError()writes to stdout. Withdisplay.showErrors: truean error line is printed before the JSON array, so the run is no longer parseable. KeepshowErrorsoff in anything that pipes the JSON output.
ffPrintGamepad() and ffGenerateGamepadJsonResult() both call ffDetectGamepad() on a fresh list
of FFGamepadDevice, but they treat ignores differently: the print path builds a second list of
pointers to the devices that survive the filter and prints from that, while the JSON path iterates
the full list and writes the ignored flag. Neither path is cached, so a --dynamic-interval run
re-enumerates the controllers every round.
GetRawInputDeviceList() supplies the HID devices; each one must have usUsagePage == 1 and
usUsage 4 (Joystick) or 5 (Gamepad). The name comes from a small built-in table keyed by
vendor/product id — Nintendo 0x057E, Sony 0x054C, Logitech 0x046D; Microsoft 0x045E is
deliberately left to Windows' own naming. When the device cannot be opened at all and is not in the
table, the name becomes Unknown gamepad VVVV-PPPP. For other devices
HidD_GetProductString() and HidD_GetManufacturerString() are combined into
Manufacturer Product, and the serial comes from HidD_GetSerialNumberString().
The battery is read only for the DualShock 4 and Switch Pro ids listed above, from a single input
report obtained through an overlapped ReadFile() with a one-second wait. The DualShock 4 path reads
byte 30 or 32 depending on whether the report length is 64, masks a 4-bit level and divides by 11
while charging or 8 otherwise; the Switch path requires report id 0x30 and takes three bits from
byte 2. Both clamp the result to 100.
/sys/class/input/ is scanned for names starting with js followed by a digit. The device name is
the contents of .../device/name, and the serial is uniq. The battery comes from
.../device/device/power_supply/<entry>/capacity when that file has data, otherwise from
<entry>_level, whose word is mapped through the five-step ladder described above.
/dev/uhidN is opened for N = 0…63, with ENOENT ending the loop and any other error continuing to
the next index. hid_get_report_desc() / hid_start_parse() walk the report descriptor looking for a
top-level usage of page 1 with usage 1, 4 or 5 (usage 1 is accepted because FreeBSD reports a Pro
Controller that way), and USB_GET_DEVICEINFO supplies udi_vendor + udi_product as the name and
udi_serial as the serial. No battery is read.
macOS offers two overlapping views of the same controller, and the backend uses both, because neither replaces the other:
-
IOHIDManagersees every HID device whose primary usage is a joystick or a gamepad, third-party pads included, and is the only source of the manufacturer, the product and the serial number — the framework's own entries borrow the serial from here (see below). -
GCControlleris the GameController framework's view of the pads Apple has whitelisted, and is the only source of a battery level (GCDeviceBattery, macOS 11.0+). It is what the system's own games read.
ffDetectGamepad() enumerates the HID devices first, then asks +[GCController supportsHIDDevice:]
whether the framework claims any of them. Only if it does are the framework's controllers appended as
whole entries — name from productCategory, falling back to vendorName and then to the literal
MFi Gamepad, with battery taken from GCDeviceBattery.batteryLevel — and the HID pass then
skips any device the framework claimed, so the same pad is not listed twice. IOHIDManager is
matched against GenericDesktop / Joystick and GenericDesktop / GamePad and stays open for the
lifetime of the call; those entries are named kIOHIDManufacturerKey + + kIOHIDProductKey when
a manufacturer is present, otherwise the product alone, with kIOHIDSerialNumberKey as the serial.
The two views stay separate entries — there is no public way to get the IOHIDDeviceRef behind a
GCController (+[GCController supportsHIDDevice:] answers in the other direction only), and SDL,
the reference implementation for all of this, does not pair them either (SDL_mfijoystick.m: "we
don't have an easy way to know if those devices correspond to a specific GCController"). The serial is
the one exception, because it is the only field both views carry and therefore the only one that would
otherwise be spelled two different ways for the same pad depending on which pass contributed the
entry. The framework entry borrows it, through the private GCController.identifier, which is the
transport address in the very form IOKit reports it:
GCController.identifier LOGICAL_DEVICE(5c-52-1e-88-7e-70)
IOHIDDevice serial 5C:52:1E:88:7E:70
identifier is not declared in the SDK header, and its declared type in the framework's own metadata
is opaque, but the value is a plain string, so it is read as one. Both sides are reduced to their hex
digits before being compared. A pad whose identity is not shaped like that — a wired pad with a real
serial number, say — simply does not match and keeps an empty serial rather than getting a guessed
one. The skip is also conditional on the framework having contributed something: with an empty
controllers array there is no duplicate to avoid, and skipping unconditionally would drop the device
from the output altogether.
That conditionality matters because the array is often empty here. It is filled by the private
_GCControllerManager once it is told the application became active, which a command line tool never
tells it, and the fill only happens while the run loop is pumped. The module pumps for it, in a loop
bounded by 50 ms, but only on the branch anyDeviceIsClaimedByGameController() opens — reading the
list and taking the run loop turn together cost ~14 ms, and neither is refunded when the array turns
out to be empty after all. Each turn is given whatever is left of the 50 ms rather than a zero
timeout, because a zero timeout returns as soon as nothing is pending and the loop then spins at 100%
CPU for the length of the wait.
A controller the framework does claim is not guaranteed a battery either: the level is fetched from
the device's own driver over XPC (physicalDevice:%@ getBattery, with
Unable to receive response from driver battery service! as the failure path), so a pad whose driver
implements no battery service reports nothing. Coverage is therefore Apple's allow list, the 37
vendor/product pairs in AppleGameControllerPersonality.kext, and HID devices that "look like an MFi
gamepad" — not every HID pad.
BJoystick::CountDevices() / GetDeviceName() supply the names; serial and battery stay empty.
[ { "type": "Gamepad", "result": [ { "serial": "", "name": "Sony DualSense", "battery": 0, "ignored": false } ] } ]