#include "common/printing.h" #include "common/jsonconfig.h" #include "detection/displayserver/displayserver.h" #include "modules/display/display.h" #include "util/stringUtils.h" #include static int sortByNameAsc(FFDisplayResult* a, FFDisplayResult* b) { return ffStrbufComp(&a->name, &b->name); } static int sortByNameDesc(FFDisplayResult* a, FFDisplayResult* b) { return -ffStrbufComp(&a->name, &b->name); } void ffPrintDisplay(FFDisplayOptions* options) { const FFDisplayServerResult* dsResult = ffConnectDisplayServer(); if(dsResult->displays.length == 0) { ffPrintError(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Couldn't detect display"); return; } if (options->order != FF_DISPLAY_ORDER_NONE) { ffListSort((FFlist*) &dsResult->displays, (void*) (options->order == FF_DISPLAY_ORDER_ASC ? sortByNameAsc : sortByNameDesc)); } if (options->compactType != FF_DISPLAY_COMPACT_TYPE_NONE) { ffPrintLogoAndKey(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT); FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); FF_LIST_FOR_EACH(FFDisplayResult, result, dsResult->displays) { if (options->compactType & FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT) { ffStrbufAppendF(&buffer, "%ix%i", result->width, result->height); } else { ffStrbufAppendF(&buffer, "%ix%i", result->scaledWidth, result->scaledHeight); } if (options->compactType & FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT) { if (result->refreshRate > 0) { if (options->preciseRefreshRate) ffStrbufAppendF(&buffer, " @ %gHz", result->refreshRate); else ffStrbufAppendF(&buffer, " @ %iHz", (uint32_t) (result->refreshRate + 0.5)); } ffStrbufAppendS(&buffer, ", "); } else { ffStrbufAppendC(&buffer, ' '); } } ffStrbufTrimRight(&buffer, ' '); ffStrbufTrimRight(&buffer, ','); ffStrbufPutTo(&buffer, stdout); return; } FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate(); for(uint32_t i = 0; i < dsResult->displays.length; i++) { FFDisplayResult* result = FF_LIST_GET(FFDisplayResult, dsResult->displays, i); uint32_t moduleIndex = dsResult->displays.length == 1 ? 0 : i + 1; const char* displayType = result->type == FF_DISPLAY_TYPE_UNKNOWN ? NULL : result->type == FF_DISPLAY_TYPE_BUILTIN ? "Built-in" : "External"; ffStrbufClear(&key); if(options->moduleArgs.key.length == 0) { if (result->name.length) ffStrbufAppendF(&key, "%s (%s)", FF_DISPLAY_MODULE_NAME, result->name.chars); else if (moduleIndex > 0) ffStrbufAppendF(&key, "%s (%d)", FF_DISPLAY_MODULE_NAME, moduleIndex); else ffStrbufAppendS(&key, FF_DISPLAY_MODULE_NAME); } else { FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) { FF_FORMAT_ARG(moduleIndex, "index"), FF_FORMAT_ARG(result->name, "name"), FF_FORMAT_ARG(displayType, "type"), FF_FORMAT_ARG(options->moduleArgs.keyIcon, "icon"), })); } FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); double inch = sqrt(result->physicalWidth * result->physicalWidth + result->physicalHeight * result->physicalHeight) / 25.4; if(options->moduleArgs.outputFormat.length == 0) { ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY); ffStrbufAppendF(&buffer, "%ix%i", result->width, result->height); if(result->refreshRate > 0) { if(options->preciseRefreshRate) ffStrbufAppendF(&buffer, " @ %g Hz", ((int) (result->refreshRate * 1000 + 0.5)) / 1000.0); else ffStrbufAppendF(&buffer, " @ %i Hz", (uint32_t) (result->refreshRate + 0.5)); } if( result->scaledWidth > 0 && result->scaledWidth != result->width && result->scaledHeight > 0 && result->scaledHeight != result->height) ffStrbufAppendF(&buffer, " (as %ix%i)", result->scaledWidth, result->scaledHeight); if (inch > 1) ffStrbufAppendF(&buffer, " in %i\"", (uint32_t) (inch + 0.5)); bool flag = false; if (result->type != FF_DISPLAY_TYPE_UNKNOWN) { ffStrbufAppendS(&buffer, result->type == FF_DISPLAY_TYPE_BUILTIN ? " [Built-in" : " [External"); flag = true; } if (result->hdrStatus == FF_DISPLAY_HDR_STATUS_ENABLED) { ffStrbufAppendS(&buffer, flag ? ", HDR" : " [HDR"); flag = true; } if (flag) ffStrbufAppendS(&buffer, "]"); if(moduleIndex > 0 && result->primary) ffStrbufAppendS(&buffer, " *"); ffStrbufPutTo(&buffer, stdout); ffStrbufClear(&buffer); } else { double ppi = inch == 0 ? 0 : sqrt(result->width * result->width + result->height * result->height) / inch; bool hdrEnabled = result->hdrStatus == FF_DISPLAY_HDR_STATUS_ENABLED; bool hdrCompatible = result->hdrStatus == FF_DISPLAY_HDR_STATUS_SUPPORTED || result->hdrStatus == FF_DISPLAY_HDR_STATUS_ENABLED; uint32_t iInch = (uint32_t) (inch + 0.5), iPpi = (uint32_t) (ppi + 0.5); char refreshRate[16]; if(result->refreshRate > 0) { if(options->preciseRefreshRate) snprintf(refreshRate, ARRAY_SIZE(refreshRate), "%g", ((int) (result->refreshRate * 1000 + 0.5)) / 1000.0); else snprintf(refreshRate, ARRAY_SIZE(refreshRate), "%i", (uint32_t) (result->refreshRate + 0.5)); } else refreshRate[0] = 0; char preferredRefreshRate[16]; if(result->preferredRefreshRate > 0) { if(options->preciseRefreshRate) snprintf(preferredRefreshRate, ARRAY_SIZE(preferredRefreshRate), "%g", ((int) (result->preferredRefreshRate * 1000 + 0.5)) / 1000.0); else snprintf(preferredRefreshRate, ARRAY_SIZE(preferredRefreshRate), "%i", (uint32_t) (result->preferredRefreshRate + 0.5)); } else preferredRefreshRate[0] = 0; char buf[32]; if (result->serial) { const uint8_t* nums = (uint8_t*) &result->serial; snprintf(buf, ARRAY_SIZE(buf), "%2X-%2X-%2X-%2X", nums[0], nums[1], nums[2], nums[3]); } else buf[0] = '\0'; double scaleFactor = (double) result->height / (double) result->scaledHeight; FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) { FF_FORMAT_ARG(result->width, "width"), FF_FORMAT_ARG(result->height, "height"), FF_FORMAT_ARG(refreshRate, "refresh-rate"), FF_FORMAT_ARG(result->scaledWidth, "scaled-width"), FF_FORMAT_ARG(result->scaledHeight, "scaled-height"), FF_FORMAT_ARG(result->name, "name"), FF_FORMAT_ARG(displayType, "type"), FF_FORMAT_ARG(result->rotation, "rotation"), FF_FORMAT_ARG(result->primary, "is-primary"), FF_FORMAT_ARG(result->physicalWidth, "physical-width"), FF_FORMAT_ARG(result->physicalHeight, "physical-height"), FF_FORMAT_ARG(iInch, "inch"), FF_FORMAT_ARG(iPpi, "ppi"), FF_FORMAT_ARG(result->bitDepth, "bit-depth"), FF_FORMAT_ARG(hdrEnabled, "hdr-enabled"), FF_FORMAT_ARG(result->manufactureYear, "manufacture-year"), FF_FORMAT_ARG(result->manufactureWeek, "manufacture-week"), FF_FORMAT_ARG(buf, "serial"), FF_FORMAT_ARG(result->platformApi, "platform-api"), FF_FORMAT_ARG(hdrCompatible, "hdr-compatible"), FF_FORMAT_ARG(scaleFactor, "scale-factor"), FF_FORMAT_ARG(result->preferredWidth, "preferred-width"), FF_FORMAT_ARG(result->preferredHeight, "preferred-height"), FF_FORMAT_ARG(preferredRefreshRate, "preferred-refresh-rate"), })); } } } bool ffParseDisplayCommandOptions(FFDisplayOptions* options, const char* key, const char* value) { const char* subKey = ffOptionTestPrefix(key, FF_DISPLAY_MODULE_NAME); if (!subKey) return false; if (ffOptionParseModuleArgs(key, subKey, value, &options->moduleArgs)) return true; if (ffStrEqualsIgnCase(subKey, "compact-type")) { options->compactType = (FFDisplayCompactType) ffOptionParseEnum(key, value, (FFKeyValuePair[]) { { "none", FF_DISPLAY_COMPACT_TYPE_NONE }, { "original", FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT }, { "scaled", FF_DISPLAY_COMPACT_TYPE_SCALED_BIT }, { "original-with-refresh-rate", FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT }, { "scaled-with-refresh-rate", FF_DISPLAY_COMPACT_TYPE_SCALED_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT }, {}, }); return true; } if (ffStrEqualsIgnCase(subKey, "precise-refresh-rate")) { options->preciseRefreshRate = ffOptionParseBoolean(value); return true; } if (ffStrEqualsIgnCase(subKey, "order")) { options->order = (FFDisplayOrder) ffOptionParseEnum(key, value, (FFKeyValuePair[]) { { "asc", FF_DISPLAY_ORDER_ASC }, { "desc", FF_DISPLAY_ORDER_DESC }, { "none", FF_DISPLAY_ORDER_NONE }, {}, }); return true; } return false; } void ffParseDisplayJsonObject(FFDisplayOptions* options, yyjson_val* module) { yyjson_val *key_, *val; size_t idx, max; yyjson_obj_foreach(module, idx, max, key_, val) { const char* key = yyjson_get_str(key_); if(ffStrEqualsIgnCase(key, "type")) continue; if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) continue; if (ffStrEqualsIgnCase(key, "compactType")) { int value; const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) { { "none", FF_DISPLAY_COMPACT_TYPE_NONE }, { "original", FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT }, { "scaled", FF_DISPLAY_COMPACT_TYPE_SCALED_BIT }, { "original-with-refresh-rate", FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT }, { "scaled-with-refresh-rate", FF_DISPLAY_COMPACT_TYPE_SCALED_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT }, {}, }); if (error) ffPrintError(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", key, error); else options->compactType = (FFDisplayCompactType) value; continue; } if (ffStrEqualsIgnCase(key, "preciseRefreshRate")) { options->preciseRefreshRate = yyjson_get_bool(val); continue; } if (ffStrEqualsIgnCase(key, "order")) { int value; const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) { { "asc", FF_DISPLAY_ORDER_ASC }, { "desc", FF_DISPLAY_ORDER_DESC }, { "none", FF_DISPLAY_ORDER_NONE }, {}, }); if (error) ffPrintError(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", key, error); else options->order = (FFDisplayOrder) value; continue; } ffPrintError(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", key); } } void ffGenerateDisplayJsonConfig(FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { __attribute__((__cleanup__(ffDestroyDisplayOptions))) FFDisplayOptions defaultOptions; ffInitDisplayOptions(&defaultOptions); ffJsonConfigGenerateModuleArgsConfig(doc, module, &defaultOptions.moduleArgs, &options->moduleArgs); if (options->compactType != defaultOptions.compactType) { switch ((int) options->compactType) { case FF_DISPLAY_COMPACT_TYPE_NONE: yyjson_mut_obj_add_str(doc, module, "compactType", "none"); break; case FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT: yyjson_mut_obj_add_str(doc, module, "compactType", "original"); break; case FF_DISPLAY_COMPACT_TYPE_SCALED_BIT: yyjson_mut_obj_add_str(doc, module, "compactType", "scaled"); break; case FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT: yyjson_mut_obj_add_str(doc, module, "compactType", "original-with-refresh-rate"); break; case FF_DISPLAY_COMPACT_TYPE_SCALED_BIT | FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT: yyjson_mut_obj_add_str(doc, module, "compactType", "scaled-with-refresh-rate"); break; } } if (options->preciseRefreshRate != defaultOptions.preciseRefreshRate) yyjson_mut_obj_add_bool(doc, module, "preciseRefreshRate", options->preciseRefreshRate); } void ffGenerateDisplayJsonResult(FF_MAYBE_UNUSED FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) { const FFDisplayServerResult* dsResult = ffConnectDisplayServer(); if(dsResult->displays.length == 0) { yyjson_mut_obj_add_str(doc, module, "error", "Couldn't detect display"); return; } yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result"); FF_LIST_FOR_EACH(FFDisplayResult, item, dsResult->displays) { yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr); yyjson_mut_obj_add_uint(doc, obj, "id", item->id); yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name); yyjson_mut_obj_add_bool(doc, obj, "primary", item->primary); yyjson_mut_val* output = yyjson_mut_obj_add_obj(doc, obj, "output"); yyjson_mut_obj_add_uint(doc, output, "width", item->width); yyjson_mut_obj_add_uint(doc, output, "height", item->height); yyjson_mut_obj_add_real(doc, output, "refreshRate", item->refreshRate); yyjson_mut_val* scaled = yyjson_mut_obj_add_obj(doc, obj, "scaled"); yyjson_mut_obj_add_uint(doc, scaled, "width", item->scaledWidth); yyjson_mut_obj_add_uint(doc, scaled, "height", item->scaledHeight); yyjson_mut_val* preferred = yyjson_mut_obj_add_obj(doc, obj, "preferred"); yyjson_mut_obj_add_uint(doc, preferred, "width", item->preferredWidth); yyjson_mut_obj_add_uint(doc, preferred, "height", item->preferredHeight); yyjson_mut_obj_add_real(doc, preferred, "refreshRate", item->preferredRefreshRate); yyjson_mut_val* physical = yyjson_mut_obj_add_obj(doc, obj, "physical"); yyjson_mut_obj_add_uint(doc, physical, "width", item->physicalWidth); yyjson_mut_obj_add_uint(doc, physical, "height", item->physicalHeight); yyjson_mut_obj_add_uint(doc, obj, "rotation", item->rotation); yyjson_mut_obj_add_uint(doc, obj, "bitDepth", item->bitDepth); if (item->hdrStatus == FF_DISPLAY_HDR_STATUS_UNKNOWN) yyjson_mut_obj_add_null(doc, obj, "hdrStatus"); else switch (item->hdrStatus) { case FF_DISPLAY_HDR_STATUS_UNSUPPORTED: yyjson_mut_obj_add_str(doc, obj, "hdrStatus", "Unsupported"); break; case FF_DISPLAY_HDR_STATUS_SUPPORTED: yyjson_mut_obj_add_str(doc, obj, "hdrStatus", "Supported"); break; case FF_DISPLAY_HDR_STATUS_ENABLED: yyjson_mut_obj_add_str(doc, obj, "hdrStatus", "Enabled"); break; default: yyjson_mut_obj_add_str(doc, obj, "hdrStatus", "Unknown"); break; } switch (item->type) { case FF_DISPLAY_TYPE_BUILTIN: yyjson_mut_obj_add_str(doc, obj, "type", "Builtin"); break; case FF_DISPLAY_TYPE_EXTERNAL: yyjson_mut_obj_add_str(doc, obj, "type", "External"); break; default: yyjson_mut_obj_add_str(doc, obj, "type", "Unknown"); break; } if (item->manufactureYear) { yyjson_mut_val* manufactureDate = yyjson_mut_obj_add_obj(doc, obj, "manufactureDate"); yyjson_mut_obj_add_uint(doc, manufactureDate, "year", item->manufactureYear); yyjson_mut_obj_add_uint(doc, manufactureDate, "week", item->manufactureWeek); } else { yyjson_mut_obj_add_null(doc, obj, "manufactureDate"); } if (item->serial) yyjson_mut_obj_add_uint(doc, obj, "serial", item->serial); else yyjson_mut_obj_add_null(doc, obj, "serial"); yyjson_mut_obj_add_str(doc, obj, "platformApi", item->platformApi); } } static FFModuleBaseInfo ffModuleInfo = { .name = FF_DISPLAY_MODULE_NAME, .description = "Print resolutions, refresh rates, etc", .parseCommandOptions = (void*) ffParseDisplayCommandOptions, .parseJsonObject = (void*) ffParseDisplayJsonObject, .printModule = (void*) ffPrintDisplay, .generateJsonResult = (void*) ffGenerateDisplayJsonResult, .generateJsonConfig = (void*) ffGenerateDisplayJsonConfig, .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { {"Screen configured width (in pixels)", "width"}, {"Screen configured height (in pixels)", "height"}, {"Screen configured refresh rate (in Hz)", "refresh-rate"}, {"Screen scaled width (in pixels)", "scaled-width"}, {"Screen scaled height (in pixels)", "scaled-height"}, {"Screen name", "name"}, {"Screen type (Built-in or External)", "type"}, {"Screen rotation (in degrees)", "rotation"}, {"True if being the primary screen", "is-primary"}, {"Screen physical width (in millimeters)", "physical-width"}, {"Screen physical height (in millimeters)", "physical-height"}, {"Physical diagonal length in inches", "inch"}, {"Pixels per inch (PPI)", "ppi"}, {"Bits per color channel", "bit-depth"}, {"True if high dynamic range (HDR) mode is enabled", "hdr-enabled"}, {"Year of manufacturing", "manufacture-year"}, {"Nth week of manufacturing in the year", "manufacture-week"}, {"Serial number", "serial"}, {"The platform API used when detecting the display", "platform-api"}, {"True if the display is HDR compatible", "hdr-compatible"}, {"HiDPI scale factor", "scale-factor"}, {"Screen preferred width (in pixels)", "preferred-width"}, {"Screen preferred height (in pixels)", "preferred-height"}, {"Screen preferred refresh rate (in Hz)", "preferred-refresh-rate"}, })) }; void ffInitDisplayOptions(FFDisplayOptions* options) { options->moduleInfo = ffModuleInfo; ffOptionInitModuleArg(&options->moduleArgs, "󰍹"); options->compactType = FF_DISPLAY_COMPACT_TYPE_NONE; options->preciseRefreshRate = false; } void ffDestroyDisplayOptions(FFDisplayOptions* options) { ffOptionDestroyModuleArg(&options->moduleArgs); }