Repository navigation
Modules Display
Print resolutions, refresh rates, etc
| Module type | display |
| Default order | 17 (only used by --gen-config) |
| Module source | src/modules/display/display.c |
| Detection source | src/detection/displayserver/ |
Prints one line per connected display: the configured resolution, the HiDPI scale factor, the physical diagonal, the refresh rate, and markers for the display type and the primary display.
Display (Color LCD): 3456x2234 @ 2x in 16", 120 Hz [Built-in] *
Display (Sample Monitor): 3840x2160 @ 2x in 32", 60 Hz [External]
Every part after the resolution is conditional: @ <n>x appears only when the DPI is not 96,
in <n>" only when the physical size is known and the diagonal is longer than an inch, and the
refresh rate only when one was reported. [Built-in]/[External] is omitted when the type is
unknown, and the trailing * marks the primary display — but only when more than one display is
connected.
Display and Monitor are the same detection behind two different default
layouts. Brightness, DE, WM, WMTheme, Theme, Icons, Font, Cursor, Wallpaper and
TerminalFont read the same detection result — see Implementation.
The module layer is platform independent. The detection layer is the displayserver subsystem,
which is shared by several modules and always has a real implementation on all ten platforms.
| Platform | Implementation | Notes |
|---|---|---|
| Linux |
linux/displayserver_linux.c + common.c, wmde.c, xcb.c, xlib.c, drm.c, wayland/
|
Wayland, then XCB, then Xlib, then DRM/sysfs |
| Android |
displayserver_android.c + common.c, wmde.c, xcb.c, xlib.c, wayland/
|
The display service over binder. No drm.c
|
| FreeBSD |
linux/displayserver_linux.c + the same helper set as Linux |
Plus a kenv fallback for text consoles |
| NetBSD |
linux/displayserver_linux.c + the same helper set as Linux |
|
| OpenBSD |
linux/displayserver_linux.c + the same helper set as Linux |
|
| Solaris/illumos |
linux/displayserver_linux.c + the same helper set as Linux |
|
| Haiku | displayserver_haiku.cpp |
BScreen |
| GNU/Hurd |
linux/displayserver_linux.c + a reduced Wayland set |
Only wayland/wayland.c; the KDE, xdg-output and colour-management protocol files are not compiled |
| macOS | displayserver_apple.c |
CoreGraphics |
| Windows | displayserver_windows.c |
GDI / DisplayConfigGetDeviceInfo
|
src/detection/displayserver/displayserver.c itself is in the common source list
(CMakeLists.txt:483) and holds the cache entry, ffdsAppendDisplay() and the DPI normalisation.
Because the Linux implementation is reused on the BSDs and Solaris, those platforms inherit the same backend order — Wayland first, then X11 through XCB, then X11 through Xlib, and only if all of them fail, DRM. On a machine with no display server at all (a text console, a container), the DRM/sysfs path is what answers.
| Key | Type | Default | Description |
|---|---|---|---|
key |
string | module name + display name | Module key. A single space hides the key and the separator. |
keyColor |
color | – | Overrides display.color.keys. |
keyWidth |
integer | – | Overrides display.key.width. |
keyIcon |
string | built-in glyph | Printed when display.key.type includes the icon bit. Any glyph works, "" prints none. |
outputColor |
color | – | Overrides display.color.output. |
format |
string | – | Custom output format (see below). Ignored by the compact layouts. |
condition |
object | – | Show the module only if the conditions match. |
compactType |
string or null
|
"none" |
Lay all displays out on a single line (see below). |
preciseRefreshRate |
boolean | false |
Print the refresh rate as reported instead of rounding it to a whole number. |
order |
string or null
|
"none" |
Sort the displays by name: "asc", "desc" or "none". |
The default key is Display (<name>), falling back to Display (<n>) when the display has no
name and to a bare Display when there is only one.
compactType replaces the whole output with one line listing every display. Only four values are
accepted by the parser (display.c:216-223); the value is stored as a bit set, which is why the
last two combine two flags.
| Value | Resolution shown | Refresh rate | Example |
|---|---|---|---|
"none" (default) |
– | – | the two-line output above |
"original" |
the configured mode | no | Display: 3456x2234 3840x2160 |
"scaled" |
96-DPI normalised | no | Display: 1728x1117 1920x1080 |
"original-with-refresh-rate" |
the configured mode | yes | Display: 3456x2234 @ 120 Hz, 3840x2160 @ 60 Hz |
"scaled-with-refresh-rate" |
96-DPI normalised | yes | Display: 1728x1117 @ 120 Hz, 1920x1080 @ 60 Hz |
There is no null handling difference: "compactType": null resets it to "none".
Two things surprise people here. The compact layouts ignore format entirely — the compact
branch returns before the format string is ever read (see Pitfalls). And scaled is not the
resolution your desktop reports; it is the physical mode re-divided by the DPI, normalised to
96 DPI:
scaledWidth = (width * 96 + dpi / 2) / dpi; // display.c:38-39, :335-336On a HiDPI panel whose compositor reports a fractional scale (1.5x, 2.5x), that number will not
match what the desktop shows. "compactType": "original" is the only way to get the physical mode
back.
order sorts the display list by name, ascending or descending, with ffStrbufComp(). Both the
console output and the JSON result follow it. The sort is applied to a local copy of the detection
result, so no other module sees the reordered list — see Pitfalls.
Run fastfetch -h display-format for the authoritative list. Descriptions ending in * are also
available in the module key format string.
| Variable | Description |
|---|---|
{width} |
Configured width in pixels |
{height} |
Configured height in pixels |
{refresh-rate} |
Refresh rate in Hz, rounded unless preciseRefreshRate is set. Empty when unknown |
{scaled-width} |
96-DPI normalised width |
{scaled-height} |
96-DPI normalised height |
{name} |
Display name * |
{type} |
Built-in or External, empty when unknown * |
{rotation} |
Rotation in degrees |
{is-primary} |
true for the primary display |
{physical-width} |
Physical width in millimetres |
{physical-height} |
Physical height in millimetres |
{inch} |
Physical diagonal in inches, rounded to a whole number |
{ppi} |
Pixels per inch, rounded; 0 when the physical size is unknown |
{bit-depth} |
Bits per colour channel |
{hdr-enabled} |
true when HDR is currently enabled |
{hdr-compatible} |
true when the display supports HDR, whether or not it is on |
{manufacture-year} |
Year of manufacture, 0 when unknown |
{manufacture-week} |
Week of manufacture, 0 when unknown |
{serial} |
Serial number, empty when unknown |
{platform-api} |
Which backend produced this entry (see Implementation) |
{scale-factor} |
dpi / 96, formatted with display.fraction.ndigits
|
{preferred-width} |
Preferred width in pixels |
{preferred-height} |
Preferred height in pixels |
{preferred-refresh-rate} |
Preferred refresh rate in Hz, empty when unknown |
{dpi} |
The DPI value as detected |
Inside key the available variables are {index}, {name}, {type}, {icon} and
{module-name}.
Notes on the shape:
-
resultis an array, one object per display, in detection order unlessorderis set. -
refreshRateis always a float and is always present;0.0means unknown. The console output suppresses it in that case, the JSON does not. -
drrStatusisnullon almost every platform."Enabled"/"Disabled"come from the Windows and Wayland paths only. -
hdrStatusis one of"Unsupported","Supported","Enabled"ornull."Supported"does not mean HDR is on —hdr-enabledis the field that means that. -
manufactureDateisnullunless EDID was parsed, in which case it is{ "year": …, "week": … }.serialisnullwhen unknown. -
typeis"Builtin"/"External"/"Unknown"— note the lowercasei, unlike theBuilt-inspelling used in the console output and in{type}. -
idis a platform handle, not an index: aCGDirectDisplayIDon macOS, anHMONITORon Windows, a DRM connector id on Linux, aBScreenid on Haiku, and0on several paths. -
platformApiis a free-form string naming the backend that produced the entry. It is the only reliable way to tell whether a value came from Wayland, X11, DRM or sysfs. -
compactType,preciseRefreshRateandorderare not reflected in the JSON at all.
// One line, physical modes, refresh rates
{ "type": "display", "compactType": "original-with-refresh-rate" }// Keep the physical mode but drop the refresh rate, and sort by name
{ "type": "display", "compactType": "original", "order": "asc" }// A custom line, with the backend that answered
{ "type": "display", "format": "{name}: {width}x{height} @ {refresh-rate}Hz via {platform-api}" }// Only the primary display, named by the key instead of by the module
{ "type": "display", "key": "Screen {index}", "format": "{is-primary} {ppi}ppi" }-
The compact layouts ignore
format. The wholecompactType != noneblock ends inreturn true, somoduleArgs.outputFormatis never read. No warning is printed, and"compactType": "scaled"with"format": "FMT:{width}x{height}"still printsDisplay: 1728x1117 1920x1080. ReadingcompactTypeas "just a layout change" is wrong — it also disables every format variable. -
display.freq.spaceBeforeUnitbehaves the same in both layouts. Compact and normal both test!= FF_SPACE_BEFORE_UNIT_NEVER, so"default"and"always"print120 Hzand only"never"prints120Hz:display.freq.spaceBeforeUnitnormal layout compact layout "default"120 Hz120 Hz"always"120 Hz120 Hz"never"120Hz120Hz -
Other modules always see detection order, not
order.ffConnectDisplayServer()returns a pointer to a process-wide static, andDisplaysorts a local copy of it rather than the shared list. SoMonitor, which prints the same list, does not followDisplay'sorder; neither doesBrightness, which pairs its entries with the display list positionally on Linux and Windows. SetorderonMonitortoo if you want the two lines to match. -
{ppi}is0when the physical size is unknown.ppiis derived from{inch}, which is derived from the EDID physical size. A virtual or remote display, or one whose EDID is not readable, reports0. -
@ <n>xis a scale factor, not a resolution. It is only printed when the detected DPI differs from 96, and it isdpi / 96— so a 1.5x desktop prints@ 1.5x, and a display whose DPI could not be determined prints nothing rather than@ 1x. -
The
*primary marker needs at least two displays.display.c:136gates it onmoduleIndex > 0, which is0when there is only one display. A single-display machine never shows it, even thoughprimaryistruein the JSON. -
{type}and{name}can be empty. Both are marked*in-h display-formatfor that reason.{type}is empty when the backend could not classify the display, which is common on the DRM path. -
{hdr-compatible}and{hdr-enabled}are different questions."Supported"in the JSON means the panel can do HDR; only"Enabled"means it is on, and only"Enabled"produces the[HDR]marker in the default output. -
hdrStatusis"Unsupported"rather thannullwhen EDID says so.nullmeans "not determined". A backend that does not read EDID at all leaves itnull, so the two are not interchangeable. -
serialcan be a non-string-looking value. EDID serials are often a hex word ("0x12345678") on panels that do not carry a textual one. It is a string either way. -
The compact layout's separator is positional, not configurable. With
compactType: originalthe displays are joined by a space and there is no way to change it; with a-with-refresh-ratevalue they are joined by", ". A trailing separator is trimmed (display.c:57-58). -
On Android there is one route, and it fails closed.
platformApiis alwaysbinder; thecmdanddumpsysroutes that used to answer when the binder parser declined are gone. A device whoseDisplayInfolayout the parser does not recognise now reports no display at all rather than a slower one — so an emptyresulton Android means the parser declined the record, not that the device has no display. -
On Android, the refresh rate is the float the framework carries, not a rounded value. The parcel holds it as a 32-bit float, so a 120 Hz panel reads
120.00000762939453in the JSON where the text route used to print120.00001. The console output is unaffected — the default rounds the rate to a whole number, and onlypreciseRefreshRateshows the difference.
ffConnectDisplayServer() (src/detection/displayserver/displayserver.c:93) returns a pointer to
a file-scope static held by an FFcacheEntry named displayServer. The entry is built on first
use by calling the platform's ffConnectDisplayServerImpl(), and destroyed — including every
display's name and serial strbuf — when ffCacheInvalidateAll() runs at a
--dynamic-interval round boundary. In other words the whole subsystem is re-detected on every
round of --dynamic-interval, and cached for the whole run otherwise.
Every backend appends through ffdsAppendDisplay() (displayserver.c:4), which is the single
place where the contract is enforced:
- A display with
width == 0orheight == 0is dropped andnullptris returned. -
display->dpi = dpi ?: 96; // 0 means unknown(:29) — a backend that cannot report a DPI gets 96, soscale-factoris1.0andscaledequals the configured mode. This is also why thescaled-width/scaled-heightdivision can never divide by zero. -
nameis moved, not copied (ffStrbufInitMove,:34); callers must not destroy it afterwards. -
bitDepth,hdrStatus,manufactureYear,manufactureWeek,serialanddrrStatusare reset to their unknown values, so a backend that has EDID data fills them in after the call — which is exactly what the X11, DRM and macOS paths do.
The result struct also carries the WM and DE fields (wmProcessName, wmPrettyName,
wmProtocolName, deProcessName, dePrettyName). They are filled by the same detection — on
Linux by ffdsDetectWMDE() (linux/wmde.c) — which is why DE, WM, Icons, Theme,
Cursor, Font, Wallpaper and TerminalFont all trigger the same detection as Display.
ffConnectDisplayServerImpl() (linux/displayserver_linux.c:43) tries, in order, stopping at the
first backend that produces at least one display:
| Order | Backend | File |
platformApi values |
|---|---|---|---|
| 1 | Wayland |
wayland/global-output.c, kde-output.c
|
wayland-base, wayland-zxdg, wayland-wpcolor, wayland-zxdg+wpcolor, wayland-kde
|
| 2 | XCB + RandR | xcb.c |
xcb-randr-mode, xcb-randr-crtc, xcb-randr-monitor, xcb-randr-screen, plus -emu- variants under XWayland |
| 3 | Xlib + RandR | xlib.c |
xlib-randr-mode, xlib-randr-crtc, xlib-randr-monitor, xlib-randr-screen, plus -emu- variants |
| 4 | DRM ioctls | drm.c |
libdrm |
| 5 | sysfs | drm.c |
sysfs-drm |
Steps 1–3 are skipped entirely when general.dsForceDrm is set. The -emu- suffix means XWayland:
RandR reports a single emulated output, so the per-CRTC and per-mode paths are distinguishable from
the real ones.
The X11 backends read EDID from the output's RandR property and fill in hdrStatus (via
ffEdidGetHdrCompatible()), manufactureYear/manufactureWeek and serial. On X11 the rotation
case needs care: when rotation is 90 or 180 and RandR is not emulated, width and height are
swapped after the fact (xcb.c:234-239), because XWayland already swaps them itself.
The DRM path needs no display server. drm.c first walks /sys/class/drm/*/modes (the preferred
mode, which is why the entry is reported as sysfs-drm), and falls back to opening the DRM device
and issuing DRM_IOCTL_MODE_GETRESOURCES / GETCONNECTOR / GETCRTC (libdrm). On the libdrm
path the name comes from the connector type plus index (eDP-1, HDMI-A-1, …) unless EDID
supplies a real name, and the display type is derived from the connector type: eDP and LVDS are
Built-in, HDMI-A, HDMI-B and DisplayPort are External, everything else is Unknown.
FreeBSD adds one more fallback after DRM: if there is still no display, kenv is consulted for
screen.width and screen.height (displayserver_linux.c:67-81), which is how a plain text
console gets a resolution. The entry is reported with platformApi kenv.
After the display list is settled, ffdsDetectWMDE() fills in the WM/DE fields, but only when the
session is not a bare TTY.
displayserver_android.c has one route: the display service
(android.hardware.display.IDisplayManager) over /dev/binder, reported as platformApi binder.
It needs no permission and starts no child process, which is what makes it the only route an app UID
can use at all — cmd display get-displays does not exist before Android 13, and dumpsys display is
behind android.permission.DUMP. The cmd, dumpsys and vendor getprop routes that used to sit
behind it have been removed, because binder answers on every release they covered and on the ones none
of them could.
It reads the enabled displays from getDisplayIds() first, because display ids are not contiguous,
and then asks for one getDisplayInfo() per id. What comes back is the same DisplayInfo record a
dump prints, field for field — but it arrives as a Java Parcelable instead of as text, and that is
where the work is.
That record's field set moves with every release and every vendor fork: DisplayInfo writes 34 fields
on Android 11, 42 on 13 and 57 on 16. A fixed offset would silently read a plausible number out of the
middle of a different field, so the write order is read out of the device's own
/system/framework/framework.jar — from the iget order of DisplayInfo.writeToParcel — and the
record is walked field by field along it with a bounds-checked cursor. Every nested Parcelable is
anchored on its class name, every value that can be checked against its neighbours is checked
(type, displayId, rotation, the mode id, the refresh rates, the density and both DPIs, and
booleans that must be 0 or 1), and every display has to parse before any of them is reported.
A field the running release does not write is simply not in the sequence, which is what makes every one
of them optional without a version check; an unrecognised type is a hard failure, because the type
descriptor is the only thing that says how long a field is. The point is never to guess a value.
Two of the shapes behind that are worth naming, because they are what the Android 11 and Android 16
layouts were verified against. Display$Mode is walked by its own sequence rather than by a fixed
field count — its fourth field is mRefreshRate on Android 11 and 13 and mPeakRefreshRate on 16,
and both names are matched. And DeviceProductInfo.mManufactureDate goes through
Parcel.writeValue, which prefixes a length on Android 13 and later but not on Android 11, so both
framings are tried against the class name: it is a constant string, which makes the attempt decisive.
Android 11 has been read this way on a device. Android 12 has not, and it sits between the two layouts
that have.
Android's build block deliberately omits drm.c — there are no DRM device nodes to read.
displayserver_apple.c uses CoreGraphics: CGGetOnlineDisplayList(), then
CGDisplayCopyDisplayMode() / CGDisplayModeGetWidth() / CGDisplayModeGetHeight() for the
pixel mode and CGDisplayModeGetRefreshRate() for the rate. The DPI is computed from the pixel
height over the point height, and platformApi is always CoreGraphics.
CGDisplayRotation() supplies the rotation. On macOS 10.11 and later the bit depth comes from the
mode's pixel encoding, on older systems from CGDisplayModeCopyPixelEncoding(). The built-in
panel is recognised by CGDisplayIsBuiltin(); the name comes from the display's product
dictionary. HDR status is filled from EDID where the panel provides it.
The DDC/CI path that Brightness uses on macOS lives in the brightness subsystem, not here — this
module only reports what CoreGraphics knows.
displayserver_windows.c enumerates monitors with EnumDisplayMonitors(), then resolves each one
through GetDisplayConfigBufferSizes() / QueryDisplayConfig() /
DisplayConfigGetDeviceInfo(), which is what provides the target name, the refresh rate as a
rational number, and the rotation. Width and height are swapped when the rotation is 90 or 270
(displayserver_windows.c:132-137), so the reported mode is always in the panel's own
orientation. The system DPI comes from GetDeviceCaps(hdc, LOGPIXELSX), defaulting to 96 when the
device context cannot be obtained. platformApi is always GDI.
displayserver_haiku.cpp iterates BScreen objects. The refresh rate is computed from the mode
timing rather than read directly:
(double) mode.timing.pixel_clock * 1000 / (mode.timing.v_total * mode.timing.h_total)platformApi is BScreen. The manufacturer week and year come from the monitor info, which is
only available on some panels.
drrStatus (dynamic refresh rate) is only filled by the Windows and Wayland paths; on every other
backend it stays unknown and serialises as null. bitDepth is filled by the X11, DRM and macOS
paths. On the Android route hdrStatus and manufactureDate are filled — from the record's own
hdrCapabilities and deviceProductInfo fields — while bitDepth and serial are left at their
defaults, so Android entries serialise serial: null.
{ "type": "Display", "result": [ { "id": 1, "name": "Color LCD", "primary": true, "output": { "width": 3456, "height": 2234, "refreshRate": 120.0, "drrStatus": null, "dpi": 192 }, "scaled": { "width": 1728, "height": 1117 }, "preferred": { "width": 3456, "height": 2234, "refreshRate": 120.0 }, "physical": { "width": 344, "height": 223 }, "rotation": 0, "bitDepth": 10, "hdrStatus": "Supported", "type": "Builtin", "manufactureDate": null, "serial": "0x12345678", "platformApi": "CoreGraphics" } ] }