summaryrefslogtreecommitdiffstats
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/battery/battery.c326
-rw-r--r--src/modules/battery/battery.h11
-rw-r--r--src/modules/battery/option.h14
-rw-r--r--src/modules/bios/bios.c148
-rw-r--r--src/modules/bios/bios.h11
-rw-r--r--src/modules/bios/option.h9
-rw-r--r--src/modules/bluetooth/bluetooth.c170
-rw-r--r--src/modules/bluetooth/bluetooth.h11
-rw-r--r--src/modules/bluetooth/option.h13
-rw-r--r--src/modules/bluetoothradio/bluetoothradio.c210
-rw-r--r--src/modules/bluetoothradio/bluetoothradio.h11
-rw-r--r--src/modules/bluetoothradio/option.h10
-rw-r--r--src/modules/board/board.c125
-rw-r--r--src/modules/board/board.h11
-rw-r--r--src/modules/board/option.h9
-rw-r--r--src/modules/bootmgr/bootmgr.c121
-rw-r--r--src/modules/bootmgr/bootmgr.h11
-rw-r--r--src/modules/bootmgr/option.h9
-rw-r--r--src/modules/break/break.c36
-rw-r--r--src/modules/break/break.h11
-rw-r--r--src/modules/break/option.h8
-rw-r--r--src/modules/brightness/brightness.c210
-rw-r--r--src/modules/brightness/brightness.h11
-rw-r--r--src/modules/brightness/option.h14
-rw-r--r--src/modules/btrfs/btrfs.c224
-rw-r--r--src/modules/btrfs/btrfs.h11
-rw-r--r--src/modules/btrfs/option.h12
-rw-r--r--src/modules/camera/camera.c134
-rw-r--r--src/modules/camera/camera.h11
-rw-r--r--src/modules/camera/option.h9
-rw-r--r--src/modules/chassis/chassis.c127
-rw-r--r--src/modules/chassis/chassis.h11
-rw-r--r--src/modules/chassis/option.h9
-rw-r--r--src/modules/codec/codec.c268
-rw-r--r--src/modules/codec/codec.h11
-rw-r--r--src/modules/codec/option.h20
-rw-r--r--src/modules/colors/colors.c305
-rw-r--r--src/modules/colors/colors.h11
-rw-r--r--src/modules/colors/option.h35
-rw-r--r--src/modules/command/command.c167
-rw-r--r--src/modules/command/command.h13
-rw-r--r--src/modules/command/option.h16
-rw-r--r--src/modules/cpu/cpu.c262
-rw-r--r--src/modules/cpu/cpu.h11
-rw-r--r--src/modules/cpu/option.h14
-rw-r--r--src/modules/cpucache/cpucache.c238
-rw-r--r--src/modules/cpucache/cpucache.h11
-rw-r--r--src/modules/cpucache/option.h11
-rw-r--r--src/modules/cpuusage/cpuusage.c202
-rw-r--r--src/modules/cpuusage/cpuusage.h13
-rw-r--r--src/modules/cpuusage/option.h14
-rw-r--r--src/modules/cursor/cursor.c115
-rw-r--r--src/modules/cursor/cursor.h11
-rw-r--r--src/modules/cursor/option.h9
-rw-r--r--src/modules/custom/custom.c45
-rw-r--r--src/modules/custom/custom.h11
-rw-r--r--src/modules/custom/option.h9
-rw-r--r--src/modules/datetime/datetime.c189
-rw-r--r--src/modules/datetime/datetime.h11
-rw-r--r--src/modules/datetime/option.h9
-rw-r--r--src/modules/de/de.c98
-rw-r--r--src/modules/de/de.h11
-rw-r--r--src/modules/de/option.h9
-rw-r--r--src/modules/disk/disk.c490
-rw-r--r--src/modules/disk/disk.h11
-rw-r--r--src/modules/disk/option.h33
-rw-r--r--src/modules/diskio/diskio.c193
-rw-r--r--src/modules/diskio/diskio.h13
-rw-r--r--src/modules/diskio/option.h13
-rw-r--r--src/modules/display/display.c457
-rw-r--r--src/modules/display/display.h11
-rw-r--r--src/modules/display/option.h27
-rw-r--r--src/modules/dns/dns.c148
-rw-r--r--src/modules/dns/dns.h11
-rw-r--r--src/modules/dns/option.h18
-rw-r--r--src/modules/editor/editor.c121
-rw-r--r--src/modules/editor/editor.h11
-rw-r--r--src/modules/editor/option.h9
-rw-r--r--src/modules/font/font.c113
-rw-r--r--src/modules/font/font.h11
-rw-r--r--src/modules/font/option.h9
-rw-r--r--src/modules/gamepad/gamepad.c206
-rw-r--r--src/modules/gamepad/gamepad.h11
-rw-r--r--src/modules/gamepad/option.h13
-rw-r--r--src/modules/gpu/gpu.c488
-rw-r--r--src/modules/gpu/gpu.h11
-rw-r--r--src/modules/gpu/option.h32
-rw-r--r--src/modules/host/host.c156
-rw-r--r--src/modules/host/host.h11
-rw-r--r--src/modules/host/option.h9
-rw-r--r--src/modules/icons/icons.c109
-rw-r--r--src/modules/icons/icons.h11
-rw-r--r--src/modules/icons/option.h9
-rw-r--r--src/modules/initsystem/initsystem.c120
-rw-r--r--src/modules/initsystem/initsystem.h11
-rw-r--r--src/modules/initsystem/option.h9
-rw-r--r--src/modules/kernel/kernel.c81
-rw-r--r--src/modules/kernel/kernel.h11
-rw-r--r--src/modules/kernel/option.h9
-rw-r--r--src/modules/keyboard/keyboard.c164
-rw-r--r--src/modules/keyboard/keyboard.h11
-rw-r--r--src/modules/keyboard/option.h12
-rw-r--r--src/modules/lm/lm.c122
-rw-r--r--src/modules/lm/lm.h11
-rw-r--r--src/modules/lm/option.h9
-rw-r--r--src/modules/loadavg/loadavg.c164
-rw-r--r--src/modules/loadavg/loadavg.h11
-rw-r--r--src/modules/loadavg/option.h13
-rw-r--r--src/modules/locale/locale.c81
-rw-r--r--src/modules/locale/locale.h11
-rw-r--r--src/modules/locale/option.h9
-rw-r--r--src/modules/localip/localip.c443
-rw-r--r--src/modules/localip/localip.h11
-rw-r--r--src/modules/localip/option.h44
-rw-r--r--src/modules/logo/logo.c87
-rw-r--r--src/modules/logo/logo.h11
-rw-r--r--src/modules/logo/option.h8
-rw-r--r--src/modules/media/media.c277
-rw-r--r--src/modules/media/media.h11
-rw-r--r--src/modules/media/option.h11
-rw-r--r--src/modules/memory/memory.c135
-rw-r--r--src/modules/memory/memory.h11
-rw-r--r--src/modules/memory/option.h12
-rw-r--r--src/modules/modules.c364
-rw-r--r--src/modules/modules.h77
-rw-r--r--src/modules/monitor/monitor.c137
-rw-r--r--src/modules/monitor/monitor.h11
-rw-r--r--src/modules/monitor/option.h9
-rw-r--r--src/modules/mouse/mouse.c167
-rw-r--r--src/modules/mouse/mouse.h11
-rw-r--r--src/modules/mouse/option.h12
-rw-r--r--src/modules/netio/netio.c224
-rw-r--r--src/modules/netio/netio.h13
-rw-r--r--src/modules/netio/option.h14
-rw-r--r--src/modules/opencl/opencl.c136
-rw-r--r--src/modules/opencl/opencl.h11
-rw-r--r--src/modules/opencl/option.h9
-rw-r--r--src/modules/opengl/opengl.c145
-rw-r--r--src/modules/opengl/opengl.h11
-rw-r--r--src/modules/opengl/option.h17
-rw-r--r--src/modules/options.h77
-rw-r--r--src/modules/os/option.h9
-rw-r--r--src/modules/os/os.c197
-rw-r--r--src/modules/os/os.h11
-rw-r--r--src/modules/packages/option.h55
-rw-r--r--src/modules/packages/packages.c560
-rw-r--r--src/modules/packages/packages.h11
-rw-r--r--src/modules/physicaldisk/option.h34
-rw-r--r--src/modules/physicaldisk/physicaldisk.c288
-rw-r--r--src/modules/physicaldisk/physicaldisk.h11
-rw-r--r--src/modules/physicalmemory/option.h10
-rw-r--r--src/modules/physicalmemory/physicalmemory.c184
-rw-r--r--src/modules/physicalmemory/physicalmemory.h11
-rw-r--r--src/modules/player/option.h9
-rw-r--r--src/modules/player/player.c126
-rw-r--r--src/modules/player/player.h12
-rw-r--r--src/modules/poweradapter/option.h9
-rw-r--r--src/modules/poweradapter/poweradapter.c129
-rw-r--r--src/modules/poweradapter/poweradapter.h11
-rw-r--r--src/modules/processes/option.h9
-rw-r--r--src/modules/processes/processes.c76
-rw-r--r--src/modules/processes/processes.h11
-rw-r--r--src/modules/publicip/option.h13
-rw-r--r--src/modules/publicip/publicip.c125
-rw-r--r--src/modules/publicip/publicip.h13
-rw-r--r--src/modules/separator/option.h11
-rw-r--r--src/modules/separator/separator.c130
-rw-r--r--src/modules/separator/separator.h11
-rw-r--r--src/modules/shell/option.h9
-rw-r--r--src/modules/shell/shell.c110
-rw-r--r--src/modules/shell/shell.h11
-rw-r--r--src/modules/sound/option.h21
-rw-r--r--src/modules/sound/sound.c219
-rw-r--r--src/modules/sound/sound.h11
-rw-r--r--src/modules/swap/option.h13
-rw-r--r--src/modules/swap/swap.c200
-rw-r--r--src/modules/swap/swap.h11
-rw-r--r--src/modules/terminal/option.h9
-rw-r--r--src/modules/terminal/terminal.c104
-rw-r--r--src/modules/terminal/terminal.h11
-rw-r--r--src/modules/terminalfont/option.h9
-rw-r--r--src/modules/terminalfont/terminalfont.c122
-rw-r--r--src/modules/terminalfont/terminalfont.h11
-rw-r--r--src/modules/terminalsize/option.h9
-rw-r--r--src/modules/terminalsize/terminalsize.c93
-rw-r--r--src/modules/terminalsize/terminalsize.h11
-rw-r--r--src/modules/terminaltheme/option.h9
-rw-r--r--src/modules/terminaltheme/terminaltheme.c110
-rw-r--r--src/modules/terminaltheme/terminaltheme.h11
-rw-r--r--src/modules/theme/option.h9
-rw-r--r--src/modules/theme/theme.c102
-rw-r--r--src/modules/theme/theme.h11
-rw-r--r--src/modules/title/option.h14
-rw-r--r--src/modules/title/title.c201
-rw-r--r--src/modules/title/title.h11
-rw-r--r--src/modules/tpm/option.h9
-rw-r--r--src/modules/tpm/tpm.c98
-rw-r--r--src/modules/tpm/tpm.h11
-rw-r--r--src/modules/uptime/option.h9
-rw-r--r--src/modules/uptime/uptime.c106
-rw-r--r--src/modules/uptime/uptime.h11
-rw-r--r--src/modules/users/option.h12
-rw-r--r--src/modules/users/users.c206
-rw-r--r--src/modules/users/users.h11
-rw-r--r--src/modules/version/option.h9
-rw-r--r--src/modules/version/version.c117
-rw-r--r--src/modules/version/version.h11
-rw-r--r--src/modules/vulkan/option.h9
-rw-r--r--src/modules/vulkan/vulkan.c149
-rw-r--r--src/modules/vulkan/vulkan.h11
-rw-r--r--src/modules/wallpaper/option.h9
-rw-r--r--src/modules/wallpaper/wallpaper.c90
-rw-r--r--src/modules/wallpaper/wallpaper.h11
-rw-r--r--src/modules/weather/option.h13
-rw-r--r--src/modules/weather/weather.c105
-rw-r--r--src/modules/weather/weather.h13
-rw-r--r--src/modules/wifi/option.h11
-rw-r--r--src/modules/wifi/wifi.c239
-rw-r--r--src/modules/wifi/wifi.h11
-rw-r--r--src/modules/wm/option.h11
-rw-r--r--src/modules/wm/wm.c138
-rw-r--r--src/modules/wm/wm.h11
-rw-r--r--src/modules/wmtheme/option.h9
-rw-r--r--src/modules/wmtheme/wmtheme.c75
-rw-r--r--src/modules/wmtheme/wmtheme.h11
-rw-r--r--src/modules/zpool/option.h12
-rw-r--r--src/modules/zpool/zpool.c218
-rw-r--r--src/modules/zpool/zpool.h11
228 files changed, 15662 insertions, 0 deletions
diff --git a/src/modules/battery/battery.c b/src/modules/battery/battery.c
new file mode 100644
index 0000000..61cef84
--- /dev/null
+++ b/src/modules/battery/battery.c
@@ -0,0 +1,326 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/duration.h"
+#include "common/temps.h"
+#include "common/strutil.h"
+#include "detection/battery/battery.h"
+#include "modules/battery/battery.h"
+
+static void printBattery(FFBatteryOptions* options, FFBatteryResult* result, uint8_t index) {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ if (options->moduleArgs.key.length == 0) {
+ if (result->modelName.length > 0) {
+ ffStrbufSetF(&key, "%s (%s)", FF_BATTERY_MODULE_NAME, result->modelName.chars);
+ } else {
+ ffStrbufSetS(&key, FF_BATTERY_MODULE_NAME);
+ }
+ } else {
+ ffStrbufClear(&key);
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(result->modelName, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+ bool showStatus = !(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT) && result->status;
+
+ if (result->capacity >= 0) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, result->capacity, options->percent, &options->moduleArgs);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ if (str.length > 0) {
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ ffPercentAppendNum(&str, result->capacity, options->percent, str.length > 0, &options->moduleArgs);
+ }
+
+ if (result->timeRemaining > 0) {
+ if (str.length > 0) {
+ ffStrbufAppendS(&str, " (");
+ }
+
+ ffDurationAppendNum((uint32_t) result->timeRemaining, &str);
+ ffStrbufAppendS(&str, " remaining)");
+ }
+ }
+
+ if (showStatus) {
+ bool hasOtherInfo = str.length > 0;
+ if (hasOtherInfo) {
+ ffStrbufAppendS(&str, " [");
+ }
+ if (result->status & FF_BATTERY_STATUS_AC_CONNECTED) {
+ ffStrbufAppendS(&str, "AC Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_USB_CONNECTED) {
+ ffStrbufAppendS(&str, "USB Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_WIRELESS_CONNECTED) {
+ ffStrbufAppendS(&str, "Wireless Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_CHARGING) {
+ ffStrbufAppendS(&str, "Charging, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_DISCHARGING) {
+ ffStrbufAppendS(&str, "Discharging, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_CRITICAL) {
+ ffStrbufAppendS(&str, "Critical, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_UNKNOWN) {
+ ffStrbufAppendS(&str, "Unknown, ");
+ }
+ ffStrbufSubstrBefore(&str, str.length - 2); // Remove last ", "
+ if (hasOtherInfo) {
+ ffStrbufAppendC(&str, ']');
+ }
+ }
+
+ if (result->temperature != FF_BATTERY_TEMP_UNSET) {
+ if (str.length > 0) {
+ ffStrbufAppendS(&str, " - ");
+ }
+
+ ffTempsAppendNum(result->temperature, &str, options->tempConfig, &options->moduleArgs);
+ }
+
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ uint32_t timeRemaining = result->timeRemaining < 0 ? 0 : (uint32_t) result->timeRemaining;
+ uint32_t seconds = timeRemaining % 60;
+ timeRemaining /= 60;
+ uint32_t minutes = timeRemaining % 60;
+ timeRemaining /= 60;
+ uint32_t hours = timeRemaining % 24;
+ timeRemaining /= 24;
+ uint32_t days = timeRemaining;
+
+ FF_STRBUF_AUTO_DESTROY capacityNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&capacityNum, result->capacity, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY capacityBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&capacityBar, result->capacity, options->percent, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY tempStr = ffStrbufCreate();
+ ffTempsAppendNum(result->temperature, &tempStr, options->tempConfig, &options->moduleArgs);
+ FF_STRBUF_AUTO_DESTROY timeStr = ffStrbufCreate();
+ if (result->timeRemaining > 0) {
+ ffDurationAppendNum((uint32_t) result->timeRemaining, &timeStr);
+ }
+
+ FF_STRBUF_AUTO_DESTROY statusStr = ffStrbufCreate();
+ if (result->status & FF_BATTERY_STATUS_AC_CONNECTED) {
+ ffStrbufAppendS(&statusStr, "AC Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_USB_CONNECTED) {
+ ffStrbufAppendS(&statusStr, "USB Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_WIRELESS_CONNECTED) {
+ ffStrbufAppendS(&statusStr, "Wireless Connected, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_CHARGING) {
+ ffStrbufAppendS(&statusStr, "Charging, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_DISCHARGING) {
+ ffStrbufAppendS(&statusStr, "Discharging, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_CRITICAL) {
+ ffStrbufAppendS(&statusStr, "Critical, ");
+ }
+ if (result->status & FF_BATTERY_STATUS_UNKNOWN) {
+ ffStrbufAppendS(&statusStr, "Unknown, ");
+ }
+ ffStrbufSubstrBefore(&statusStr, statusStr.length - 2); // Remove last ", "
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(result->manufacturer, "manufacturer"),
+ FF_ARG(result->modelName, "model-name"),
+ FF_ARG(result->technology, "technology"),
+ FF_ARG(capacityNum, "capacity"),
+ FF_ARG(statusStr, "status"),
+ FF_ARG(tempStr, "temperature"),
+ FF_ARG(result->cycleCount, "cycle-count"),
+ FF_ARG(result->serial, "serial"),
+ FF_ARG(result->manufactureDate, "manufacture-date"),
+ FF_ARG(capacityBar, "capacity-bar"),
+ FF_ARG(days, "time-days"),
+ FF_ARG(hours, "time-hours"),
+ FF_ARG(minutes, "time-minutes"),
+ FF_ARG(seconds, "time-seconds"),
+ FF_ARG(timeStr, "time-formatted"),
+ }));
+ }
+}
+
+bool ffPrintBattery(FFBatteryOptions* options) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBattery(options, &results);
+
+ if (error) {
+ ffPrintError(FF_BATTERY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+ if (results.length == 0) {
+ ffPrintError(FF_BATTERY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", "No batteries found");
+ return false;
+ }
+
+ for (uint32_t i = 0; i < results.length; i++) {
+ FFBatteryResult* result = FF_LIST_GET(FFBatteryResult, results, i);
+ printBattery(options, result, results.length == 1 ? 0 : (uint8_t) (i + 1));
+ }
+
+ FF_LIST_FOR_EACH (FFBatteryResult, result, results) {
+ ffStrbufDestroy(&result->manufacturer);
+ ffStrbufDestroy(&result->modelName);
+ ffStrbufDestroy(&result->technology);
+ ffStrbufDestroy(&result->serial);
+ ffStrbufDestroy(&result->manufactureDate);
+ }
+ return true;
+}
+
+void ffParseBatteryJsonObject(FFBatteryOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffTempsParseJsonObject(key, val, &options->temp, &options->tempConfig)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_BATTERY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBatteryJsonConfig(FFBatteryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffTempsGenerateJsonConfig(doc, module, options->temp, options->tempConfig);
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateBatteryJsonResult(FFBatteryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBattery(options, &results);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFBatteryResult, battery, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_real(doc, obj, "capacity", battery->capacity);
+ yyjson_mut_obj_add_strbuf(doc, obj, "manufacturer", &battery->manufacturer);
+ yyjson_mut_obj_add_strbuf(doc, obj, "manufactureDate", &battery->manufactureDate);
+ yyjson_mut_obj_add_strbuf(doc, obj, "modelName", &battery->modelName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "technology", &battery->technology);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &battery->serial);
+ if (battery->temperature != FF_BATTERY_TEMP_UNSET) {
+ yyjson_mut_obj_add_real(doc, obj, "temperature", battery->temperature);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "temperature");
+ }
+ yyjson_mut_obj_add_uint(doc, obj, "cycleCount", battery->cycleCount);
+ if (battery->timeRemaining > 0) {
+ yyjson_mut_obj_add_int(doc, obj, "timeRemaining", battery->timeRemaining);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "timeRemaining");
+ }
+ yyjson_mut_val* status = yyjson_mut_obj_add_arr(doc, obj, "status");
+ if (battery->status & FF_BATTERY_STATUS_AC_CONNECTED) {
+ yyjson_mut_arr_add_str(doc, status, "AC Connected");
+ }
+ if (battery->status & FF_BATTERY_STATUS_USB_CONNECTED) {
+ yyjson_mut_arr_add_str(doc, status, "USB Connected");
+ }
+ if (battery->status & FF_BATTERY_STATUS_WIRELESS_CONNECTED) {
+ yyjson_mut_arr_add_str(doc, status, "Wireless Connected");
+ }
+ if (battery->status & FF_BATTERY_STATUS_CHARGING) {
+ yyjson_mut_arr_add_str(doc, status, "Charging");
+ }
+ if (battery->status & FF_BATTERY_STATUS_DISCHARGING) {
+ yyjson_mut_arr_add_str(doc, status, "Discharging");
+ }
+ if (battery->status & FF_BATTERY_STATUS_CRITICAL) {
+ yyjson_mut_arr_add_str(doc, status, "Critical");
+ }
+ if (battery->status & FF_BATTERY_STATUS_UNKNOWN) {
+ yyjson_mut_arr_add_str(doc, status, "Unknown");
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFBatteryResult, battery, results) {
+ ffStrbufDestroy(&battery->manufacturer);
+ ffStrbufDestroy(&battery->manufactureDate);
+ ffStrbufDestroy(&battery->modelName);
+ ffStrbufDestroy(&battery->technology);
+ ffStrbufDestroy(&battery->serial);
+ }
+
+ return true;
+}
+
+void ffInitBatteryOptions(FFBatteryOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ options->temp = false;
+ options->tempConfig = (FFColorRangeConfig) { 60, 80 };
+ options->percent = (FFPercentageModuleConfig) { 50, 20, 0 };
+}
+
+void ffDestroyBatteryOptions(FFBatteryOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBatteryModuleInfo = {
+ .name = FF_BATTERY_MODULE_NAME,
+ .description = "Print battery information",
+ .initOptions = (void*) ffInitBatteryOptions,
+ .destroyOptions = (void*) ffDestroyBatteryOptions,
+ .parseJsonObject = (void*) ffParseBatteryJsonObject,
+ .printModule = (void*) ffPrintBattery,
+ .generateJsonResult = (void*) ffGenerateBatteryJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBatteryJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Battery manufacturer", "manufacturer" },
+ { "Battery model name", "model-name" },
+ { "Battery technology", "technology" },
+ { "Battery capacity (percentage num)", "capacity" },
+ { "Battery status", "status" },
+ { "Battery temperature (formatted)", "temperature" },
+ { "Battery cycle count", "cycle-count" },
+ { "Battery serial number", "serial" },
+ { "Battery manufacture date", "manufacture-date" },
+ { "Battery capacity (percentage bar)", "capacity-bar" },
+ { "Battery time remaining days", "time-days" },
+ { "Battery time remaining hours", "time-hours" },
+ { "Battery time remaining minutes", "time-minutes" },
+ { "Battery time remaining seconds", "time-seconds" },
+ { "Battery time remaining (formatted)", "time-formatted" },
+ }))
+};
diff --git a/src/modules/battery/battery.h b/src/modules/battery/battery.h
new file mode 100644
index 0000000..16ad027
--- /dev/null
+++ b/src/modules/battery/battery.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BATTERY_MODULE_NAME "Battery"
+
+bool ffPrintBattery(FFBatteryOptions* options);
+void ffInitBatteryOptions(FFBatteryOptions* options);
+void ffDestroyBatteryOptions(FFBatteryOptions* options);
+
+extern FFModuleBaseInfo ffBatteryModuleInfo;
diff --git a/src/modules/battery/option.h b/src/modules/battery/option.h
new file mode 100644
index 0000000..d65fd1f
--- /dev/null
+++ b/src/modules/battery/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFBatteryOptions {
+ FFModuleArgs moduleArgs;
+
+ bool temp;
+ FFColorRangeConfig tempConfig;
+ FFPercentageModuleConfig percent;
+} FFBatteryOptions;
+
+static_assert(sizeof(FFBatteryOptions) <= FF_OPTION_MAX_SIZE, "FFBatteryOptions size exceeds maximum allowed size");
diff --git a/src/modules/bios/bios.c b/src/modules/bios/bios.c
new file mode 100644
index 0000000..8f8700f
--- /dev/null
+++ b/src/modules/bios/bios.c
@@ -0,0 +1,148 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/bios/bios.h"
+#include "modules/bios/bios.h"
+
+bool ffPrintBios(FFBiosOptions* options) {
+ bool success = false;
+ FFBiosResult bios;
+ ffStrbufInit(&bios.date);
+ ffStrbufInit(&bios.release);
+ ffStrbufInit(&bios.vendor);
+ ffStrbufInit(&bios.version);
+ ffStrbufInit(&bios.type);
+
+ const char* error = ffDetectBios(&bios);
+
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ if (error) {
+ ffPrintError(FF_BIOS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (bios.version.length == 0) {
+ ffPrintError(FF_BIOS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "bios_version is not set.");
+ goto exit;
+ }
+
+ if (options->moduleArgs.key.length == 0) {
+ if (bios.type.length == 0) {
+ ffStrbufSetStatic(&bios.type, "Unknown");
+ } else if (ffStrbufIgnCaseEqualS(&bios.type, "BIOS")) {
+ ffStrbufSetStatic(&bios.type, "Legacy");
+ }
+
+ ffStrbufSetF(&key, FF_BIOS_MODULE_NAME " (%s)", bios.type.chars);
+ } else {
+ ffStrbufClear(&key);
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(bios.type, "type"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ ffStrbufWriteTo(&bios.version, stdout);
+ if (bios.release.length) {
+ printf(" (%s)\n", bios.release.chars);
+ } else {
+ putchar('\n');
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(bios.date, "date"),
+ FF_ARG(bios.release, "release"),
+ FF_ARG(bios.vendor, "vendor"),
+ FF_ARG(bios.version, "version"),
+ FF_ARG(bios.type, "type"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&bios.date);
+ ffStrbufDestroy(&bios.release);
+ ffStrbufDestroy(&bios.vendor);
+ ffStrbufDestroy(&bios.version);
+ ffStrbufDestroy(&bios.type);
+
+ return success;
+}
+
+void ffParseBiosJsonObject(FFBiosOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_BIOS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBiosJsonConfig(FFBiosOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateBiosJsonResult(FF_A_UNUSED FFBiosOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFBiosResult bios;
+ ffStrbufInit(&bios.date);
+ ffStrbufInit(&bios.release);
+ ffStrbufInit(&bios.vendor);
+ ffStrbufInit(&bios.version);
+ ffStrbufInit(&bios.type);
+
+ const char* error = ffDetectBios(&bios);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "date", &bios.date);
+ yyjson_mut_obj_add_strbuf(doc, obj, "release", &bios.release);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &bios.vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &bios.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "type", &bios.type);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&bios.date);
+ ffStrbufDestroy(&bios.release);
+ ffStrbufDestroy(&bios.vendor);
+ ffStrbufDestroy(&bios.version);
+ ffStrbufDestroy(&bios.type);
+ return success;
+}
+
+void ffInitBiosOptions(FFBiosOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyBiosOptions(FFBiosOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBiosModuleInfo = {
+ .name = FF_BIOS_MODULE_NAME,
+ .description = "Print first-stage bootloader information (name, version, release date, etc.)",
+ .initOptions = (void*) ffInitBiosOptions,
+ .destroyOptions = (void*) ffDestroyBiosOptions,
+ .parseJsonObject = (void*) ffParseBiosJsonObject,
+ .printModule = (void*) ffPrintBios,
+ .generateJsonResult = (void*) ffGenerateBiosJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBiosJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "BIOS date", "date" },
+ { "BIOS release", "release" },
+ { "BIOS vendor", "vendor" },
+ { "BIOS version", "version" },
+ { "Firmware type", "type" },
+ }))
+};
diff --git a/src/modules/bios/bios.h b/src/modules/bios/bios.h
new file mode 100644
index 0000000..d3b077e
--- /dev/null
+++ b/src/modules/bios/bios.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BIOS_MODULE_NAME "BIOS"
+
+bool ffPrintBios(FFBiosOptions* options);
+void ffInitBiosOptions(FFBiosOptions* options);
+void ffDestroyBiosOptions(FFBiosOptions* options);
+
+extern FFModuleBaseInfo ffBiosModuleInfo;
diff --git a/src/modules/bios/option.h b/src/modules/bios/option.h
new file mode 100644
index 0000000..aae4c8c
--- /dev/null
+++ b/src/modules/bios/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFBiosOptions {
+ FFModuleArgs moduleArgs;
+} FFBiosOptions;
+
+static_assert(sizeof(FFBiosOptions) <= FF_OPTION_MAX_SIZE, "FFBiosOptions size exceeds maximum allowed size");
diff --git a/src/modules/bluetooth/bluetooth.c b/src/modules/bluetooth/bluetooth.c
new file mode 100644
index 0000000..6721c33
--- /dev/null
+++ b/src/modules/bluetooth/bluetooth.c
@@ -0,0 +1,170 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/bluetooth/bluetooth.h"
+#include "modules/bluetooth/bluetooth.h"
+
+static void printDevice(FFBluetoothOptions* options, const FFBluetoothResult* device, uint8_t index) {
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_BLUETOOTH_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ bool showBatteryLevel = device->battery > 0 && device->battery <= 100;
+
+ if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_BAR_BIT)) {
+ ffPercentAppendBar(&buffer, device->battery, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&buffer, ' ');
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppend(&buffer, &device->name);
+ }
+
+ if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_NUM_BIT)) {
+ if (buffer.length) {
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ ffPercentAppendNum(&buffer, device->battery, options->percent, buffer.length > 0, &options->moduleArgs);
+ }
+
+ if (!device->connected) {
+ ffStrbufAppendS(&buffer, " [disconnected]");
+ }
+
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentageNum, device->battery, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentageBar, device->battery, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_BLUETOOTH_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(device->name, "name"),
+ FF_ARG(device->address, "address"),
+ FF_ARG(device->type, "type"),
+ FF_ARG(percentageNum, "battery-percentage"),
+ FF_ARG(device->connected, "connected"),
+ FF_ARG(percentageBar, "battery-percentage-bar"),
+ }));
+ }
+}
+
+bool ffPrintBluetooth(FFBluetoothOptions* options) {
+ FF_LIST_AUTO_DESTROY devices = ffListCreate();
+ const char* error = ffDetectBluetooth(options, &devices);
+
+ if (error) {
+ ffPrintError(FF_BLUETOOTH_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (devices.length == 0) {
+ ffPrintError(FF_BLUETOOTH_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No bluetooth devices found");
+ return false;
+ }
+
+ uint8_t i = 1;
+ FF_LIST_FOR_EACH (FFBluetoothResult, device, devices) {
+ printDevice(options, device, devices.length == 0 ? 0 : i);
+ ++i;
+ }
+
+ FF_LIST_FOR_EACH (FFBluetoothResult, device, devices) {
+ ffStrbufDestroy(&device->name);
+ ffStrbufDestroy(&device->type);
+ ffStrbufDestroy(&device->address);
+ }
+ return true;
+}
+
+void ffParseBluetoothJsonObject(FFBluetoothOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showDisconnected")) {
+ options->showDisconnected = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_BLUETOOTH_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBluetoothJsonConfig(FFBluetoothOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "showDisconnected", options->showDisconnected);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateBluetoothJsonResult(FFBluetoothOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBluetooth(options, &results);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFBluetoothResult, item, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "address", &item->address);
+ yyjson_mut_obj_add_uint(doc, obj, "battery", item->battery);
+ yyjson_mut_obj_add_bool(doc, obj, "connected", item->connected);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "type", &item->type);
+ }
+
+ FF_LIST_FOR_EACH (FFBluetoothResult, device, results) {
+ ffStrbufDestroy(&device->name);
+ ffStrbufDestroy(&device->type);
+ ffStrbufDestroy(&device->address);
+ }
+ return true;
+}
+
+void ffInitBluetoothOptions(FFBluetoothOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ options->showDisconnected = false;
+ options->percent = (FFPercentageModuleConfig) { 50, 20, 0 };
+}
+
+void ffDestroyBluetoothOptions(FFBluetoothOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBluetoothModuleInfo = {
+ .name = FF_BLUETOOTH_MODULE_NAME,
+ .description = "List connected Bluetooth devices",
+ .initOptions = (void*) ffInitBluetoothOptions,
+ .destroyOptions = (void*) ffDestroyBluetoothOptions,
+ .parseJsonObject = (void*) ffParseBluetoothJsonObject,
+ .printModule = (void*) ffPrintBluetooth,
+ .generateJsonResult = (void*) ffGenerateBluetoothJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBluetoothJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name", "name" },
+ { "Address", "address" },
+ { "Type", "type" },
+ { "Battery percentage number", "battery-percentage" },
+ { "Is connected", "connected" },
+ { "Battery percentage bar", "battery-percentage-bar" },
+ }))
+};
diff --git a/src/modules/bluetooth/bluetooth.h b/src/modules/bluetooth/bluetooth.h
new file mode 100644
index 0000000..d9aa059
--- /dev/null
+++ b/src/modules/bluetooth/bluetooth.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BLUETOOTH_MODULE_NAME "Bluetooth"
+
+bool ffPrintBluetooth(FFBluetoothOptions* options);
+void ffInitBluetoothOptions(FFBluetoothOptions* options);
+void ffDestroyBluetoothOptions(FFBluetoothOptions* options);
+
+extern FFModuleBaseInfo ffBluetoothModuleInfo;
diff --git a/src/modules/bluetooth/option.h b/src/modules/bluetooth/option.h
new file mode 100644
index 0000000..1f11d29
--- /dev/null
+++ b/src/modules/bluetooth/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFBluetoothOptions {
+ FFModuleArgs moduleArgs;
+
+ bool showDisconnected;
+ FFPercentageModuleConfig percent;
+} FFBluetoothOptions;
+
+static_assert(sizeof(FFBluetoothOptions) <= FF_OPTION_MAX_SIZE, "FFBluetoothOptions size exceeds maximum allowed size");
diff --git a/src/modules/bluetoothradio/bluetoothradio.c b/src/modules/bluetoothradio/bluetoothradio.c
new file mode 100644
index 0000000..594b00c
--- /dev/null
+++ b/src/modules/bluetoothradio/bluetoothradio.c
@@ -0,0 +1,210 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/bluetoothradio/bluetoothradio.h"
+#include "modules/bluetoothradio/bluetoothradio.h"
+
+#define FF_BLUETOOTHRADIO_DISPLAY_NAME "Bluetooth Radio"
+
+static void printDevice(FFBluetoothRadioOptions* options, const FFBluetoothRadioResult* radio, uint8_t index) {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufAppendF(&key, "%s (%s)", FF_BLUETOOTHRADIO_DISPLAY_NAME, radio->name.chars);
+ } else {
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(radio->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ const char* version = NULL;
+
+ switch (radio->lmpVersion < 0 ? -radio->lmpVersion : radio->lmpVersion) {
+ case 0:
+ version = "1.0b";
+ break;
+ case 1:
+ version = "1.1";
+ break;
+ case 2:
+ version = "1.2";
+ break;
+ case 3:
+ version = "2.0";
+ break;
+ case 4:
+ version = "2.1";
+ break;
+ case 5:
+ version = "3.0";
+ break;
+ case 6:
+ version = "4.0";
+ break;
+ case 7:
+ version = "4.1";
+ break;
+ case 8:
+ version = "4.2";
+ break;
+ case 9:
+ version = "5.0";
+ break;
+ case 10:
+ version = "5.1";
+ break;
+ case 11:
+ version = "5.2";
+ break;
+ case 12:
+ version = "5.3";
+ break;
+ case 13:
+ version = "5.4";
+ break;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ if (version) {
+ printf("Bluetooth %s%s (%s)\n", version, (radio->lmpVersion < 0 ? "+" : ""), radio->vendor.chars);
+ } else {
+ ffStrbufPutTo(&radio->vendor, stdout);
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(radio->name, "name"),
+ FF_ARG(radio->address, "address"),
+ FF_ARG(radio->lmpVersion, "lmp-version"),
+ FF_ARG(radio->lmpSubversion, "lmp-subversion"),
+ FF_ARG(version, "version"),
+ FF_ARG(radio->vendor, "vendor"),
+ FF_ARG(radio->discoverable, "discoverable"),
+ FF_ARG(radio->connectable, "connectable"),
+ }));
+ }
+}
+
+bool ffPrintBluetoothRadio(FFBluetoothRadioOptions* options) {
+ FF_LIST_AUTO_DESTROY radios = ffListCreate();
+ const char* error = ffDetectBluetoothRadio(&radios);
+
+ if (error) {
+ ffPrintError(FF_BLUETOOTHRADIO_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ uint8_t index = 0;
+ FF_LIST_FOR_EACH (FFBluetoothRadioResult, radio, radios) {
+ if (!radio->enabled) {
+ continue;
+ }
+
+ index++;
+ printDevice(options, radio, index);
+ }
+
+ if (index == 0) {
+ if (radios.length > 0) {
+ ffPrintError(FF_BLUETOOTHRADIO_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Bluetooth radios found but none enabled");
+ } else {
+ ffPrintError(FF_BLUETOOTHRADIO_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No devices detected");
+ }
+ return false;
+ }
+
+ FF_LIST_FOR_EACH (FFBluetoothRadioResult, radio, radios) {
+ ffStrbufDestroy(&radio->name);
+ ffStrbufDestroy(&radio->address);
+ ffStrbufDestroy(&radio->vendor);
+ }
+ return true;
+}
+
+void ffParseBluetoothRadioJsonObject(FFBluetoothRadioOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_BLUETOOTHRADIO_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBluetoothRadioJsonConfig(FFBluetoothRadioOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateBluetoothRadioJsonResult(FF_A_UNUSED FFBluetoothRadioOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBluetoothRadio(&results);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFBluetoothRadioResult, item, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "address", &item->address);
+ if (item->lmpVersion == INT_MIN) {
+ yyjson_mut_obj_add_null(doc, obj, "lmpVersion");
+ } else {
+ yyjson_mut_obj_add_int(doc, obj, "lmpVersion", item->lmpVersion);
+ }
+ if (item->lmpSubversion == INT_MIN) {
+ yyjson_mut_obj_add_null(doc, obj, "lmpSubversion");
+ } else {
+ yyjson_mut_obj_add_int(doc, obj, "lmpSubversion", item->lmpSubversion);
+ }
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &item->vendor);
+ yyjson_mut_obj_add_bool(doc, obj, "enabled", item->enabled);
+ yyjson_mut_obj_add_bool(doc, obj, "discoverable", item->discoverable);
+ yyjson_mut_obj_add_bool(doc, obj, "connectable", item->connectable);
+ }
+
+ FF_LIST_FOR_EACH (FFBluetoothRadioResult, radio, results) {
+ ffStrbufDestroy(&radio->name);
+ ffStrbufDestroy(&radio->address);
+ ffStrbufDestroy(&radio->vendor);
+ }
+
+ return true;
+}
+
+void ffInitBluetoothRadioOptions(FFBluetoothRadioOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰐻");
+}
+
+void ffDestroyBluetoothRadioOptions(FFBluetoothRadioOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBluetoothRadioModuleInfo = {
+ .name = FF_BLUETOOTHRADIO_MODULE_NAME,
+ .description = "List Bluetooth radios (supported versions, vendors, etc.)",
+ .initOptions = (void*) ffInitBluetoothRadioOptions,
+ .destroyOptions = (void*) ffDestroyBluetoothRadioOptions,
+ .parseJsonObject = (void*) ffParseBluetoothRadioJsonObject,
+ .printModule = (void*) ffPrintBluetoothRadio,
+ .generateJsonResult = (void*) ffGenerateBluetoothRadioJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBluetoothRadioJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Radio name for discovering", "name" },
+ { "Address", "address" },
+ { "LMP version", "lmp-version" },
+ { "LMP subversion", "lmp-subversion" },
+ { "Bluetooth version", "version" },
+ { "Vendor", "vendor" },
+ { "Discoverable", "discoverable" },
+ { "Connectable / Pairable", "connectable" },
+ }))
+};
diff --git a/src/modules/bluetoothradio/bluetoothradio.h b/src/modules/bluetoothradio/bluetoothradio.h
new file mode 100644
index 0000000..4707f1b
--- /dev/null
+++ b/src/modules/bluetoothradio/bluetoothradio.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BLUETOOTHRADIO_MODULE_NAME "BluetoothRadio"
+
+bool ffPrintBluetoothRadio(FFBluetoothRadioOptions* options);
+void ffInitBluetoothRadioOptions(FFBluetoothRadioOptions* options);
+void ffDestroyBluetoothRadioOptions(FFBluetoothRadioOptions* options);
+
+extern FFModuleBaseInfo ffBluetoothRadioModuleInfo;
diff --git a/src/modules/bluetoothradio/option.h b/src/modules/bluetoothradio/option.h
new file mode 100644
index 0000000..579e899
--- /dev/null
+++ b/src/modules/bluetoothradio/option.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFBluetoothRadioOptions {
+ FFModuleArgs moduleArgs;
+} FFBluetoothRadioOptions;
+
+static_assert(sizeof(FFBluetoothRadioOptions) <= FF_OPTION_MAX_SIZE, "FFBluetoothRadioOptions size exceeds maximum allowed size");
diff --git a/src/modules/board/board.c b/src/modules/board/board.c
new file mode 100644
index 0000000..a0801a2
--- /dev/null
+++ b/src/modules/board/board.c
@@ -0,0 +1,125 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/board/board.h"
+#include "modules/board/board.h"
+
+bool ffPrintBoard(FFBoardOptions* options) {
+ bool success = false;
+ FFBoardResult result;
+ ffStrbufInit(&result.name);
+ ffStrbufInit(&result.vendor);
+ ffStrbufInit(&result.version);
+ ffStrbufInit(&result.serial);
+
+ const char* error = ffDetectBoard(&result);
+ if (error) {
+ ffPrintError(FF_BOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (result.name.length == 0) {
+ ffPrintError(FF_BOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "board_name is not set.");
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_BOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result.name, stdout);
+ if (result.version.length) {
+ printf(" (%s)", result.version.chars);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_BOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.name, "name"),
+ FF_ARG(result.vendor, "vendor"),
+ FF_ARG(result.version, "version"),
+ FF_ARG(result.serial, "serial"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.name);
+ ffStrbufDestroy(&result.vendor);
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.serial);
+ return success;
+}
+
+void ffParseBoardJsonObject(FFBoardOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_BOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBoardJsonConfig(FFBoardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateBoardJsonResult(FF_A_UNUSED FFBoardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFBoardResult board;
+ ffStrbufInit(&board.name);
+ ffStrbufInit(&board.vendor);
+ ffStrbufInit(&board.version);
+ ffStrbufInit(&board.serial);
+
+ const char* error = ffDetectBoard(&board);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ if (board.name.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "board_name is not set.");
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &board.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &board.vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &board.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &board.serial);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&board.name);
+ ffStrbufDestroy(&board.vendor);
+ ffStrbufDestroy(&board.version);
+ ffStrbufDestroy(&board.serial);
+ return success;
+}
+
+void ffInitBoardOptions(FFBoardOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyBoardOptions(FFBoardOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBoardModuleInfo = {
+ .name = FF_BOARD_MODULE_NAME,
+ .description = "Print motherboard name and other information",
+ .initOptions = (void*) ffInitBoardOptions,
+ .destroyOptions = (void*) ffDestroyBoardOptions,
+ .parseJsonObject = (void*) ffParseBoardJsonObject,
+ .printModule = (void*) ffPrintBoard,
+ .generateJsonResult = (void*) ffGenerateBoardJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBoardJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Board name", "name" },
+ { "Board vendor", "vendor" },
+ { "Board version", "version" },
+ { "Board serial number", "serial" },
+ }))
+};
diff --git a/src/modules/board/board.h b/src/modules/board/board.h
new file mode 100644
index 0000000..eae3d73
--- /dev/null
+++ b/src/modules/board/board.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BOARD_MODULE_NAME "Board"
+
+bool ffPrintBoard(FFBoardOptions* options);
+void ffInitBoardOptions(FFBoardOptions* options);
+void ffDestroyBoardOptions(FFBoardOptions* options);
+
+extern FFModuleBaseInfo ffBoardModuleInfo;
diff --git a/src/modules/board/option.h b/src/modules/board/option.h
new file mode 100644
index 0000000..c45ddd8
--- /dev/null
+++ b/src/modules/board/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFBoardOptions {
+ FFModuleArgs moduleArgs;
+} FFBoardOptions;
+
+static_assert(sizeof(FFBoardOptions) <= FF_OPTION_MAX_SIZE, "FFBoardOptions size exceeds maximum allowed size");
diff --git a/src/modules/bootmgr/bootmgr.c b/src/modules/bootmgr/bootmgr.c
new file mode 100644
index 0000000..f44d67a
--- /dev/null
+++ b/src/modules/bootmgr/bootmgr.c
@@ -0,0 +1,121 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/bootmgr/bootmgr.h"
+#include "modules/bootmgr/bootmgr.h"
+
+bool ffPrintBootmgr(FFBootmgrOptions* options) {
+ bool success = false;
+ FFBootmgrResult bootmgr = {
+ .name = ffStrbufCreate(),
+ .firmware = ffStrbufCreate(),
+ };
+
+ const char* error = ffDetectBootmgr(&bootmgr);
+
+ if (error) {
+ ffPrintError(FF_BOOTMGR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ } else {
+ FF_STRBUF_AUTO_DESTROY firmwareName = ffStrbufCreateCopy(&bootmgr.firmware);
+#ifndef __APPLE__
+ ffStrbufSubstrAfterLastC(&firmwareName, '\\');
+#else
+ ffStrbufSubstrAfterLastC(&firmwareName, '/');
+#endif
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_BOOTMGR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&bootmgr.name, stdout);
+ if (firmwareName.length > 0) {
+ printf(" - %s\n", firmwareName.chars);
+ } else {
+ putchar('\n');
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_BOOTMGR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(bootmgr.name, "name"),
+ FF_ARG(bootmgr.firmware, "firmware-path"),
+ FF_ARG(firmwareName, "firmware-name"),
+ FF_ARG(bootmgr.secureBoot, "secure-boot"),
+ FF_ARG(bootmgr.order, "order"),
+ }));
+ }
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&bootmgr.name);
+ ffStrbufDestroy(&bootmgr.firmware);
+
+ return success;
+}
+
+void ffParseBootmgrJsonObject(FFBootmgrOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_BOOTMGR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBootmgrJsonConfig(FFBootmgrOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateBootmgrJsonResult(FF_A_UNUSED FFBootmgrOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFBootmgrResult bootmgr = {
+ .name = ffStrbufCreate(),
+ .firmware = ffStrbufCreate(),
+ };
+
+ const char* error = ffDetectBootmgr(&bootmgr);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &bootmgr.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "firmware", &bootmgr.firmware);
+ yyjson_mut_obj_add_uint(doc, obj, "order", bootmgr.order);
+ yyjson_mut_obj_add_bool(doc, obj, "secureBoot", bootmgr.secureBoot);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&bootmgr.name);
+ ffStrbufDestroy(&bootmgr.firmware);
+ return success;
+}
+
+void ffInitBootmgrOptions(FFBootmgrOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyBootmgrOptions(FFBootmgrOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBootmgrModuleInfo = {
+ .name = FF_BOOTMGR_MODULE_NAME,
+ .description = "Print second-stage bootloader information (name, firmware, etc.)",
+ .initOptions = (void*) ffInitBootmgrOptions,
+ .destroyOptions = (void*) ffDestroyBootmgrOptions,
+ .parseJsonObject = (void*) ffParseBootmgrJsonObject,
+ .printModule = (void*) ffPrintBootmgr,
+ .generateJsonResult = (void*) ffGenerateBootmgrJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBootmgrJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name / description", "name" },
+ { "Firmware file path", "firmware-path" },
+ { "Firmware file name", "firmware-name" },
+ { "Is secure boot enabled", "secure-boot" },
+ { "Boot order", "order" },
+ }))
+};
diff --git a/src/modules/bootmgr/bootmgr.h b/src/modules/bootmgr/bootmgr.h
new file mode 100644
index 0000000..4f6a2ba
--- /dev/null
+++ b/src/modules/bootmgr/bootmgr.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BOOTMGR_MODULE_NAME "Bootmgr"
+
+bool ffPrintBootmgr(FFBootmgrOptions* options);
+void ffInitBootmgrOptions(FFBootmgrOptions* options);
+void ffDestroyBootmgrOptions(FFBootmgrOptions* options);
+
+extern FFModuleBaseInfo ffBootmgrModuleInfo;
diff --git a/src/modules/bootmgr/option.h b/src/modules/bootmgr/option.h
new file mode 100644
index 0000000..2a01fb4
--- /dev/null
+++ b/src/modules/bootmgr/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFBootmgrOptions {
+ FFModuleArgs moduleArgs;
+} FFBootmgrOptions;
+
+static_assert(sizeof(FFBootmgrOptions) <= FF_OPTION_MAX_SIZE, "FFBootmgrOptions size exceeds maximum allowed size");
diff --git a/src/modules/break/break.c b/src/modules/break/break.c
new file mode 100644
index 0000000..e560e18
--- /dev/null
+++ b/src/modules/break/break.c
@@ -0,0 +1,36 @@
+#include "common/printing.h"
+#include "logo/logo.h"
+#include "modules/break/break.h"
+
+bool ffPrintBreak(FF_A_UNUSED FFBreakOptions* options) {
+ ffLogoPrintLine();
+ putchar('\n');
+ return true;
+}
+
+void ffParseBreakJsonObject(FF_A_UNUSED FFBreakOptions* options, FF_A_UNUSED yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
+ continue;
+ }
+
+ ffPrintError(FF_BREAK_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffInitBreakOptions(FF_A_UNUSED FFBreakOptions* options) {
+}
+
+void ffDestroyBreakOptions(FF_A_UNUSED FFBreakOptions* options) {
+}
+
+FFModuleBaseInfo ffBreakModuleInfo = {
+ .name = FF_BREAK_MODULE_NAME,
+ .description = "Print an empty line",
+ .initOptions = (void*) ffInitBreakOptions,
+ .destroyOptions = (void*) ffDestroyBreakOptions,
+ .parseJsonObject = (void*) ffParseBreakJsonObject,
+ .printModule = (void*) ffPrintBreak,
+};
diff --git a/src/modules/break/break.h b/src/modules/break/break.h
new file mode 100644
index 0000000..f611b73
--- /dev/null
+++ b/src/modules/break/break.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BREAK_MODULE_NAME "Break"
+
+bool ffPrintBreak(FFBreakOptions* options);
+void ffInitBreakOptions(FFBreakOptions* options);
+void ffDestroyBreakOptions(FFBreakOptions* options);
+
+extern FFModuleBaseInfo ffBreakModuleInfo;
diff --git a/src/modules/break/option.h b/src/modules/break/option.h
new file mode 100644
index 0000000..90a44df
--- /dev/null
+++ b/src/modules/break/option.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFBreakOptions {
+} FFBreakOptions;
+
+static_assert(sizeof(FFBreakOptions) <= FF_OPTION_MAX_SIZE, "FFBreakOptions size exceeds maximum allowed size");
diff --git a/src/modules/brightness/brightness.c b/src/modules/brightness/brightness.c
new file mode 100644
index 0000000..54bb989
--- /dev/null
+++ b/src/modules/brightness/brightness.c
@@ -0,0 +1,210 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/brightness/brightness.h"
+#include "modules/brightness/brightness.h"
+
+bool ffPrintBrightness(FFBrightnessOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectBrightness(options, &result);
+
+ if (error) {
+ ffPrintError(FF_BRIGHTNESS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_BRIGHTNESS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No result is detected.");
+ return false;
+ }
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->compact) {
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+
+ FF_LIST_FOR_EACH (FFBrightnessResult, item, result) {
+ if (str.length > 0) {
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ const double percent = (item->current - item->min) / (item->max - item->min) * 100;
+ ffPercentAppendNum(&str, percent, options->percent, false, &options->moduleArgs);
+ }
+
+ ffPrintLogoAndKey(FF_BRIGHTNESS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&str, stdout);
+ return true;
+ }
+
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ uint32_t index = 0;
+ FF_LIST_FOR_EACH (FFBrightnessResult, item, result) {
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufAppendF(&key, "%s (%s)", FF_BRIGHTNESS_MODULE_NAME, item->name.chars);
+ } else {
+ uint32_t moduleIndex = result.length == 1 ? 0 : index + 1;
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(moduleIndex, "index"),
+ FF_ARG(item->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ const double percent = (item->current - item->min) / (item->max - item->min) * 100;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, percent, options->percent, &options->moduleArgs);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ if (str.length > 0) {
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ ffPercentAppendNum(&str, percent, options->percent, str.length > 0, &options->moduleArgs);
+ }
+
+ ffStrbufAppendS(&str, item->builtin ? " [Built-in]" : " [External]");
+
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY valueNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&valueNum, percent, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY valueBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&valueBar, percent, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(valueNum, "percentage"),
+ FF_ARG(item->name, "name"),
+ FF_ARG(item->max, "max"),
+ FF_ARG(item->min, "min"),
+ FF_ARG(item->current, "current"),
+ FF_ARG(valueBar, "percentage-bar"),
+ FF_ARG(item->builtin, "is-builtin"),
+ }));
+ }
+
+ ffStrbufClear(&key);
+ ffStrbufDestroy(&item->name);
+ ++index;
+ }
+
+ return true;
+}
+
+void ffParseBrightnessJsonObject(FFBrightnessOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ddcciSleep")) {
+ if (yyjson_is_null(val)) {
+ options->ddcciSleep = FF_BRIGHTNESS_DDCCI_SLEEP_SKIP;
+ } else {
+ options->ddcciSleep = (uint32_t) yyjson_get_uint(val);
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compact")) {
+ options->compact = (uint32_t) yyjson_get_bool(val);
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_BRIGHTNESS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBrightnessJsonConfig(FFBrightnessOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ if (options->ddcciSleep == FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) {
+ yyjson_mut_obj_add_null(doc, module, "ddcciSleep");
+ } else {
+ yyjson_mut_obj_add_uint(doc, module, "ddcciSleep", options->ddcciSleep);
+ }
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+
+ yyjson_mut_obj_add_bool(doc, module, "compact", options->compact);
+}
+
+bool ffGenerateBrightnessJsonResult(FF_A_UNUSED FFBrightnessOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectBrightness(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_arr(doc);
+ yyjson_mut_obj_add_val(doc, module, "result", arr);
+
+ FF_LIST_FOR_EACH (FFBrightnessResult, item, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_obj_add_real(doc, obj, "max", item->max);
+ yyjson_mut_obj_add_real(doc, obj, "min", item->min);
+ yyjson_mut_obj_add_real(doc, obj, "current", item->current);
+ yyjson_mut_obj_add_bool(doc, obj, "builtin", item->builtin);
+ }
+
+ FF_LIST_FOR_EACH (FFBrightnessResult, item, result) {
+ ffStrbufDestroy(&item->name);
+ }
+
+ return true;
+}
+
+void ffInitBrightnessOptions(FFBrightnessOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰯪");
+
+ options->ddcciSleep = 10;
+ options->percent = (FFPercentageModuleConfig) { 100, 100, 0 };
+ options->compact = false;
+}
+
+void ffDestroyBrightnessOptions(FFBrightnessOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBrightnessModuleInfo = {
+ .name = FF_BRIGHTNESS_MODULE_NAME,
+ .description = "Print the current brightness level of your monitors",
+ .initOptions = (void*) ffInitBrightnessOptions,
+ .destroyOptions = (void*) ffDestroyBrightnessOptions,
+ .parseJsonObject = (void*) ffParseBrightnessJsonObject,
+ .printModule = (void*) ffPrintBrightness,
+ .generateJsonResult = (void*) ffGenerateBrightnessJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBrightnessJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Screen brightness (percentage num)", "percentage" },
+ { "Screen name", "name" },
+ { "Maximum brightness value", "max" },
+ { "Minimum brightness value", "min" },
+ { "Current brightness value", "current" },
+ { "Screen brightness (percentage bar)", "percentage-bar" },
+ { "Is built-in screen", "is-builtin" },
+ }))
+};
diff --git a/src/modules/brightness/brightness.h b/src/modules/brightness/brightness.h
new file mode 100644
index 0000000..0853661
--- /dev/null
+++ b/src/modules/brightness/brightness.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BRIGHTNESS_MODULE_NAME "Brightness"
+
+bool ffPrintBrightness(FFBrightnessOptions* options);
+void ffInitBrightnessOptions(FFBrightnessOptions* options);
+void ffDestroyBrightnessOptions(FFBrightnessOptions* options);
+
+extern FFModuleBaseInfo ffBrightnessModuleInfo;
diff --git a/src/modules/brightness/option.h b/src/modules/brightness/option.h
new file mode 100644
index 0000000..a31228d
--- /dev/null
+++ b/src/modules/brightness/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFBrightnessOptions {
+ FFModuleArgs moduleArgs;
+
+ uint32_t ddcciSleep; // ms
+ FFPercentageModuleConfig percent;
+ bool compact;
+} FFBrightnessOptions;
+
+static_assert(sizeof(FFBrightnessOptions) <= FF_OPTION_MAX_SIZE, "FFBrightnessOptions size exceeds maximum allowed size");
diff --git a/src/modules/btrfs/btrfs.c b/src/modules/btrfs/btrfs.c
new file mode 100644
index 0000000..238d177
--- /dev/null
+++ b/src/modules/btrfs/btrfs.c
@@ -0,0 +1,224 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/btrfs/btrfs.h"
+#include "modules/btrfs/btrfs.h"
+
+static void printBtrfs(FFBtrfsOptions* options, FFBtrfsResult* result, uint8_t index) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ if (options->moduleArgs.key.length == 0) {
+ if (result->name.length > 0) {
+ ffStrbufSetF(&buffer, "%s (%s)", FF_BTRFS_MODULE_NAME, result->name.chars);
+ } else {
+ ffStrbufSetS(&buffer, FF_BTRFS_MODULE_NAME);
+ }
+ } else {
+ ffStrbufClear(&buffer);
+ FF_PARSE_FORMAT_STRING_CHECKED(&buffer, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ uint64_t used = 0, allocated = 0, total = result->totalSize;
+ for (uint32_t i = 0; i < ARRAY_SIZE(result->allocation); ++i) {
+ uint64_t times = result->allocation[i].copies;
+ used += result->allocation[i].used * times;
+ allocated += result->allocation[i].total * times;
+ }
+
+ FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
+ ffSizeAppendNum(used, &usedPretty);
+ FF_STRBUF_AUTO_DESTROY allocatedPretty = ffStrbufCreate();
+ ffSizeAppendNum(allocated, &allocatedPretty);
+ FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
+ ffSizeAppendNum(total, &totalPretty);
+
+ double usedPercentage = total > 0 ? (double) used / (double) total * 100.0 : 0;
+ double allocatedPercentage = total > 0 ? (double) allocated / (double) total * 100.0 : 0;
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(buffer.chars, index, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ ffStrbufClear(&buffer);
+ ffStrbufSetF(&buffer, "%s / %s (", usedPretty.chars, totalPretty.chars);
+ ffPercentAppendNum(&buffer, usedPercentage, options->percent, false, &options->moduleArgs);
+ ffStrbufAppendS(&buffer, ", ");
+ ffPercentAppendNum(&buffer, allocatedPercentage, options->percent, false, &options->moduleArgs);
+ ffStrbufAppendF(&buffer, " allocated)");
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY usedPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&usedPercentageNum, usedPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY usedPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&usedPercentageBar, usedPercentage, options->percent, &options->moduleArgs);
+ }
+
+ FF_STRBUF_AUTO_DESTROY allocatedPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&allocatedPercentageNum, allocatedPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY allocatedPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&allocatedPercentageBar, allocatedPercentage, options->percent, &options->moduleArgs);
+ }
+
+ FF_STRBUF_AUTO_DESTROY nodeSizePretty = ffStrbufCreate();
+ ffSizeAppendNum(result->nodeSize, &nodeSizePretty);
+ FF_STRBUF_AUTO_DESTROY sectorSizePretty = ffStrbufCreate();
+ ffSizeAppendNum(result->sectorSize, &sectorSizePretty);
+
+ FF_PRINT_FORMAT_CHECKED(buffer.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(result->name, "name"),
+ FF_ARG(result->uuid, "uuid"),
+ FF_ARG(result->devices, "devices"),
+ FF_ARG(result->features, "features"),
+ FF_ARG(usedPretty, "used"),
+ FF_ARG(allocatedPretty, "allocated"),
+ FF_ARG(totalPretty, "total"),
+ FF_ARG(usedPercentageNum, "used-percentage"),
+ FF_ARG(allocatedPercentageNum, "allocated-percentage"),
+ FF_ARG(usedPercentageBar, "used-percentage-bar"),
+ FF_ARG(allocatedPercentageBar, "allocated-percentage-bar"),
+ FF_ARG(nodeSizePretty, "node-size"),
+ FF_ARG(sectorSizePretty, "sector-size"),
+ }));
+ }
+}
+
+bool ffPrintBtrfs(FFBtrfsOptions* options) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBtrfs(&results);
+
+ if (error) {
+ ffPrintError(FF_BTRFS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+ if (results.length == 0) {
+ ffPrintError(FF_BTRFS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", "No btrfs drive found");
+ return false;
+ }
+
+ for (uint32_t i = 0; i < results.length; i++) {
+ FFBtrfsResult* result = FF_LIST_GET(FFBtrfsResult, results, i);
+ uint8_t index = results.length == 1 ? 0 : (uint8_t) (i + 1);
+ printBtrfs(options, result, index);
+ }
+
+ FF_LIST_FOR_EACH (FFBtrfsResult, result, results) {
+ ffStrbufDestroy(&result->name);
+ ffStrbufDestroy(&result->uuid);
+ ffStrbufDestroy(&result->devices);
+ ffStrbufDestroy(&result->features);
+ }
+
+ return true;
+}
+
+void ffParseBtrfsJsonObject(FFBtrfsOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_BTRFS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateBtrfsJsonConfig(FFBtrfsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateBtrfsJsonResult(FF_A_UNUSED FFBtrfsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectBtrfs(&results);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFBtrfsResult, btrfs, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &btrfs->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "uuid", &btrfs->uuid);
+ yyjson_mut_obj_add_strbuf(doc, obj, "devices", &btrfs->devices);
+ yyjson_mut_obj_add_strbuf(doc, obj, "features", &btrfs->features);
+ yyjson_mut_obj_add_uint(doc, obj, "generation", btrfs->generation);
+ yyjson_mut_obj_add_uint(doc, obj, "nodeSize", btrfs->nodeSize);
+ yyjson_mut_obj_add_uint(doc, obj, "sectorSize", btrfs->sectorSize);
+ yyjson_mut_obj_add_uint(doc, obj, "totalSize", btrfs->totalSize);
+ yyjson_mut_val* allocation = yyjson_mut_obj_add_arr(doc, obj, "allocation");
+ for (uint32_t i = 0; i < ARRAY_SIZE(btrfs->allocation); ++i) {
+ yyjson_mut_val* item = yyjson_mut_arr_add_obj(doc, allocation);
+ yyjson_mut_obj_add_str(doc, item, "type", btrfs->allocation[i].type);
+ yyjson_mut_obj_add_str(doc, item, "profile", btrfs->allocation[i].profile);
+ yyjson_mut_obj_add_uint(doc, item, "copies", btrfs->allocation[i].copies);
+ yyjson_mut_obj_add_uint(doc, item, "used", btrfs->allocation[i].used);
+ yyjson_mut_obj_add_uint(doc, item, "total", btrfs->allocation[i].total);
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFBtrfsResult, btrfs, results) {
+ ffStrbufDestroy(&btrfs->name);
+ ffStrbufDestroy(&btrfs->uuid);
+ ffStrbufDestroy(&btrfs->devices);
+ ffStrbufDestroy(&btrfs->features);
+ }
+
+ return true;
+}
+
+void ffInitBtrfsOptions(FFBtrfsOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󱑛");
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+}
+
+void ffDestroyBtrfsOptions(FFBtrfsOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffBtrfsModuleInfo = {
+ .name = FF_BTRFS_MODULE_NAME,
+ .description = "Print Linux BTRFS volumes",
+ .initOptions = (void*) ffInitBtrfsOptions,
+ .destroyOptions = (void*) ffDestroyBtrfsOptions,
+ .parseJsonObject = (void*) ffParseBtrfsJsonObject,
+ .printModule = (void*) ffPrintBtrfs,
+ .generateJsonResult = (void*) ffGenerateBtrfsJsonResult,
+ .generateJsonConfig = (void*) ffGenerateBtrfsJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name / Label", "name" },
+ { "UUID", "uuid" },
+ { "Associated devices", "devices" },
+ { "Enabled features", "features" },
+ { "Size used", "used" },
+ { "Size allocated", "allocated" },
+ { "Size total", "total" },
+ { "Used percentage num", "used-percentage" },
+ { "Allocated percentage num", "allocated-percentage" },
+ { "Used percentage bar", "used-percentage-bar" },
+ { "Allocated percentage bar", "allocated-percentage-bar" },
+ { "Node size", "node-size" },
+ { "Sector size", "sector-size" },
+ }))
+};
diff --git a/src/modules/btrfs/btrfs.h b/src/modules/btrfs/btrfs.h
new file mode 100644
index 0000000..fc0c6ab
--- /dev/null
+++ b/src/modules/btrfs/btrfs.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_BTRFS_MODULE_NAME "Btrfs"
+
+bool ffPrintBtrfs(FFBtrfsOptions* options);
+void ffInitBtrfsOptions(FFBtrfsOptions* options);
+void ffDestroyBtrfsOptions(FFBtrfsOptions* options);
+
+extern FFModuleBaseInfo ffBtrfsModuleInfo;
diff --git a/src/modules/btrfs/option.h b/src/modules/btrfs/option.h
new file mode 100644
index 0000000..0530cef
--- /dev/null
+++ b/src/modules/btrfs/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFBtrfsOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+} FFBtrfsOptions;
+
+static_assert(sizeof(FFBtrfsOptions) <= FF_OPTION_MAX_SIZE, "FFBtrfsOptions size exceeds maximum allowed size");
diff --git a/src/modules/camera/camera.c b/src/modules/camera/camera.c
new file mode 100644
index 0000000..7067bbc
--- /dev/null
+++ b/src/modules/camera/camera.c
@@ -0,0 +1,134 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/libc/libc.h"
+#include "detection/camera/camera.h"
+#include "modules/camera/camera.h"
+
+static void printDevice(FFCameraOptions* options, const FFCameraResult* device, uint8_t index) {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CAMERA_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ ffStrbufWriteTo(&device->name, stdout);
+ if (device->colorspace.length > 0) {
+ fputs(" - ", stdout);
+ ffStrbufWriteTo(&device->colorspace, stdout);
+ }
+
+ if (device->width > 0 && device->height > 0) {
+ printf(" (%ux%u px)\n", (unsigned) device->width, (unsigned) device->height);
+ } else {
+ putchar('\n');
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_CAMERA_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, (((FFformatarg[]) {
+ FF_ARG(device->name, "name"),
+ FF_ARG(device->vendor, "vendor"),
+ FF_ARG(device->colorspace, "colorspace"),
+ FF_ARG(device->id, "id"),
+ FF_ARG(device->width, "width"),
+ FF_ARG(device->height, "height"),
+ })));
+ }
+}
+
+bool ffPrintCamera(FFCameraOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectCamera(&result);
+
+ if (error) {
+ ffPrintError(FF_CAMERA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_CAMERA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No camera found");
+ return false;
+ }
+
+ for (uint32_t i = 0; i < result.length; i++) {
+ uint8_t index = (uint8_t) (result.length == 1 ? 0 : i + 1);
+ printDevice(options, FF_LIST_GET(FFCameraResult, result, i), index);
+ }
+
+ FF_LIST_FOR_EACH (FFCameraResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->id);
+ ffStrbufDestroy(&dev->colorspace);
+ }
+
+ return true;
+}
+
+void ffParseCameraJsonObject(FFCameraOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_CAMERA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCameraJsonConfig(FFCameraOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateCameraJsonResult(FF_A_UNUSED FFCameraOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectCamera(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFCameraResult, dev, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &dev->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &dev->vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "colorSpace", &dev->colorspace);
+ yyjson_mut_obj_add_strbuf(doc, obj, "id", &dev->id);
+ yyjson_mut_obj_add_uint(doc, obj, "width", dev->width);
+ yyjson_mut_obj_add_uint(doc, obj, "height", dev->height);
+ }
+
+ FF_LIST_FOR_EACH (FFCameraResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->id);
+ ffStrbufDestroy(&dev->colorspace);
+ }
+
+ return true;
+}
+
+void ffInitCameraOptions(FFCameraOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰄀");
+}
+
+void ffDestroyCameraOptions(FFCameraOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCameraModuleInfo = {
+ .name = FF_CAMERA_MODULE_NAME,
+ .description = "Print available cameras",
+ .initOptions = (void*) ffInitCameraOptions,
+ .destroyOptions = (void*) ffDestroyCameraOptions,
+ .parseJsonObject = (void*) ffParseCameraJsonObject,
+ .printModule = (void*) ffPrintCamera,
+ .generateJsonResult = (void*) ffGenerateCameraJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCameraJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Device name", "name" },
+ { "Vendor", "vendor" },
+ { "Color space", "colorspace" },
+ { "Identifier", "id" },
+ { "Width (in px)", "width" },
+ { "Height (in px)", "height" },
+ }))
+};
diff --git a/src/modules/camera/camera.h b/src/modules/camera/camera.h
new file mode 100644
index 0000000..e8b3560
--- /dev/null
+++ b/src/modules/camera/camera.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CAMERA_MODULE_NAME "Camera"
+
+bool ffPrintCamera(FFCameraOptions* options);
+void ffInitCameraOptions(FFCameraOptions* options);
+void ffDestroyCameraOptions(FFCameraOptions* options);
+
+extern FFModuleBaseInfo ffCameraModuleInfo;
diff --git a/src/modules/camera/option.h b/src/modules/camera/option.h
new file mode 100644
index 0000000..51eb29f
--- /dev/null
+++ b/src/modules/camera/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCameraOptions {
+ FFModuleArgs moduleArgs;
+} FFCameraOptions;
+
+static_assert(sizeof(FFCameraOptions) <= FF_OPTION_MAX_SIZE, "FFCameraOptions size exceeds maximum allowed size");
diff --git a/src/modules/chassis/chassis.c b/src/modules/chassis/chassis.c
new file mode 100644
index 0000000..d944baa
--- /dev/null
+++ b/src/modules/chassis/chassis.c
@@ -0,0 +1,127 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/chassis/chassis.h"
+#include "modules/chassis/chassis.h"
+
+bool ffPrintChassis(FFChassisOptions* options) {
+ bool success = false;
+
+ FFChassisResult result;
+ ffStrbufInit(&result.type);
+ ffStrbufInit(&result.vendor);
+ ffStrbufInit(&result.version);
+ ffStrbufInit(&result.serial);
+
+ const char* error = ffDetectChassis(&result);
+
+ if (error) {
+ ffPrintError(FF_CHASSIS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (result.type.length == 0) {
+ ffPrintError(FF_CHASSIS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "chassis_type is not set by O.E.M.");
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CHASSIS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result.type, stdout);
+ if (result.version.length) {
+ printf(" (%s)", result.version.chars);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_CHASSIS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.type, "type"),
+ FF_ARG(result.vendor, "vendor"),
+ FF_ARG(result.version, "version"),
+ FF_ARG(result.serial, "serial"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.type);
+ ffStrbufDestroy(&result.vendor);
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.serial);
+ return success;
+}
+
+void ffParseChassisJsonObject(FFChassisOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_CHASSIS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateChassisJsonConfig(FFChassisOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateChassisJsonResult(FF_A_UNUSED FFChassisOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFChassisResult result;
+ ffStrbufInit(&result.type);
+ ffStrbufInit(&result.vendor);
+ ffStrbufInit(&result.version);
+ ffStrbufInit(&result.serial);
+
+ const char* error = ffDetectChassis(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ if (result.type.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "chassis_type is not set by O.E.M.");
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "type", &result.type);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &result.vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &result.serial);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.type);
+ ffStrbufDestroy(&result.vendor);
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.serial);
+ return success;
+}
+
+void ffInitChassisOptions(FFChassisOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyChassisOptions(FFChassisOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffChassisModuleInfo = {
+ .name = FF_CHASSIS_MODULE_NAME,
+ .description = "Print chassis type information (desktop, laptop, etc.)",
+ .initOptions = (void*) ffInitChassisOptions,
+ .destroyOptions = (void*) ffDestroyChassisOptions,
+ .parseJsonObject = (void*) ffParseChassisJsonObject,
+ .printModule = (void*) ffPrintChassis,
+ .generateJsonResult = (void*) ffGenerateChassisJsonResult,
+ .generateJsonConfig = (void*) ffGenerateChassisJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Chassis type", "type" },
+ { "Chassis vendor", "vendor" },
+ { "Chassis version", "version" },
+ { "Chassis serial number", "serial" },
+ })),
+};
diff --git a/src/modules/chassis/chassis.h b/src/modules/chassis/chassis.h
new file mode 100644
index 0000000..d3ba489
--- /dev/null
+++ b/src/modules/chassis/chassis.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CHASSIS_MODULE_NAME "Chassis"
+
+bool ffPrintChassis(FFChassisOptions* options);
+void ffInitChassisOptions(FFChassisOptions* options);
+void ffDestroyChassisOptions(FFChassisOptions* options);
+
+extern FFModuleBaseInfo ffChassisModuleInfo;
diff --git a/src/modules/chassis/option.h b/src/modules/chassis/option.h
new file mode 100644
index 0000000..dc54227
--- /dev/null
+++ b/src/modules/chassis/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFChassisOptions {
+ FFModuleArgs moduleArgs;
+} FFChassisOptions;
+
+static_assert(sizeof(FFChassisOptions) <= FF_OPTION_MAX_SIZE, "FFChassisOptions size exceeds maximum allowed size");
diff --git a/src/modules/codec/codec.c b/src/modules/codec/codec.c
new file mode 100644
index 0000000..4e6889e
--- /dev/null
+++ b/src/modules/codec/codec.c
@@ -0,0 +1,268 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "detection/codec/codec.h"
+#include "modules/codec/codec.h"
+
+static const char* ffCodecTypeToString(FFCodecType type) {
+ switch (type) {
+ case FF_CODEC_TYPE_H261:
+ return "H.261";
+ case FF_CODEC_TYPE_H263:
+ return "H.263";
+ case FF_CODEC_TYPE_MJPEG:
+ return "MJPEG";
+ case FF_CODEC_TYPE_MPEG1:
+ return "MPEG-1";
+ case FF_CODEC_TYPE_MPEG2:
+ return "MPEG-2";
+ case FF_CODEC_TYPE_DIVX_XVID:
+ return "DivX / Xvid";
+ case FF_CODEC_TYPE_H264:
+ return "H.264";
+ case FF_CODEC_TYPE_WMV8:
+ return "WMV-8";
+ case FF_CODEC_TYPE_WMV9:
+ return "WMV-9";
+ case FF_CODEC_TYPE_VC1:
+ return "VC-1";
+ case FF_CODEC_TYPE_VP8:
+ return "VP8";
+ case FF_CODEC_TYPE_HEVC:
+ return "HEVC / H.265";
+ case FF_CODEC_TYPE_VP9:
+ return "VP9";
+ case FF_CODEC_TYPE_AV1:
+ return "AV1";
+ case FF_CODEC_TYPE_VVC:
+ return "VVC / H.266";
+ case FF_CODEC_TYPE_DOLBY_VISION_HEVC:
+ return "Dolby Vision (HEVC)";
+ case FF_CODEC_TYPE_PRORES:
+ return "Apple ProRes";
+ case FF_CODEC_TYPE_PRORES_RAW:
+ return "Apple ProRes RAW";
+ case FF_CODEC_TYPE_UNKNOWN:
+ default:
+ return "Unknown";
+ }
+}
+
+static void printCodecLine(const FFCodecOptions* options, uint8_t index, FFstrbuf* gpu, const char* direction, FFCodecType types, const char* platformApi) {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ if (options->moduleArgs.key.length == 0) {
+ if (gpu->length > 0) {
+ ffStrbufSetF(&key, "%s (%s - %s)", FF_CODEC_MODULE_NAME, direction, gpu->chars);
+ } else {
+ ffStrbufSetF(&key, "%s (%s)", FF_CODEC_MODULE_NAME, direction);
+ }
+ } else {
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]){
+ FF_ARG(index, "index"),
+ FF_ARG(*gpu, "gpu"),
+ FF_ARG(direction, "direction"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ FF_STRBUF_AUTO_DESTROY typesJoined = ffStrbufCreate();
+ for (FFCodecType type = FF_CODEC_TYPE_H261; type <= FF_CODEC_TYPE_MAX; type <<= 1) {
+ if ((types & type) == 0) {
+ continue;
+ }
+ if (typesJoined.length > 0) {
+ ffStrbufAppendS(&typesJoined, ", ");
+ }
+ ffStrbufAppendS(&typesJoined, ffCodecTypeToString(type));
+ }
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ puts(typesJoined.length ? typesJoined.chars : "None");
+ } else {
+ FF_LIST_AUTO_DESTROY typeList = ffListCreate(); // Use list instead of pre-joined string for qjs and lua
+ for (FFCodecType type = FF_CODEC_TYPE_H261; type <= FF_CODEC_TYPE_MAX; type <<= 1) {
+ if ((types & type) == 0) {
+ continue;
+ }
+ ffStrbufInitStatic(FF_LIST_ADD(FFstrbuf, typeList), ffCodecTypeToString(type));
+ }
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]){
+ FF_ARG(*gpu, "gpu"),
+ FF_ARG(direction, "direction"),
+ FF_ARG(typeList, "types"),
+ FF_ARG(platformApi, "platform-api"),
+ }));
+ // No need to destroy strings in lists, as they are static strings
+ }
+}
+
+bool ffPrintCodec(FFCodecOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectCodec(options, &result);
+
+ if (error) {
+ ffPrintError(FF_CODEC_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_CODEC_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No hardware video acceleration found");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ if (options->splitGPU) {
+ for (uint32_t i = 0; i < result.length; ++i) {
+ FFCodecResult* item = FF_LIST_GET(FFCodecResult, result, i);
+ uint8_t index = (uint8_t) (result.length == 1 ? 0 : i + 1);
+ if (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) {
+ printCodecLine(options, index, &item->gpu, "Encoder", item->encoders, item->platformApi);
+ }
+ if (options->showType & FF_CODEC_SHOW_TYPE_DECODER) {
+ printCodecLine(options, index, &item->gpu, "Decoder", item->decoders, item->platformApi);
+ }
+ }
+ } else {
+ FFCodecResult merged = {
+ .gpu = ffStrbufCreate(),
+ .decoders = FF_CODEC_TYPE_NONE,
+ .encoders = FF_CODEC_TYPE_NONE,
+ };
+
+ FF_LIST_FOR_EACH (FFCodecResult, item, result) {
+ merged.decoders |= item->decoders;
+ merged.encoders |= item->encoders;
+ merged.platformApi = item->platformApi;
+ }
+ if (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) {
+ printCodecLine(options, 0, &merged.gpu, "Encoder", merged.encoders, merged.platformApi);
+ }
+ if (options->showType & FF_CODEC_SHOW_TYPE_DECODER) {
+ printCodecLine(options, 0, &merged.gpu, "Decoder", merged.decoders, merged.platformApi);
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFCodecResult, item, result) {
+ ffStrbufDestroy(&item->gpu);
+ }
+ return true;
+}
+
+void ffParseCodecJsonObject(FFCodecOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "splitGPU")) {
+ options->splitGPU = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "useVulkan")) {
+ options->useVulkan = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showType")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]){
+ { "encoder", FF_CODEC_SHOW_TYPE_ENCODER },
+ { "decoder", FF_CODEC_SHOW_TYPE_DECODER },
+ { "both", FF_CODEC_SHOW_TYPE_BOTH },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_CODEC_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->showType = (FFCodecShowType) value;
+ }
+ continue;
+ }
+
+ ffPrintError(FF_CODEC_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCodecJsonConfig(FFCodecOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+ yyjson_mut_obj_add_bool(doc, module, "splitGPU", options->splitGPU);
+ yyjson_mut_obj_add_bool(doc, module, "useVulkan", options->useVulkan);
+ switch (options->showType) {
+ case FF_CODEC_SHOW_TYPE_ENCODER:
+ yyjson_mut_obj_add_str(doc, module, "showType", "encoder");
+ break;
+ case FF_CODEC_SHOW_TYPE_DECODER:
+ yyjson_mut_obj_add_str(doc, module, "showType", "decoder");
+ break;
+ case FF_CODEC_SHOW_TYPE_BOTH:
+ yyjson_mut_obj_add_str(doc, module, "showType", "both");
+ break;
+ default:
+ break;
+ }
+}
+
+bool ffGenerateCodecJsonResult(FFCodecOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectCodec(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFCodecResult, item, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "gpu", &item->gpu);
+
+ yyjson_mut_val* encoders = yyjson_mut_obj_add_arr(doc, obj, "encoders");
+ yyjson_mut_val* decoders = yyjson_mut_obj_add_arr(doc, obj, "decoders");
+
+ for (FFCodecType type = FF_CODEC_TYPE_UNKNOWN; type <= FF_CODEC_TYPE_MAX; type <<= 1) {
+ if (item->encoders & type) {
+ yyjson_mut_arr_add_str(doc, encoders, ffCodecTypeToString(type));
+ }
+ if (item->decoders & type) {
+ yyjson_mut_arr_add_str(doc, decoders, ffCodecTypeToString(type));
+ }
+ }
+ yyjson_mut_obj_add_str(doc, obj, "platformApi", item->platformApi);
+ }
+
+ FF_LIST_FOR_EACH (FFCodecResult, item, result) {
+ ffStrbufDestroy(&item->gpu);
+ }
+ return true;
+}
+
+void ffInitCodecOptions(FFCodecOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰈫");
+ options->splitGPU = false;
+ options->useVulkan = false;
+ options->showType = FF_CODEC_SHOW_TYPE_BOTH;
+}
+
+void ffDestroyCodecOptions(FFCodecOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCodecModuleInfo = {
+ .name = FF_CODEC_MODULE_NAME,
+ .description = "Print hardware video acceleration codec types (decode / encode)",
+ .initOptions = (void*) ffInitCodecOptions,
+ .destroyOptions = (void*) ffDestroyCodecOptions,
+ .parseJsonObject = (void*) ffParseCodecJsonObject,
+ .printModule = (void*) ffPrintCodec,
+ .generateJsonResult = (void*) ffGenerateCodecJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCodecJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){
+ { "GPU name", "gpu" },
+ { "Decoder / Encoder", "direction" },
+ { "Compatibility alias of codec types", "types" },
+ { "Platform API used for detection", "platform-api" },
+ }))
+};
diff --git a/src/modules/codec/codec.h b/src/modules/codec/codec.h
new file mode 100644
index 0000000..efd3d7c
--- /dev/null
+++ b/src/modules/codec/codec.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CODEC_MODULE_NAME "Codec"
+
+bool ffPrintCodec(FFCodecOptions* options);
+void ffInitCodecOptions(FFCodecOptions* options);
+void ffDestroyCodecOptions(FFCodecOptions* options);
+
+extern FFModuleBaseInfo ffCodecModuleInfo;
diff --git a/src/modules/codec/option.h b/src/modules/codec/option.h
new file mode 100644
index 0000000..bcc9201
--- /dev/null
+++ b/src/modules/codec/option.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFCodecShowType {
+ FF_CODEC_SHOW_TYPE_NONE = 0,
+ FF_CODEC_SHOW_TYPE_ENCODER = 1 << 0,
+ FF_CODEC_SHOW_TYPE_DECODER = 1 << 1,
+ FF_CODEC_SHOW_TYPE_BOTH = FF_CODEC_SHOW_TYPE_ENCODER | FF_CODEC_SHOW_TYPE_DECODER,
+ FF_CODEC_SHOW_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFCodecShowType;
+
+typedef struct FFCodecOptions {
+ FFModuleArgs moduleArgs;
+ bool splitGPU;
+ bool useVulkan;
+ FFCodecShowType showType;
+} FFCodecOptions;
+
+static_assert(sizeof(FFCodecOptions) <= FF_OPTION_MAX_SIZE, "FFCodecOptions size exceeds maximum allowed size");
diff --git a/src/modules/colors/colors.c b/src/modules/colors/colors.c
new file mode 100644
index 0000000..ff821d1
--- /dev/null
+++ b/src/modules/colors/colors.c
@@ -0,0 +1,305 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/textModifier.h"
+#include "common/color.h"
+#include "common/strutil.h"
+#include "logo/logo.h"
+#include "modules/colors/colors.h"
+
+static inline uint8_t min(uint8_t a, uint8_t b) {
+ return a < b ? a : b;
+}
+
+static inline uint8_t max(uint8_t a, uint8_t b) {
+ return a > b ? a : b;
+}
+
+bool ffPrintColors(FFColorsOptions* options) {
+ bool flag = false;
+
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateA(128);
+
+ if (options->symbol == FF_COLORS_SYMBOL_BLOCK || options->symbol == FF_COLORS_SYMBOL_BACKGROUND) {
+ if (options->brightness != FF_COLORS_BRIGHTNESS_LIGHT) {
+ // 3%d: Set the foreground color
+ for (uint8_t i = options->block.range[0]; i <= min(options->block.range[1], 7); i++) {
+ if (options->symbol == FF_COLORS_SYMBOL_BLOCK) {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendF(&result, "\e[3%dm", i);
+ }
+ for (uint8_t j = 0; j < options->block.width; j++) {
+ ffStrbufAppendS(&result, "█");
+ }
+ } else {
+ ffStrbufAppendF(&result, "\e[4%dm", i);
+ ffStrbufAppendNC(&result, options->block.width, ' ');
+ }
+ }
+ }
+ if (result.length > 0) {
+ ffPrintLogoAndKey(FF_COLORS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ flag = true;
+
+ if (options->paddingLeft > 0) {
+ ffPrintCharTimes(' ', options->paddingLeft);
+ }
+
+ if (!instance.config.display.pipe || options->symbol == FF_COLORS_SYMBOL_BACKGROUND) {
+ ffStrbufAppendS(&result, FASTFETCH_TEXT_MODIFIER_RESET);
+ }
+ ffStrbufPutTo(&result, stdout);
+ ffStrbufClear(&result);
+ }
+
+#ifdef __linux__
+ // Required by Linux Console for light background to work
+ if (options->symbol == FF_COLORS_SYMBOL_BACKGROUND) {
+ const char* term = getenv("TERM");
+ // Should be "linux", however some terminal mulitplexer overrides $TERM
+ if (term && !ffStrStartsWith(term, "xterm")) {
+ ffStrbufAppendS(&result, "\e[5m");
+ }
+ }
+#endif
+
+ if (options->brightness != FF_COLORS_BRIGHTNESS_NORMAL) {
+ // 9%d: Set the foreground to the bright color
+ for (uint8_t i = max(options->block.range[0], 8); i <= options->block.range[1]; i++) {
+ if (options->symbol == FF_COLORS_SYMBOL_BLOCK) {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendF(&result, "\e[9%dm", i - 8);
+ }
+ for (uint8_t j = 0; j < options->block.width; j++) {
+ ffStrbufAppendS(&result, "█");
+ }
+ } else {
+ ffStrbufAppendF(&result, "\e[10%dm", i - 8);
+ ffStrbufAppendNC(&result, options->block.width, ' ');
+ }
+ }
+ }
+ } else {
+ const char* symbol;
+ switch (options->symbol) {
+ case FF_COLORS_SYMBOL_CIRCLE:
+ symbol = "● ";
+ break;
+ case FF_COLORS_SYMBOL_DIAMOND:
+ symbol = "◆ ";
+ break;
+ case FF_COLORS_SYMBOL_TRIANGLE:
+ symbol = "▲ ";
+ break;
+ case FF_COLORS_SYMBOL_SQUARE:
+ symbol = "■ ";
+ break;
+ case FF_COLORS_SYMBOL_STAR:
+ symbol = "★ ";
+ break;
+ default:
+ symbol = "███ ";
+ break;
+ }
+ if (options->brightness == FF_COLORS_BRIGHTNESS_DEFAULT) {
+ for (int i = 8; i >= 1; --i) {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendF(&result, "\e[" FF_COLOR_FG_256 "%dm", i);
+ }
+ ffStrbufAppendS(&result, symbol);
+ }
+ } else {
+ for (int i = 0; i <= 7; ++i) {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendF(&result, "\e[%c%dm", options->brightness == FF_COLORS_BRIGHTNESS_NORMAL ? '3' : '9', i);
+ }
+ ffStrbufAppendS(&result, symbol);
+ }
+ }
+ ffStrbufTrimRight(&result, ' ');
+ }
+
+ if (result.length > 0) {
+ if (flag) {
+ ffLogoPrintLine();
+ } else {
+ ffPrintLogoAndKey(FF_COLORS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ flag = true;
+ }
+
+ if (options->paddingLeft > 0) {
+ ffPrintCharTimes(' ', options->paddingLeft);
+ }
+ if (!instance.config.display.pipe || options->symbol == FF_COLORS_SYMBOL_BACKGROUND) {
+ ffStrbufAppendS(&result, FASTFETCH_TEXT_MODIFIER_RESET);
+ }
+ ffStrbufPutTo(&result, stdout);
+ }
+
+ if (!flag) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", "Nothing to print");
+ return false;
+ }
+
+ return true;
+}
+
+void ffParseColorsJsonObject(FFColorsOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "symbol")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "block", FF_COLORS_SYMBOL_BLOCK },
+ { "background", FF_COLORS_SYMBOL_BACKGROUND },
+ { "circle", FF_COLORS_SYMBOL_CIRCLE },
+ { "diamond", FF_COLORS_SYMBOL_DIAMOND },
+ { "triangle", FF_COLORS_SYMBOL_TRIANGLE },
+ { "square", FF_COLORS_SYMBOL_SQUARE },
+ { "star", FF_COLORS_SYMBOL_STAR },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->symbol = (FFColorsSymbol) value;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "paddingLeft")) {
+ options->paddingLeft = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "block")) {
+ if (!yyjson_is_obj(val)) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s value: must be an object", unsafe_yyjson_get_str(key));
+ } else {
+ yyjson_val* width = yyjson_obj_get(val, "width");
+ if (width) {
+ options->block.width = (uint8_t) yyjson_get_uint(width);
+ }
+
+ yyjson_val* range = yyjson_obj_get(val, "range");
+ if (range) {
+ if (!yyjson_is_arr(range) || yyjson_arr_size(range) != 2) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: must be an array of 2 elements", unsafe_yyjson_get_str(key));
+ } else {
+ uint8_t start = (uint8_t) yyjson_get_uint(yyjson_arr_get(range, 0));
+ uint8_t end = (uint8_t) yyjson_get_uint(yyjson_arr_get(range, 1));
+ if (start > end) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: range[0] > range[1]", unsafe_yyjson_get_str(key));
+ } else if (end > 15) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: range[1] > 15", unsafe_yyjson_get_str(key));
+ } else {
+ options->block.range[0] = start;
+ options->block.range[1] = end;
+ }
+ }
+ }
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "brightness")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "default", FF_COLORS_BRIGHTNESS_DEFAULT },
+ { "normal", FF_COLORS_BRIGHTNESS_NORMAL },
+ { "light", FF_COLORS_BRIGHTNESS_LIGHT },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->brightness = (FFColorsBrightness) value;
+ }
+ continue;
+ }
+
+ ffPrintError(FF_COLORS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateColorsJsonConfig(FFColorsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_remove_key(module, "format"); // Not supported
+
+ switch (options->symbol) {
+ case FF_COLORS_SYMBOL_CIRCLE:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "circle");
+ break;
+ case FF_COLORS_SYMBOL_DIAMOND:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "diamond");
+ break;
+ case FF_COLORS_SYMBOL_TRIANGLE:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "triangle");
+ break;
+ case FF_COLORS_SYMBOL_SQUARE:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "square");
+ break;
+ case FF_COLORS_SYMBOL_STAR:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "star");
+ break;
+ default:
+ yyjson_mut_obj_add_str(doc, module, "symbol", "block");
+ break;
+ }
+
+ yyjson_mut_obj_add_uint(doc, module, "paddingLeft", options->paddingLeft);
+
+ {
+ yyjson_mut_val* block = yyjson_mut_obj_add_obj(doc, module, "block");
+
+ yyjson_mut_obj_add_uint(doc, block, "width", options->block.width);
+
+ yyjson_mut_val* range = yyjson_mut_obj_add_arr(doc, block, "range");
+ for (uint8_t i = 0; i < 2; i++) {
+ yyjson_mut_arr_add_uint(doc, range, options->block.range[i]);
+ }
+ }
+
+ switch (options->brightness) {
+ case FF_COLORS_BRIGHTNESS_NORMAL:
+ yyjson_mut_obj_add_str(doc, module, "brightness", "normal");
+ break;
+ case FF_COLORS_BRIGHTNESS_LIGHT:
+ yyjson_mut_obj_add_str(doc, module, "brightness", "light");
+ break;
+ default:
+ yyjson_mut_obj_add_str(doc, module, "brightness", "default");
+ break;
+ }
+}
+
+void ffInitColorsOptions(FFColorsOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ ffStrbufSetStatic(&options->moduleArgs.key, " ");
+ options->symbol = FF_COLORS_SYMBOL_BACKGROUND;
+ options->paddingLeft = 0;
+ options->block = (FFBlockConfig) {
+ .width = 3,
+ .range = { 0, 15 },
+ };
+ options->brightness = FF_COLORS_BRIGHTNESS_DEFAULT;
+}
+
+void ffDestroyColorsOptions(FFColorsOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffColorsModuleInfo = {
+ .name = FF_COLORS_MODULE_NAME,
+ .description = "Display the terminal's 16-color palette",
+ .initOptions = (void*) ffInitColorsOptions,
+ .destroyOptions = (void*) ffDestroyColorsOptions,
+ .parseJsonObject = (void*) ffParseColorsJsonObject,
+ .printModule = (void*) ffPrintColors,
+ .generateJsonConfig = (void*) ffGenerateColorsJsonConfig,
+};
diff --git a/src/modules/colors/colors.h b/src/modules/colors/colors.h
new file mode 100644
index 0000000..c88ccd7
--- /dev/null
+++ b/src/modules/colors/colors.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_COLORS_MODULE_NAME "Colors"
+
+bool ffPrintColors(FFColorsOptions* options);
+void ffInitColorsOptions(FFColorsOptions* options);
+void ffDestroyColorsOptions(FFColorsOptions* options);
+
+extern FFModuleBaseInfo ffColorsModuleInfo;
diff --git a/src/modules/colors/option.h b/src/modules/colors/option.h
new file mode 100644
index 0000000..c9971c7
--- /dev/null
+++ b/src/modules/colors/option.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFColorsSymbol {
+ FF_COLORS_SYMBOL_BLOCK,
+ FF_COLORS_SYMBOL_BACKGROUND,
+ FF_COLORS_SYMBOL_CIRCLE,
+ FF_COLORS_SYMBOL_DIAMOND,
+ FF_COLORS_SYMBOL_SQUARE,
+ FF_COLORS_SYMBOL_TRIANGLE,
+ FF_COLORS_SYMBOL_STAR,
+} FFColorsSymbol;
+
+typedef struct FFBlockConfig {
+ uint8_t width;
+ uint8_t range[2];
+} FFBlockConfig;
+
+typedef enum FF_A_PACKED FFColorsBrightness {
+ FF_COLORS_BRIGHTNESS_DEFAULT,
+ FF_COLORS_BRIGHTNESS_NORMAL,
+ FF_COLORS_BRIGHTNESS_LIGHT,
+} FFColorsBrightness;
+
+typedef struct FFColorsOptions {
+ FFModuleArgs moduleArgs;
+
+ FFColorsSymbol symbol;
+ uint32_t paddingLeft;
+ FFBlockConfig block;
+ FFColorsBrightness brightness;
+} FFColorsOptions;
+
+static_assert(sizeof(FFColorsOptions) <= FF_OPTION_MAX_SIZE, "FFColorsOptions size exceeds maximum allowed size");
diff --git a/src/modules/command/command.c b/src/modules/command/command.c
new file mode 100644
index 0000000..9890911
--- /dev/null
+++ b/src/modules/command/command.c
@@ -0,0 +1,167 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "modules/command/command.h"
+#include "detection/command/command.h"
+
+bool ffPrintCommand(FFCommandOptions* options) {
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+ const char* error = ffDetectCommand(options, &result);
+
+ if (error) {
+ ffPrintError(FF_COMMAND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (!result.length) {
+ ffPrintError(FF_COMMAND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No result generated");
+ return false;
+ }
+
+ if (options->splitLines) {
+ uint8_t index = 0;
+ char* line = NULL;
+ size_t len = 0;
+ while (ffStrbufGetline(&line, &len, &result)) {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_COMMAND_MODULE_NAME, ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ puts(line);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_COMMAND_MODULE_NAME, ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(line, "result") }));
+ }
+ }
+ } else {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_COMMAND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&result, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_COMMAND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(result, "result") }));
+ }
+ }
+
+ return true;
+}
+
+void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "shell")) {
+ ffStrbufSetJsonVal(&options->shell, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "param")) {
+ ffStrbufSetJsonVal(&options->param, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "text")) {
+ ffStrbufSetJsonVal(&options->text, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "useStdErr")) {
+ options->useStdErr = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "parallel")) {
+ options->parallel = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "splitLines")) {
+ options->splitLines = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_COMMAND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell);
+ yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param);
+ yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text);
+ yyjson_mut_obj_add_bool(doc, module, "useStdErr", options->useStdErr);
+ yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel);
+ yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines);
+}
+
+bool ffGenerateCommandJsonResult(FF_A_UNUSED FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+ const char* error = ffDetectCommand(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ if (!result.length) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No result generated");
+ return false;
+ }
+
+ if (options->splitLines) {
+ yyjson_mut_val* jsonArray = yyjson_mut_obj_add_arr(doc, module, "result");
+ char* line = NULL;
+ size_t len = 0;
+ while (ffStrbufGetline(&line, &len, &result)) {
+ yyjson_mut_arr_add_strncpy(doc, jsonArray, line, len);
+ }
+ } else {
+ yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
+ }
+
+ return true;
+}
+
+void ffInitCommandOptions(FFCommandOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ ffStrbufInitStatic(&options->shell,
+#ifdef _WIN32
+ "cmd.exe"
+#else
+ "/bin/sh"
+#endif
+ );
+ ffStrbufInitStatic(&options->param,
+#ifdef _WIN32
+ "/c"
+#else
+ "-c"
+#endif
+ );
+ ffStrbufInit(&options->text);
+ options->useStdErr = false;
+ options->parallel = true;
+ options->splitLines = false;
+}
+
+void ffDestroyCommandOptions(FFCommandOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->shell);
+ ffStrbufDestroy(&options->param);
+ ffStrbufDestroy(&options->text);
+}
+
+FFModuleBaseInfo ffCommandModuleInfo = {
+ .name = FF_COMMAND_MODULE_NAME,
+ .description = "Run custom shell scripts",
+ .initOptions = (void*) ffInitCommandOptions,
+ .destroyOptions = (void*) ffDestroyCommandOptions,
+ .parseJsonObject = (void*) ffParseCommandJsonObject,
+ .printModule = (void*) ffPrintCommand,
+ .generateJsonResult = (void*) ffGenerateCommandJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCommandJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Command result", "result" },
+ }))
+};
diff --git a/src/modules/command/command.h b/src/modules/command/command.h
new file mode 100644
index 0000000..e0e143e
--- /dev/null
+++ b/src/modules/command/command.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_COMMAND_MODULE_NAME "Command"
+
+bool ffPrepareCommand(FFCommandOptions* options);
+
+bool ffPrintCommand(FFCommandOptions* options);
+void ffInitCommandOptions(FFCommandOptions* options);
+void ffDestroyCommandOptions(FFCommandOptions* options);
+
+extern FFModuleBaseInfo ffCommandModuleInfo;
diff --git a/src/modules/command/option.h b/src/modules/command/option.h
new file mode 100644
index 0000000..6b2108f
--- /dev/null
+++ b/src/modules/command/option.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCommandOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf shell;
+ FFstrbuf param;
+ FFstrbuf text;
+ bool useStdErr;
+ bool parallel;
+ bool splitLines;
+} FFCommandOptions;
+
+static_assert(sizeof(FFCommandOptions) <= FF_OPTION_MAX_SIZE, "FFCommandOptions size exceeds maximum allowed size");
diff --git a/src/modules/cpu/cpu.c b/src/modules/cpu/cpu.c
new file mode 100644
index 0000000..0d3803a
--- /dev/null
+++ b/src/modules/cpu/cpu.c
@@ -0,0 +1,262 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/parsing.h"
+#include "common/temps.h"
+#include "common/frequency.h"
+#include "detection/cpu/cpu.h"
+#include "modules/cpu/cpu.h"
+
+static int sortCores(const FFCPUCore* a, const FFCPUCore* b) {
+ return (int) b->freq - (int) a->freq;
+}
+
+bool ffPrintCPU(FFCPUOptions* options) {
+ bool success = false;
+ FFCPUResult cpu = {
+ .temperature = FF_CPU_TEMP_UNSET,
+ .frequencyMax = 0,
+ .frequencyBase = 0,
+ .name = ffStrbufCreate(),
+ .vendor = ffStrbufCreate(),
+ };
+
+ const char* error = ffDetectCPU(options, &cpu);
+
+ if (error) {
+ ffPrintError(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ } else if (cpu.vendor.length == 0 && cpu.name.length == 0 && cpu.coresOnline <= 1) {
+ ffPrintError(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No CPU detected");
+ } else {
+ FF_STRBUF_AUTO_DESTROY coreTypes = ffStrbufCreate();
+ if (options->showPeCoreCount) {
+ uint32_t typeCount = 0;
+ while (cpu.coreTypes[typeCount].count != 0 && typeCount < ARRAY_SIZE(cpu.coreTypes)) {
+ typeCount++;
+ }
+ if (typeCount > 0) {
+ qsort(cpu.coreTypes, typeCount, sizeof(cpu.coreTypes[0]), (void*) sortCores);
+
+ for (uint32_t i = 0; i < typeCount; i++) {
+ ffStrbufAppendF(&coreTypes, "%s%u", i == 0 ? "" : "+", cpu.coreTypes[i].count);
+ }
+ }
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+
+ if (cpu.packages > 1) {
+ ffStrbufAppendF(&str, "%u x ", cpu.packages);
+ }
+
+ if (cpu.name.length > 0) {
+ ffStrbufAppend(&str, &cpu.name);
+ } else if (cpu.vendor.length > 0) {
+ ffStrbufAppend(&str, &cpu.vendor);
+ ffStrbufAppendS(&str, " CPU");
+ } else {
+ ffStrbufAppendS(&str, "Unknown");
+ }
+
+ if (coreTypes.length > 0) {
+ ffStrbufAppendF(&str, " (%s)", coreTypes.chars);
+ } else if (cpu.coresOnline > 1) {
+ ffStrbufAppendF(&str, " (%u)", cpu.coresOnline);
+ }
+
+ uint32_t freq = cpu.frequencyMax;
+ if (freq == 0) {
+ freq = cpu.frequencyBase;
+ }
+ if (freq > 0) {
+ ffStrbufAppendS(&str, " @ ");
+ ffFreqAppendNum(freq, &str);
+ }
+
+ if (cpu.temperature != FF_CPU_TEMP_UNSET) {
+ ffStrbufAppendS(&str, " - ");
+ ffTempsAppendNum(cpu.temperature, &str, options->tempConfig, &options->moduleArgs);
+ }
+
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY freqBase = ffStrbufCreate();
+ ffFreqAppendNum(cpu.frequencyBase, &freqBase);
+ FF_STRBUF_AUTO_DESTROY freqMax = ffStrbufCreate();
+ ffFreqAppendNum(cpu.frequencyMax, &freqMax);
+
+ FF_STRBUF_AUTO_DESTROY tempStr = ffStrbufCreate();
+ ffTempsAppendNum(cpu.temperature, &tempStr, options->tempConfig, &options->moduleArgs);
+ FF_PRINT_FORMAT_CHECKED(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(cpu.name, "name"),
+ FF_ARG(cpu.vendor, "vendor"),
+ FF_ARG(cpu.coresPhysical, "cores-physical"),
+ FF_ARG(cpu.coresLogical, "cores-logical"),
+ FF_ARG(cpu.coresOnline, "cores-online"),
+ FF_ARG(freqBase, "freq-base"),
+ FF_ARG(freqMax, "freq-max"),
+ FF_ARG(tempStr, "temperature"),
+ FF_ARG(coreTypes, "core-types"),
+ FF_ARG(cpu.packages, "packages"),
+ FF_ARG(cpu.march, "march"),
+ FF_ARG(cpu.numaNodes, "numa-nodes"),
+ }));
+ }
+ success = true;
+ }
+
+ ffStrbufDestroy(&cpu.name);
+ ffStrbufDestroy(&cpu.vendor);
+
+ return success;
+}
+
+void ffParseCPUJsonObject(FFCPUOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffTempsParseJsonObject(key, val, &options->temp, &options->tempConfig)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "tempSensor")) {
+ ffStrbufSetJsonVal(&options->tempSensor, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "freqNdigits")) {
+ ffPrintError(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "modules.CPU.freqNdigits has been moved to display.freq.ndigits");
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showPeCoreCount")) {
+ options->showPeCoreCount = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_CPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCPUJsonConfig(FFCPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffTempsGenerateJsonConfig(doc, module, options->temp, options->tempConfig);
+
+ yyjson_mut_obj_add_bool(doc, module, "showPeCoreCount", options->showPeCoreCount);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "tempSensor", &options->tempSensor);
+}
+
+bool ffGenerateCPUJsonResult(FFCPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFCPUResult cpu = {
+ .temperature = FF_CPU_TEMP_UNSET,
+ .frequencyMax = 0,
+ .frequencyBase = 0,
+ .name = ffStrbufCreate(),
+ .vendor = ffStrbufCreate(),
+ };
+
+ const char* error = ffDetectCPU(options, &cpu);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ } else if (cpu.vendor.length == 0 && cpu.name.length == 0 && cpu.coresOnline <= 1) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No CPU detected");
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "cpu", &cpu.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &cpu.vendor);
+ if (cpu.packages == 0) {
+ yyjson_mut_obj_add_null(doc, obj, "packages");
+ } else {
+ yyjson_mut_obj_add_uint(doc, obj, "packages", cpu.packages);
+ }
+
+ yyjson_mut_val* cores = yyjson_mut_obj_add_obj(doc, obj, "cores");
+ yyjson_mut_obj_add_uint(doc, cores, "physical", cpu.coresPhysical);
+ yyjson_mut_obj_add_uint(doc, cores, "logical", cpu.coresLogical);
+ yyjson_mut_obj_add_uint(doc, cores, "online", cpu.coresOnline);
+
+ yyjson_mut_val* frequency = yyjson_mut_obj_add_obj(doc, obj, "frequency");
+ yyjson_mut_obj_add_uint(doc, frequency, "base", cpu.frequencyBase);
+ yyjson_mut_obj_add_uint(doc, frequency, "max", cpu.frequencyMax);
+
+ yyjson_mut_val* coreTypes = yyjson_mut_obj_add_arr(doc, obj, "coreTypes");
+ for (uint32_t i = 0; i < ARRAY_SIZE(cpu.coreTypes) && cpu.coreTypes[i].count > 0; i++) {
+ yyjson_mut_val* core = yyjson_mut_arr_add_obj(doc, coreTypes);
+ yyjson_mut_obj_add_uint(doc, core, "count", cpu.coreTypes[i].count);
+ yyjson_mut_obj_add_uint(doc, core, "freq", cpu.coreTypes[i].freq);
+ }
+
+ if (cpu.temperature != FF_CPU_TEMP_UNSET) {
+ yyjson_mut_obj_add_real(doc, obj, "temperature", cpu.temperature);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "temperature");
+ }
+
+ if (cpu.march) {
+ yyjson_mut_obj_add_str(doc, obj, "march", cpu.march);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "march");
+ }
+
+ if (cpu.numaNodes > 0) {
+ yyjson_mut_obj_add_uint(doc, obj, "numaNodes", cpu.numaNodes);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "numaNodes");
+ }
+
+ success = true;
+ }
+
+ ffStrbufDestroy(&cpu.name);
+ ffStrbufDestroy(&cpu.vendor);
+
+ return success;
+}
+
+void ffInitCPUOptions(FFCPUOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ ffStrbufInit(&options->tempSensor);
+ options->temp = false;
+ options->tempConfig = (FFColorRangeConfig) { 60, 80 };
+ options->showPeCoreCount = false;
+}
+
+void ffDestroyCPUOptions(FFCPUOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->tempSensor);
+}
+
+FFModuleBaseInfo ffCPUModuleInfo = {
+ .name = FF_CPU_MODULE_NAME,
+ .description = "Print CPU name, frequency, etc.",
+ .initOptions = (void*) ffInitCPUOptions,
+ .destroyOptions = (void*) ffDestroyCPUOptions,
+ .parseJsonObject = (void*) ffParseCPUJsonObject,
+ .printModule = (void*) ffPrintCPU,
+ .generateJsonResult = (void*) ffGenerateCPUJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCPUJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name", "name" },
+ { "Vendor", "vendor" },
+ { "Physical core count", "cores-physical" },
+ { "Logical core count", "cores-logical" },
+ { "Online core count", "cores-online" },
+ { "Base frequency (formatted)", "freq-base" },
+ { "Max frequency (formatted)", "freq-max" },
+ { "Temperature (formatted)", "temperature" },
+ { "Logical core count grouped by frequency", "core-types" },
+ { "Processor package count", "packages" },
+ { "CPU microarchitecture", "march" },
+ { "NUMA node count", "numa-nodes" },
+ }))
+};
diff --git a/src/modules/cpu/cpu.h b/src/modules/cpu/cpu.h
new file mode 100644
index 0000000..33fff6b
--- /dev/null
+++ b/src/modules/cpu/cpu.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CPU_MODULE_NAME "CPU"
+
+bool ffPrintCPU(FFCPUOptions* options);
+void ffInitCPUOptions(FFCPUOptions* options);
+void ffDestroyCPUOptions(FFCPUOptions* options);
+
+extern FFModuleBaseInfo ffCPUModuleInfo;
diff --git a/src/modules/cpu/option.h b/src/modules/cpu/option.h
new file mode 100644
index 0000000..0378635
--- /dev/null
+++ b/src/modules/cpu/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCPUOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf tempSensor;
+ bool temp;
+ FFColorRangeConfig tempConfig;
+ bool showPeCoreCount;
+} FFCPUOptions;
+
+static_assert(sizeof(FFCPUOptions) <= FF_OPTION_MAX_SIZE, "FFCPUOptions size exceeds maximum allowed size");
diff --git a/src/modules/cpucache/cpucache.c b/src/modules/cpucache/cpucache.c
new file mode 100644
index 0000000..7263a51
--- /dev/null
+++ b/src/modules/cpucache/cpucache.c
@@ -0,0 +1,238 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/cpucache/cpucache.h"
+#include "modules/cpucache/cpucache.h"
+
+#define FF_CPUCACHE_DISPLAY_NAME "CPU Cache"
+
+static void printCPUCacheNormal(const FFCPUCacheResult* result, FFCPUCacheOptions* options) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ char levelStr[4] = "L";
+ for (uint32_t i = 0; i < ARRAY_SIZE(result->caches) && result->caches[i].length > 0; i++) {
+ ffStrbufClear(&key);
+ levelStr[1] = (char) ('1' + i);
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufAppendF(&key, "%s (%s)", FF_CPUCACHE_DISPLAY_NAME, levelStr);
+ } else {
+ uint32_t index = i + 1;
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(levelStr, "level"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ ffStrbufClear(&buffer);
+
+ uint32_t sum = 0;
+ FF_LIST_FOR_EACH (FFCPUCache, src, result->caches[i]) {
+ char typeStr = '?';
+ switch (src->type) {
+ case FF_CPU_CACHE_TYPE_DATA:
+ typeStr = 'D';
+ break;
+ case FF_CPU_CACHE_TYPE_INSTRUCTION:
+ typeStr = 'I';
+ break;
+ case FF_CPU_CACHE_TYPE_UNIFIED:
+ typeStr = 'U';
+ break;
+ case FF_CPU_CACHE_TYPE_TRACE:
+ typeStr = 'T';
+ break;
+ }
+ if (buffer.length) {
+ ffStrbufAppendS(&buffer, ", ");
+ }
+ if (src->num > 1) {
+ ffStrbufAppendF(&buffer, "%ux", src->num);
+ }
+ ffSizeAppendNum(src->size, &buffer);
+ ffStrbufAppendF(&buffer, " (%c)", typeStr);
+
+ sum += src->size * src->num;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY buffer2 = ffStrbufCreate();
+ ffSizeAppendNum(sum, &buffer2);
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(buffer, "result"),
+ FF_ARG(buffer2, "sum"),
+ }));
+ }
+ }
+}
+
+static void printCPUCacheCompact(const FFCPUCacheResult* result, FFCPUCacheOptions* options) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ uint64_t sum = 0;
+ for (uint32_t i = 0; i < ARRAY_SIZE(result->caches) && result->caches[i].length > 0; i++) {
+ if (buffer.length) {
+ ffStrbufAppendS(&buffer, ", ");
+ }
+ uint32_t value = 0;
+ FF_LIST_FOR_EACH (FFCPUCache, src, result->caches[i]) {
+ value += src->size * src->num;
+ }
+ ffSizeAppendNum(value, &buffer);
+ ffStrbufAppendF(&buffer, " (L%u)", i + 1);
+ sum += value;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CPUCACHE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY buffer2 = ffStrbufCreate();
+ ffSizeAppendNum(sum, &buffer2);
+ FF_PRINT_FORMAT_CHECKED(FF_CPUCACHE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(buffer, "result"),
+ FF_ARG(buffer2, "sum"),
+ }));
+ }
+}
+
+bool ffPrintCPUCache(FFCPUCacheOptions* options) {
+ bool success = false;
+ FFCPUCacheResult result = {
+ .caches = {
+ ffListCreate(),
+ ffListCreate(),
+ ffListCreate(),
+ ffListCreate(),
+ },
+ };
+
+ const char* error = ffDetectCPUCache(&result);
+
+ if (error) {
+ ffPrintError(FF_CPUCACHE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (!options->compact) {
+ printCPUCacheNormal(&result, options);
+ } else {
+ printCPUCacheCompact(&result, options);
+ }
+ success = true;
+
+exit:
+ ffListDestroy(&result.caches[0]);
+ ffListDestroy(&result.caches[1]);
+ ffListDestroy(&result.caches[2]);
+ ffListDestroy(&result.caches[3]);
+
+ return success;
+}
+
+void ffParseCPUCacheJsonObject(FFCPUCacheOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compact")) {
+ options->compact = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_CPUCACHE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCPUCacheJsonConfig(FFCPUCacheOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "compact", options->compact);
+}
+
+bool ffGenerateCPUCacheJsonResult(FF_A_UNUSED FFCPUCacheOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFCPUCacheResult result = {
+ .caches = {
+ ffListCreate(),
+ ffListCreate(),
+ ffListCreate(),
+ ffListCreate(),
+ },
+ };
+
+ const char* error = ffDetectCPUCache(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ yyjson_mut_val* caches = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ for (uint32_t i = 0; i < ARRAY_SIZE(result.caches) && result.caches[i].length > 0; i++) {
+ yyjson_mut_val* level = yyjson_mut_obj_add_arr(doc, caches, &"l1\0l2\0l3\0l4\0"[i * 3]);
+ FF_LIST_FOR_EACH (FFCPUCache, src, result.caches[i]) {
+ yyjson_mut_val* item = yyjson_mut_arr_add_obj(doc, level);
+ yyjson_mut_obj_add_uint(doc, item, "size", src->size);
+ yyjson_mut_obj_add_uint(doc, item, "num", src->num);
+ const char* typeStr = "unknown";
+ switch (src->type) {
+ case FF_CPU_CACHE_TYPE_DATA:
+ typeStr = "data";
+ break;
+ case FF_CPU_CACHE_TYPE_INSTRUCTION:
+ typeStr = "instruction";
+ break;
+ case FF_CPU_CACHE_TYPE_UNIFIED:
+ typeStr = "unified";
+ break;
+ case FF_CPU_CACHE_TYPE_TRACE:
+ typeStr = "trace";
+ break;
+ }
+ yyjson_mut_obj_add_uint(doc, item, "lineSize", src->lineSize);
+ yyjson_mut_obj_add_str(doc, item, "type", typeStr);
+ }
+ }
+ success = true;
+
+exit:
+ ffListDestroy(&result.caches[0]);
+ ffListDestroy(&result.caches[1]);
+ ffListDestroy(&result.caches[2]);
+ ffListDestroy(&result.caches[3]);
+ return success;
+}
+
+void ffInitCPUCacheOptions(FFCPUCacheOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->compact = false;
+}
+
+void ffDestroyCPUCacheOptions(FFCPUCacheOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCPUCacheModuleInfo = {
+ .name = FF_CPUCACHE_MODULE_NAME,
+ .description = "Print CPU cache sizes",
+ .initOptions = (void*) ffInitCPUCacheOptions,
+ .destroyOptions = (void*) ffDestroyCPUCacheOptions,
+ .parseJsonObject = (void*) ffParseCPUCacheJsonObject,
+ .printModule = (void*) ffPrintCPUCache,
+ .generateJsonResult = (void*) ffGenerateCPUCacheJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCPUCacheJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Separate result", "result" },
+ { "Sum result", "sum" },
+ }))
+};
diff --git a/src/modules/cpucache/cpucache.h b/src/modules/cpucache/cpucache.h
new file mode 100644
index 0000000..5061b44
--- /dev/null
+++ b/src/modules/cpucache/cpucache.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CPUCACHE_MODULE_NAME "CPUCache"
+
+bool ffPrintCPUCache(FFCPUCacheOptions* options);
+void ffInitCPUCacheOptions(FFCPUCacheOptions* options);
+void ffDestroyCPUCacheOptions(FFCPUCacheOptions* options);
+
+extern FFModuleBaseInfo ffCPUCacheModuleInfo;
diff --git a/src/modules/cpucache/option.h b/src/modules/cpucache/option.h
new file mode 100644
index 0000000..186df9a
--- /dev/null
+++ b/src/modules/cpucache/option.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCPUCacheOptions {
+ FFModuleArgs moduleArgs;
+
+ bool compact;
+} FFCPUCacheOptions;
+
+static_assert(sizeof(FFCPUCacheOptions) <= FF_OPTION_MAX_SIZE, "FFCPUCacheOptions size exceeds maximum allowed size");
diff --git a/src/modules/cpuusage/cpuusage.c b/src/modules/cpuusage/cpuusage.c
new file mode 100644
index 0000000..5c01e91
--- /dev/null
+++ b/src/modules/cpuusage/cpuusage.c
@@ -0,0 +1,202 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/strutil.h"
+#include "detection/cpuusage/cpuusage.h"
+#include "modules/cpuusage/cpuusage.h"
+
+#define FF_CPUUSAGE_DISPLAY_NAME "CPU Usage"
+
+bool ffPrintCPUUsage(FFCPUUsageOptions* options) {
+ FF_LIST_AUTO_DESTROY percentages = ffListCreate();
+ const char* error = ffGetCpuUsageResult(options, &percentages);
+
+ if (error) {
+ ffPrintError(FF_CPUUSAGE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ double maxValue = -999, minValue = 999, sumValue = 0;
+ uint32_t maxIndex = 999, minIndex = 999;
+
+ uint32_t index = 0, valueCount = 0;
+ FF_LIST_FOR_EACH (double, percent, percentages) {
+ if (*percent == *percent) {
+ sumValue += *percent;
+
+#if _WIN32
+ // Windows may return values greater than 100%, cap them to 100%
+ if (*percent > 100) {
+ *percent = 100;
+ }
+#endif
+
+ if (*percent > maxValue) {
+ maxValue = *percent;
+ maxIndex = index;
+ }
+ if (*percent < minValue) {
+ minValue = *percent;
+ minIndex = index;
+ }
+ ++valueCount;
+ }
+ ++index;
+ }
+ double avgValue = sumValue / (double) valueCount;
+#if _WIN32
+ // See above comment
+ if (avgValue > 100) {
+ avgValue = 100;
+ }
+#endif
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CPUUSAGE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+ if (!options->separate) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, avgValue, options->percent, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ if (str.length > 0) {
+ ffStrbufAppendC(&str, ' ');
+ }
+ ffPercentAppendNum(&str, avgValue, options->percent, str.length > 0, &options->moduleArgs);
+ }
+ } else {
+ FF_LIST_FOR_EACH (double, percent, percentages) {
+ if (str.length > 0) {
+ ffStrbufAppendC(&str, ' ');
+ }
+ ffPercentAppendNum(&str, *percent, options->percent, false, &options->moduleArgs);
+ }
+ }
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY avgNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&avgNum, avgValue, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY avgBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&avgBar, avgValue, options->percent, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY minNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&minNum, minValue, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY minBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&minBar, minValue, options->percent, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY maxNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&maxNum, maxValue, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY maxBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&maxBar, maxValue, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_CPUUSAGE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(avgNum, "avg"),
+ FF_ARG(maxNum, "max"),
+ FF_ARG(maxIndex, "max-index"),
+ FF_ARG(minNum, "min"),
+ FF_ARG(minIndex, "min-index"),
+ FF_ARG(avgBar, "avg-bar"),
+ FF_ARG(maxBar, "max-bar"),
+ FF_ARG(minBar, "min-bar"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseCPUUsageJsonObject(FFCPUUsageOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "separate")) {
+ options->separate = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "waitTime")) {
+ options->waitTime = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_CPUUSAGE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCPUUsageJsonConfig(FFCPUUsageOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "separate", options->separate);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+
+ yyjson_mut_obj_add_uint(doc, module, "waitTime", options->waitTime);
+}
+
+bool ffGenerateCPUUsageJsonResult(FFCPUUsageOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY percentages = ffListCreate();
+ const char* error = ffGetCpuUsageResult(options, &percentages);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+ yyjson_mut_val* result = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (double, percent, percentages) {
+ yyjson_mut_arr_add_real(doc, result, *percent);
+ }
+
+ return true;
+}
+
+void ffInitCPUUsageOptions(FFCPUUsageOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰓅");
+ options->separate = false;
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+ options->waitTime = 200;
+}
+
+void ffDestroyCPUUsageOptions(FFCPUUsageOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCPUUsageModuleInfo = {
+ .name = FF_CPUUSAGE_MODULE_NAME,
+ .description = "Print CPU usage. Collecting data takes some time",
+ .initOptions = (void*) ffInitCPUUsageOptions,
+ .destroyOptions = (void*) ffDestroyCPUUsageOptions,
+ .parseJsonObject = (void*) ffParseCPUUsageJsonObject,
+ .printModule = (void*) ffPrintCPUUsage,
+ .generateJsonResult = (void*) ffGenerateCPUUsageJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCPUUsageJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "CPU usage (percentage num, average)", "avg" },
+ { "CPU usage (percentage num, maximum)", "max" },
+ { "CPU core index of maximum usage", "max-index" },
+ { "CPU usage (percentage num, minimum)", "min" },
+ { "CPU core index of minimum usage", "min-index" },
+ { "CPU usage (percentage bar, average)", "avg-bar" },
+ { "CPU usage (percentage bar, maximum)", "max-bar" },
+ { "CPU usage (percentage bar, minimum)", "min-bar" },
+ }))
+};
diff --git a/src/modules/cpuusage/cpuusage.h b/src/modules/cpuusage/cpuusage.h
new file mode 100644
index 0000000..efa8b48
--- /dev/null
+++ b/src/modules/cpuusage/cpuusage.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CPUUSAGE_MODULE_NAME "CPUUsage"
+
+void ffPrepareCPUUsage();
+
+bool ffPrintCPUUsage(FFCPUUsageOptions* options);
+void ffInitCPUUsageOptions(FFCPUUsageOptions* options);
+void ffDestroyCPUUsageOptions(FFCPUUsageOptions* options);
+
+extern FFModuleBaseInfo ffCPUUsageModuleInfo;
diff --git a/src/modules/cpuusage/option.h b/src/modules/cpuusage/option.h
new file mode 100644
index 0000000..da9512b
--- /dev/null
+++ b/src/modules/cpuusage/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFCPUUsageOptions {
+ FFModuleArgs moduleArgs;
+
+ bool separate;
+ FFPercentageModuleConfig percent;
+ uint32_t waitTime; // in ms
+} FFCPUUsageOptions;
+
+static_assert(sizeof(FFCPUUsageOptions) <= FF_OPTION_MAX_SIZE, "FFCPUUsageOptions size exceeds maximum allowed size");
diff --git a/src/modules/cursor/cursor.c b/src/modules/cursor/cursor.c
new file mode 100644
index 0000000..0b6f6c7
--- /dev/null
+++ b/src/modules/cursor/cursor.c
@@ -0,0 +1,115 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/cursor/cursor.h"
+#include "modules/cursor/cursor.h"
+
+bool ffPrintCursor(FFCursorOptions* options) {
+ bool success = false;
+ FFCursorResult result;
+ ffStrbufInit(&result.error);
+ ffStrbufInit(&result.theme);
+ ffStrbufInit(&result.size);
+
+ ffDetectCursor(&result);
+
+ if (result.error.length) {
+ ffPrintError(FF_CURSOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", result.error.chars);
+ } else {
+ ffStrbufRemoveIgnCaseEndS(&result.theme, "cursors");
+ ffStrbufRemoveIgnCaseEndS(&result.theme, "cursor");
+ ffStrbufTrimRight(&result.theme, '_');
+ ffStrbufTrimRight(&result.theme, '-');
+ if (result.theme.length == 0) {
+ ffStrbufAppendS(&result.theme, "default");
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_CURSOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result.theme, stdout);
+
+ if (result.size.length > 0 && !ffStrbufEqualS(&result.size, "0")) {
+ printf(" (%spx)", result.size.chars);
+ }
+
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_CURSOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.theme, "theme"),
+ FF_ARG(result.size, "size"),
+ }));
+ }
+
+ success = true;
+ }
+
+ ffStrbufDestroy(&result.error);
+ ffStrbufDestroy(&result.theme);
+ ffStrbufDestroy(&result.size);
+
+ return success;
+}
+
+void ffParseCursorJsonObject(FFCursorOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_CURSOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateCursorJsonConfig(FFCursorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateCursorJsonResult(FF_A_UNUSED FFCursorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFCursorResult result;
+ ffStrbufInit(&result.error);
+ ffStrbufInit(&result.theme);
+ ffStrbufInit(&result.size);
+
+ ffDetectCursor(&result);
+
+ if (result.error.length) {
+ yyjson_mut_obj_add_strbuf(doc, module, "error", &result.error);
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "theme", &result.theme);
+ yyjson_mut_obj_add_strbuf(doc, obj, "size", &result.size);
+ success = true;
+ }
+
+ ffStrbufDestroy(&result.error);
+ ffStrbufDestroy(&result.theme);
+ ffStrbufDestroy(&result.size);
+
+ return success;
+}
+
+void ffInitCursorOptions(FFCursorOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰆿");
+}
+
+void ffDestroyCursorOptions(FFCursorOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCursorModuleInfo = {
+ .name = FF_CURSOR_MODULE_NAME,
+ .description = "Print cursor style name",
+ .initOptions = (void*) ffInitCursorOptions,
+ .destroyOptions = (void*) ffDestroyCursorOptions,
+ .parseJsonObject = (void*) ffParseCursorJsonObject,
+ .printModule = (void*) ffPrintCursor,
+ .generateJsonResult = (void*) ffGenerateCursorJsonResult,
+ .generateJsonConfig = (void*) ffGenerateCursorJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Cursor theme", "theme" },
+ { "Cursor size", "size" },
+ })),
+};
diff --git a/src/modules/cursor/cursor.h b/src/modules/cursor/cursor.h
new file mode 100644
index 0000000..e769569
--- /dev/null
+++ b/src/modules/cursor/cursor.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CURSOR_MODULE_NAME "Cursor"
+
+bool ffPrintCursor(FFCursorOptions* options);
+void ffInitCursorOptions(FFCursorOptions* options);
+void ffDestroyCursorOptions(FFCursorOptions* options);
+
+extern FFModuleBaseInfo ffCursorModuleInfo;
diff --git a/src/modules/cursor/option.h b/src/modules/cursor/option.h
new file mode 100644
index 0000000..1a8e172
--- /dev/null
+++ b/src/modules/cursor/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCursorOptions {
+ FFModuleArgs moduleArgs;
+} FFCursorOptions;
+
+static_assert(sizeof(FFCursorOptions) <= FF_OPTION_MAX_SIZE, "FFCursorOptions size exceeds maximum allowed size");
diff --git a/src/modules/custom/custom.c b/src/modules/custom/custom.c
new file mode 100644
index 0000000..054f370
--- /dev/null
+++ b/src/modules/custom/custom.c
@@ -0,0 +1,45 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/textModifier.h"
+#include "common/strutil.h"
+#include "modules/custom/custom.h"
+
+bool ffPrintCustom(FFCustomOptions* options) {
+ ffPrintFormat(FF_CUSTOM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, 0, ((FFformatarg[]) {}));
+ return true;
+}
+
+void ffGenerateCustomJsonConfig(FFCustomOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+void ffParseCustomJsonObject(FFCustomOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_CUSTOM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffInitCustomOptions(FFCustomOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ ffStrbufSetStatic(&options->moduleArgs.key, " ");
+}
+
+void ffDestroyCustomOptions(FFCustomOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffCustomModuleInfo = {
+ .name = FF_CUSTOM_MODULE_NAME,
+ .description = "Print a custom string, with or without key",
+ .initOptions = (void*) ffInitCustomOptions,
+ .destroyOptions = (void*) ffDestroyCustomOptions,
+ .parseJsonObject = (void*) ffParseCustomJsonObject,
+ .printModule = (void*) ffPrintCustom,
+ .generateJsonConfig = (void*) ffGenerateCustomJsonConfig,
+};
diff --git a/src/modules/custom/custom.h b/src/modules/custom/custom.h
new file mode 100644
index 0000000..628a621
--- /dev/null
+++ b/src/modules/custom/custom.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_CUSTOM_MODULE_NAME "Custom"
+
+bool ffPrintCustom(FFCustomOptions* options);
+void ffInitCustomOptions(FFCustomOptions* options);
+void ffDestroyCustomOptions(FFCustomOptions* options);
+
+extern FFModuleBaseInfo ffCustomModuleInfo;
diff --git a/src/modules/custom/option.h b/src/modules/custom/option.h
new file mode 100644
index 0000000..2a7e5e4
--- /dev/null
+++ b/src/modules/custom/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFCustomOptions {
+ FFModuleArgs moduleArgs;
+} FFCustomOptions;
+
+static_assert(sizeof(FFCustomOptions) <= FF_OPTION_MAX_SIZE, "FFCustomOptions size exceeds maximum allowed size");
diff --git a/src/modules/datetime/datetime.c b/src/modules/datetime/datetime.c
new file mode 100644
index 0000000..e902fff
--- /dev/null
+++ b/src/modules/datetime/datetime.c
@@ -0,0 +1,189 @@
+#include "common/time.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "modules/datetime/datetime.h"
+
+#include <time.h>
+
+#pragma GCC diagnostic ignored "-Wformat" // warning: unknown conversion type character 'F' in format
+
+#define FF_DATETIME_DISPLAY_NAME "Date & Time"
+
+typedef struct FFDateTimeResult {
+ // Examples for 21.02.2022 - 15:18:37
+ uint16_t year; // 2022
+ uint8_t yearShort; // 22
+ uint8_t month; // 2
+ char monthPretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 02
+ char monthName[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // February
+ char monthNameShort[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // Feb
+ uint8_t week; // 8
+ char weekday[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // Monday
+ char weekdayShort[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // Mon
+ uint16_t dayInYear; // 52
+ uint8_t dayInMonth; // 21
+ uint8_t dayInWeek; // 1
+ char dayPretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 01
+ uint8_t hour; // 15
+ char hourPretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 15
+ uint8_t hour12; // 3
+ char hour12Pretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 03
+ uint8_t minute; // 18
+ char minutePretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 18
+ uint8_t second; // 37
+ char secondPretty[FASTFETCH_STRBUF_DEFAULT_ALLOC]; // 37
+ char offsetFromUtc[FASTFETCH_STRBUF_DEFAULT_ALLOC];
+ char timezoneName[FASTFETCH_STRBUF_DEFAULT_ALLOC];
+ char amPm[FASTFETCH_STRBUF_DEFAULT_ALLOC];
+} FFDateTimeResult;
+
+static void printDateTimeFormat(struct tm* tm, const FFModuleArgs* moduleArgs) {
+ FFDateTimeResult result;
+
+ result.year = (uint16_t) (tm->tm_year + 1900);
+ result.yearShort = (uint8_t) (result.year % 100);
+ result.month = (uint8_t) (tm->tm_mon + 1);
+ strftime(result.monthPretty, sizeof(result.monthPretty), "%m", tm);
+ strftime(result.monthName, sizeof(result.monthName), "%B", tm);
+ strftime(result.monthNameShort, sizeof(result.monthNameShort), "%b", tm);
+ result.week = (uint8_t) (tm->tm_yday / 7 + 1);
+ strftime(result.weekday, sizeof(result.weekday), "%A", tm);
+ strftime(result.weekdayShort, sizeof(result.weekdayShort), "%a", tm);
+ result.dayInYear = (uint8_t) (tm->tm_yday + 1);
+ result.dayInMonth = (uint8_t) tm->tm_mday;
+ result.dayInWeek = tm->tm_wday == 0 ? 7 : (uint8_t) tm->tm_wday;
+ strftime(result.dayPretty, sizeof(result.dayPretty), "%d", tm);
+ result.hour = (uint8_t) tm->tm_hour;
+ strftime(result.hourPretty, sizeof(result.hourPretty), "%H", tm);
+ result.hour12 = (uint8_t) (result.hour % 12);
+ strftime(result.hour12Pretty, sizeof(result.hour12Pretty), "%I", tm);
+ result.minute = (uint8_t) tm->tm_min;
+ strftime(result.minutePretty, sizeof(result.minutePretty), "%M", tm);
+ result.second = (uint8_t) tm->tm_sec;
+ strftime(result.secondPretty, sizeof(result.secondPretty), "%S", tm);
+ strftime(result.offsetFromUtc, sizeof(result.offsetFromUtc), "%z", tm);
+ strftime(result.timezoneName, sizeof(result.timezoneName), "%Z", tm);
+ strftime(result.amPm, sizeof(result.amPm), "%p", tm);
+
+ FF_PRINT_FORMAT_CHECKED(
+ FF_DATETIME_DISPLAY_NAME,
+ 0,
+ moduleArgs,
+ FF_PRINT_TYPE_DEFAULT,
+ ((FFformatarg[]){
+ FF_ARG(result.year, "year"), // 1
+ FF_ARG(result.yearShort, "year-short"), // 2
+ FF_ARG(result.month, "month"), // 3
+ FF_ARG(result.monthPretty, "month-pretty"), // 4
+ FF_ARG(result.monthName, "month-name"), // 5
+ FF_ARG(result.monthNameShort, "month-name-short"), // 6
+ FF_ARG(result.week, "week"), // 7
+ FF_ARG(result.weekday, "weekday"), // 8
+ FF_ARG(result.weekdayShort, "weekday-short"), // 9
+ FF_ARG(result.dayInYear, "day-in-year"), // 10
+ FF_ARG(result.dayInMonth, "day-in-month"), // 11
+ FF_ARG(result.dayInWeek, "day-in-week"), // 12
+ FF_ARG(result.hour, "hour"), // 13
+ FF_ARG(result.hourPretty, "hour-pretty"), // 14
+ FF_ARG(result.hour12, "hour-12"), // 15
+ FF_ARG(result.hour12Pretty, "hour-12-pretty"), // 16
+ FF_ARG(result.minute, "minute"), // 17
+ FF_ARG(result.minutePretty, "minute-pretty"), // 18
+ FF_ARG(result.second, "second"), // 19
+ FF_ARG(result.secondPretty, "second-pretty"), // 20
+ FF_ARG(result.offsetFromUtc, "offset-from-utc"), // 21
+ FF_ARG(result.timezoneName, "timezone-name"), // 22
+ FF_ARG(result.dayPretty, "day-pretty"), // 23
+ FF_ARG(result.amPm, "am-pm"), // 24
+ }));
+}
+
+bool ffPrintDateTime(FFDateTimeOptions* options) {
+ uint64_t msNow = ffTimeGetNow();
+ time_t sNow = (time_t) (msNow / 1000);
+ struct tm* tm = localtime(&sNow);
+
+ if (options->moduleArgs.outputFormat.length > 0) {
+ printDateTimeFormat(tm, &options->moduleArgs);
+ return true;
+ }
+
+ char buffer[32];
+ if (strftime(buffer, ARRAY_SIZE(buffer), "%F %T", tm) == 0) // yyyy-MM-dd HH:mm:ss
+ {
+ ffPrintError(FF_DATETIME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "strftime() failed");
+ return false;
+ }
+
+ ffPrintLogoAndKey(FF_DATETIME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ puts(buffer);
+ return true;
+}
+
+void ffParseDateTimeJsonObject(FFDateTimeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_DATETIME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDateTimeJsonConfig(FFDateTimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateDateTimeJsonResult(FF_A_UNUSED FFDateTimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ yyjson_mut_obj_add_strcpy(doc, module, "result", ffTimeToFullStr(ffTimeGetNow()));
+ return true;
+}
+
+void ffInitDateTimeOptions(FFDateTimeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyDateTimeOptions(FFDateTimeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffDateTimeModuleInfo = {
+ .name = FF_DATETIME_MODULE_NAME,
+ .description = "Print the current date and time",
+ .initOptions = (void*) ffInitDateTimeOptions,
+ .destroyOptions = (void*) ffDestroyDateTimeOptions,
+ .parseJsonObject = (void*) ffParseDateTimeJsonObject,
+ .printModule = (void*) ffPrintDateTime,
+ .generateJsonResult = (void*) ffGenerateDateTimeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateDateTimeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){
+ { "Year", "year" },
+ { "Last two digits of year", "year-short" },
+ { "Month", "month" },
+ { "Month with leading zero", "month-pretty" },
+ { "Month name", "month-name" },
+ { "Month name short", "month-name-short" },
+ { "Week number on year", "week" },
+ { "Weekday", "weekday" },
+ { "Weekday short", "weekday-short" },
+ { "Day in year", "day-in-year" },
+ { "Day in month", "day-in-month" },
+ { "Day in week", "day-in-week" },
+ { "Hour", "hour" },
+ { "Hour with leading zero", "hour-pretty" },
+ { "Hour 12h format", "hour-12" },
+ { "Hour 12h format with leading zero", "hour-12-pretty" },
+ { "Minute", "minute" },
+ { "Minute with leading zero", "minute-pretty" },
+ { "Second", "second" },
+ { "Second with leading zero", "second-pretty" },
+ { "Offset from UTC in the ISO 8601 format", "offset-from-utc" },
+ { "Locale-dependent timezone name or abbreviation", "timezone-name" },
+ { "Day in month with leading zero", "day-pretty" },
+ { "AM or PM", "am-pm" },
+ }))
+};
diff --git a/src/modules/datetime/datetime.h b/src/modules/datetime/datetime.h
new file mode 100644
index 0000000..29c6f54
--- /dev/null
+++ b/src/modules/datetime/datetime.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DATETIME_MODULE_NAME "DateTime"
+
+bool ffPrintDateTime(FFDateTimeOptions* options);
+void ffInitDateTimeOptions(FFDateTimeOptions* options);
+void ffDestroyDateTimeOptions(FFDateTimeOptions* options);
+
+extern FFModuleBaseInfo ffDateTimeModuleInfo;
diff --git a/src/modules/datetime/option.h b/src/modules/datetime/option.h
new file mode 100644
index 0000000..a060b01
--- /dev/null
+++ b/src/modules/datetime/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFDateTimeOptions {
+ FFModuleArgs moduleArgs;
+} FFDateTimeOptions;
+
+static_assert(sizeof(FFDateTimeOptions) <= FF_OPTION_MAX_SIZE, "FFDateTimeOptions size exceeds maximum allowed size");
diff --git a/src/modules/de/de.c b/src/modules/de/de.c
new file mode 100644
index 0000000..d66d205
--- /dev/null
+++ b/src/modules/de/de.c
@@ -0,0 +1,98 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/displayserver/displayserver.h"
+#include "detection/de/de.h"
+#include "modules/de/de.h"
+
+bool ffPrintDE(FFDEOptions* options) {
+ const FFDisplayServerResult* result = ffConnectDisplayServer();
+
+ if (result->dePrettyName.length == 0) {
+ ffPrintError(FF_DE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No DE found");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY version = ffStrbufCreate();
+ ffDetectDEVersion(&result->dePrettyName, &version, options);
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_DE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ ffStrbufWriteTo(&result->dePrettyName, stdout);
+
+ if (version.length > 0) {
+ putchar(' ');
+ ffStrbufWriteTo(&version, stdout);
+ }
+
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_DE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(result->deProcessName, "process-name"), FF_ARG(result->dePrettyName, "pretty-name"), FF_ARG(version, "version") }));
+ }
+
+ return true;
+}
+
+void ffParseDEJsonObject(FFDEOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "slowVersionDetection")) {
+ ffPrintError(FF_DE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Key `slowVersionDetection` is deprecated, it's always true");
+ continue;
+ }
+
+ ffPrintError(FF_DE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDEJsonConfig(FFDEOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateDEJsonResult(FF_A_UNUSED FFDEOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFDisplayServerResult* result = ffConnectDisplayServer();
+
+ if (result->dePrettyName.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No DE found");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY version = ffStrbufCreate();
+ ffDetectDEVersion(&result->dePrettyName, &version, options);
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "processName", &result->deProcessName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "prettyName", &result->dePrettyName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &version);
+ return true;
+}
+
+void ffInitDEOptions(FFDEOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyDEOptions(FFDEOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffDEModuleInfo = {
+ .name = FF_DE_MODULE_NAME,
+ .description = "Print desktop environment name",
+ .initOptions = (void*) ffInitDEOptions,
+ .destroyOptions = (void*) ffDestroyDEOptions,
+ .parseJsonObject = (void*) ffParseDEJsonObject,
+ .printModule = (void*) ffPrintDE,
+ .generateJsonResult = (void*) ffGenerateDEJsonResult,
+ .generateJsonConfig = (void*) ffGenerateDEJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "DE process name", "process-name" },
+ { "DE pretty name", "pretty-name" },
+ { "DE version", "version" },
+ }))
+};
diff --git a/src/modules/de/de.h b/src/modules/de/de.h
new file mode 100644
index 0000000..8aaf642
--- /dev/null
+++ b/src/modules/de/de.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DE_MODULE_NAME "DE"
+
+bool ffPrintDE(FFDEOptions* options);
+void ffInitDEOptions(FFDEOptions* options);
+void ffDestroyDEOptions(FFDEOptions* options);
+
+extern FFModuleBaseInfo ffDEModuleInfo;
diff --git a/src/modules/de/option.h b/src/modules/de/option.h
new file mode 100644
index 0000000..c6113ad
--- /dev/null
+++ b/src/modules/de/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFDEOptions {
+ FFModuleArgs moduleArgs;
+} FFDEOptions;
+
+static_assert(sizeof(FFDEOptions) <= FF_OPTION_MAX_SIZE, "FFDEOptions size exceeds maximum allowed size");
diff --git a/src/modules/disk/disk.c b/src/modules/disk/disk.c
new file mode 100644
index 0000000..cb06d07
--- /dev/null
+++ b/src/modules/disk/disk.c
@@ -0,0 +1,490 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/size.h"
+#include "common/time.h"
+#include "detection/disk/disk.h"
+#include "modules/disk/disk.h"
+
+#ifndef _WIN32
+ #include <fnmatch.h>
+#endif
+
+#pragma GCC diagnostic ignored "-Wsign-conversion"
+
+static void printDisk(FFDiskOptions* options, const FFDisk* disk, uint32_t index) {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufSetF(&key, "%s (%s)", FF_DISK_MODULE_NAME, disk->mountpoint.chars);
+ } else {
+ FF_STRBUF_AUTO_DESTROY mountpointLink = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY nameLink = ffStrbufCreate();
+#ifdef __linux__
+ if (getenv("WSL_DISTRO_NAME") != NULL && getenv("WT_SESSION") != NULL) {
+ if (ffStrbufEqualS(&disk->filesystem, "9p") && ffStrbufStartsWithS(&disk->mountpoint, "/mnt/")) {
+ ffStrbufSetF(&mountpointLink, "\e]8;;file:///%c:/\e\\%s\e]8;;\e\\", disk->mountpoint.chars[5], disk->mountpoint.chars);
+ ffStrbufSetF(&nameLink, "\e]8;;file:///%c:/\e\\%s\e]8;;\e\\", disk->mountpoint.chars[5], disk->name.chars);
+ } else {
+ ffStrbufSetF(&mountpointLink, "\e]8;;file:////wsl.localhost/%s%s\e\\%s\e]8;;\e\\", getenv("WSL_DISTRO_NAME"), disk->mountpoint.chars, disk->mountpoint.chars);
+ ffStrbufSetF(&nameLink, "\e]8;;file:////wsl.localhost/%s%s\e\\%s\e]8;;\e\\", getenv("WSL_DISTRO_NAME"), disk->mountpoint.chars, disk->name.chars);
+ }
+ } else
+#endif
+ {
+ ffStrbufSetF(&mountpointLink, "\e]8;;file://%s\e\\%s\e]8;;\e\\", disk->mountpoint.chars, disk->mountpoint.chars);
+ ffStrbufSetF(&nameLink, "\e]8;;file://%s\e\\%s\e]8;;\e\\", disk->mountpoint.chars, disk->name.chars);
+ }
+
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(disk->mountpoint, "mountpoint"),
+ FF_ARG(disk->name, "name"),
+ FF_ARG(disk->mountFrom, "mount-from"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ FF_ARG(index, "index"),
+ FF_ARG(disk->filesystem, "filesystem"),
+ FF_ARG(mountpointLink, "mountpoint-link"),
+ FF_ARG(nameLink, "name-link"),
+ }));
+ }
+
+ FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
+ ffSizeAppendNum(disk->bytesUsed, &usedPretty);
+
+ FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
+ ffSizeAppendNum(disk->bytesTotal, &totalPretty);
+
+ double bytesPercentage = disk->bytesTotal > 0 ? (double) disk->bytesUsed / (double) disk->bytesTotal * 100.0 : 0;
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+
+ if (disk->bytesTotal > 0) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, bytesPercentage, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppendF(&str, "%s / %s ", usedPretty.chars, totalPretty.chars);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&str, bytesPercentage, options->percent, str.length > 0, &options->moduleArgs);
+ ffStrbufAppendC(&str, ' ');
+ }
+ } else {
+ ffStrbufAppendS(&str, "Unknown ");
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ if (disk->filesystem.length) {
+ ffStrbufAppendF(&str, "- %s ", disk->filesystem.chars);
+ }
+
+ ffStrbufAppendC(&str, '[');
+ if (disk->type & FF_DISK_VOLUME_TYPE_EXTERNAL_BIT) {
+ ffStrbufAppendS(&str, "External, ");
+ }
+ if (disk->type & FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT) {
+ ffStrbufAppendS(&str, "Subvolume, ");
+ }
+ if (disk->type & FF_DISK_VOLUME_TYPE_HIDDEN_BIT) {
+ ffStrbufAppendS(&str, "Hidden, ");
+ }
+ if (disk->type & FF_DISK_VOLUME_TYPE_READONLY_BIT) {
+ ffStrbufAppendS(&str, "Read-only, ");
+ }
+ if (str.chars[str.length - 1] == '[') {
+ ffStrbufSubstrBefore(&str, str.length - 1);
+ } else {
+ ffStrbufTrimRight(&str, ' ');
+ str.chars[str.length - 1] = ']';
+ }
+ }
+
+ ffStrbufTrimRight(&str, ' ');
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY bytesPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&bytesPercentageNum, bytesPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY bytesPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&bytesPercentageBar, bytesPercentage, options->percent, &options->moduleArgs);
+ }
+
+ double filesPercentage = disk->filesTotal > 0 ? ((double) disk->filesUsed / (double) disk->filesTotal) * 100.0 : 0;
+ FF_STRBUF_AUTO_DESTROY filesPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&filesPercentageNum, filesPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY filesPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&filesPercentageBar, filesPercentage, options->percent, &options->moduleArgs);
+ }
+
+ bool isExternal = !!(disk->type & FF_DISK_VOLUME_TYPE_EXTERNAL_BIT);
+ bool isHidden = !!(disk->type & FF_DISK_VOLUME_TYPE_HIDDEN_BIT);
+ bool isReadOnly = !!(disk->type & FF_DISK_VOLUME_TYPE_READONLY_BIT);
+
+ FF_STRBUF_AUTO_DESTROY freePretty = ffStrbufCreate();
+ ffSizeAppendNum(disk->bytesFree, &freePretty);
+
+ FF_STRBUF_AUTO_DESTROY availPretty = ffStrbufCreate();
+ ffSizeAppendNum(disk->bytesAvailable, &availPretty);
+
+ uint64_t now = ffTimeGetNow();
+ uint64_t duration = now - disk->createTime;
+ uint32_t milliseconds = (uint32_t) (duration % 1000);
+ duration /= 1000;
+ uint32_t seconds = (uint32_t) (duration % 60);
+ duration /= 60;
+ uint32_t minutes = (uint32_t) (duration % 60);
+ duration /= 60;
+ uint32_t hours = (uint32_t) (duration % 24);
+ duration /= 24;
+ uint32_t days = (uint32_t) duration;
+
+ FFTimeGetAgeResult age = ffTimeGetAge(disk->createTime, now);
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(usedPretty, "size-used"),
+ FF_ARG(totalPretty, "size-total"),
+ FF_ARG(bytesPercentageNum, "size-percentage"),
+ FF_ARG(disk->filesUsed, "files-used"),
+ FF_ARG(disk->filesTotal, "files-total"),
+ FF_ARG(filesPercentageNum, "files-percentage"),
+ FF_ARG(isExternal, "is-external"),
+ FF_ARG(isHidden, "is-hidden"),
+ FF_ARG(disk->filesystem, "filesystem"),
+ FF_ARG(disk->name, "name"),
+ FF_ARG(isReadOnly, "is-readonly"),
+ { FF_ARG_TYPE_STRING, ffTimeToShortStr(disk->createTime), "create-time" },
+ FF_ARG(bytesPercentageBar, "size-percentage-bar"),
+ FF_ARG(filesPercentageBar, "files-percentage-bar"),
+ FF_ARG(days, "days"),
+ FF_ARG(hours, "hours"),
+ FF_ARG(minutes, "minutes"),
+ FF_ARG(seconds, "seconds"),
+ FF_ARG(milliseconds, "milliseconds"),
+ FF_ARG(disk->mountpoint, "mountpoint"),
+ FF_ARG(disk->mountFrom, "mount-from"),
+ FF_ARG(age.years, "years"),
+ FF_ARG(age.daysOfYear, "days-of-year"),
+ FF_ARG(age.yearsFraction, "years-fraction"),
+ FF_ARG(freePretty, "size-free"),
+ FF_ARG(availPretty, "size-available"),
+ }));
+ }
+}
+
+bool ffPrintDisk(FFDiskOptions* options) {
+ FF_LIST_AUTO_DESTROY disks = ffListCreate();
+ const char* error = ffDetectDisks(options, &disks);
+
+ if (error) {
+ ffPrintError(FF_DISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (disks.length == 0) {
+ ffPrintError(FF_DISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No disks found");
+ return false;
+ }
+
+ uint32_t index = 0;
+ FF_LIST_FOR_EACH (FFDisk, disk, disks) {
+ if (__builtin_expect(options->folders.length == 0, 1) && (disk->type & ~options->showTypes)) {
+ continue;
+ }
+
+ printDisk(options, disk, ++index);
+ }
+
+ FF_LIST_FOR_EACH (FFDisk, disk, disks) {
+ ffStrbufDestroy(&disk->mountFrom);
+ ffStrbufDestroy(&disk->mountpoint);
+ ffStrbufDestroy(&disk->filesystem);
+ ffStrbufDestroy(&disk->name);
+ }
+
+ return true;
+}
+
+static bool setSeparatedList(FFstrbuf* strbuf, yyjson_val* val, char separator) {
+ if (yyjson_is_str(val)) {
+ ffStrbufSetJsonVal(strbuf, val);
+ return true;
+ }
+ if (yyjson_is_arr(val)) {
+ ffStrbufClear(strbuf);
+ yyjson_val* elem;
+ size_t eidx, emax;
+ yyjson_arr_foreach (val, eidx, emax, elem) {
+ if (yyjson_is_str(elem)) {
+ if (strbuf->length > 0) {
+ ffStrbufAppendC(strbuf, separator);
+ }
+ ffStrbufAppendJsonVal(strbuf, elem);
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+void ffParseDiskJsonObject(FFDiskOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "folders")) {
+ setSeparatedList(&options->folders, val, FF_DISK_FOLDER_SEPARATOR);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "hideFolders")) {
+ setSeparatedList(&options->hideFolders, val, FF_DISK_FOLDER_SEPARATOR);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "hideFS")) {
+ setSeparatedList(&options->hideFS, val, ':');
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showRegular")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_REGULAR_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_REGULAR_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showExternal")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_EXTERNAL_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_EXTERNAL_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showHidden")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_HIDDEN_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_HIDDEN_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showSubvolumes")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showReadOnly")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_READONLY_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_READONLY_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showUnknown")) {
+ if (yyjson_get_bool(val)) {
+ options->showTypes |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT;
+ } else {
+ options->showTypes &= ~FF_DISK_VOLUME_TYPE_UNKNOWN_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "useAvailable")) {
+ if (yyjson_get_bool(val)) {
+ options->calcType = FF_DISK_CALC_TYPE_AVAILABLE;
+ } else {
+ options->calcType = FF_DISK_CALC_TYPE_FREE;
+ }
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_DISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDiskJsonConfig(FFDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "showRegular", !!(options->showTypes & FF_DISK_VOLUME_TYPE_REGULAR_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showExternal", !!(options->showTypes & FF_DISK_VOLUME_TYPE_EXTERNAL_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showHidden", !!(options->showTypes & FF_DISK_VOLUME_TYPE_HIDDEN_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showSubvolumes", !!(options->showTypes & FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showReadOnly", !!(options->showTypes & FF_DISK_VOLUME_TYPE_READONLY_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showUnknown", !!(options->showTypes & FF_DISK_VOLUME_TYPE_UNKNOWN_BIT));
+
+ yyjson_mut_obj_add_strbuf(doc, module, "folders", &options->folders);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "hideFolders", &options->hideFolders);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "hideFS", &options->hideFS);
+
+ yyjson_mut_obj_add_bool(doc, module, "useAvailable", options->calcType == FF_DISK_CALC_TYPE_AVAILABLE);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateDiskJsonResult(FFDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY disks = ffListCreate();
+ const char* error = ffDetectDisks(options, &disks);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFDisk, item, disks) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+
+ yyjson_mut_val* bytes = yyjson_mut_obj_add_obj(doc, obj, "bytes");
+ yyjson_mut_obj_add_uint(doc, bytes, "available", item->bytesAvailable);
+ yyjson_mut_obj_add_uint(doc, bytes, "free", item->bytesFree);
+ yyjson_mut_obj_add_uint(doc, bytes, "total", item->bytesTotal);
+ yyjson_mut_obj_add_uint(doc, bytes, "used", item->bytesUsed);
+
+ yyjson_mut_val* files = yyjson_mut_obj_add_obj(doc, obj, "files");
+ if (item->filesTotal == 0 && item->filesUsed == 0) {
+ yyjson_mut_obj_add_null(doc, files, "total");
+ yyjson_mut_obj_add_null(doc, files, "used");
+ } else {
+ yyjson_mut_obj_add_uint(doc, files, "total", item->filesTotal);
+ yyjson_mut_obj_add_uint(doc, files, "used", item->filesUsed);
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "filesystem", &item->filesystem);
+ yyjson_mut_obj_add_strbuf(doc, obj, "mountpoint", &item->mountpoint);
+ yyjson_mut_obj_add_strbuf(doc, obj, "mountFrom", &item->mountFrom);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_val* typeArr = yyjson_mut_obj_add_arr(doc, obj, "volumeType");
+ if (item->type & FF_DISK_VOLUME_TYPE_REGULAR_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "Regular");
+ }
+ if (item->type & FF_DISK_VOLUME_TYPE_EXTERNAL_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "External");
+ }
+ if (item->type & FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "Subvolume");
+ }
+ if (item->type & FF_DISK_VOLUME_TYPE_HIDDEN_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "Hidden");
+ }
+ if (item->type & FF_DISK_VOLUME_TYPE_READONLY_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "Read-only");
+ }
+ if (item->type & FF_DISK_VOLUME_TYPE_UNKNOWN_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "Unknown");
+ }
+
+ const char* pstr = ffTimeToFullStr(item->createTime);
+ if (*pstr) {
+ yyjson_mut_obj_add_strcpy(doc, obj, "createTime", pstr);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "createTime");
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFDisk, item, disks) {
+ ffStrbufDestroy(&item->mountpoint);
+ ffStrbufDestroy(&item->mountFrom);
+ ffStrbufDestroy(&item->filesystem);
+ ffStrbufDestroy(&item->name);
+ }
+
+ return true;
+}
+
+void ffInitDiskOptions(FFDiskOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ ffStrbufInit(&options->folders);
+#if _WIN32 || __APPLE__ || __ANDROID__
+ ffStrbufInit(&options->hideFolders);
+#else
+ ffStrbufInitS(&options->hideFolders, "/efi:/boot:/boot/*");
+#endif
+ ffStrbufInit(&options->hideFS);
+ options->showTypes = FF_DISK_VOLUME_TYPE_REGULAR_BIT | FF_DISK_VOLUME_TYPE_EXTERNAL_BIT | FF_DISK_VOLUME_TYPE_READONLY_BIT;
+ options->calcType = FF_DISK_CALC_TYPE_FREE;
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+}
+
+void ffDestroyDiskOptions(FFDiskOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->folders);
+ ffStrbufDestroy(&options->hideFolders);
+ ffStrbufDestroy(&options->hideFS);
+}
+
+FFModuleBaseInfo ffDiskModuleInfo = {
+ .name = FF_DISK_MODULE_NAME,
+ .description = "Print partitions, space usage, file system, etc",
+ .initOptions = (void*) ffInitDiskOptions,
+ .destroyOptions = (void*) ffDestroyDiskOptions,
+ .parseJsonObject = (void*) ffParseDiskJsonObject,
+ .printModule = (void*) ffPrintDisk,
+ .generateJsonResult = (void*) ffGenerateDiskJsonResult,
+ .generateJsonConfig = (void*) ffGenerateDiskJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Size used", "size-used" },
+ { "Size total", "size-total" },
+ { "Size percentage num", "size-percentage" },
+ { "Files used", "files-used" },
+ { "Files total", "files-total" },
+ { "Files percentage num", "files-percentage" },
+ { "True if external volume", "is-external" },
+ { "True if hidden volume", "is-hidden" },
+ { "Filesystem", "filesystem" },
+ { "Label / name", "name" },
+ { "True if read-only", "is-readonly" },
+ { "Create time in local timezone", "create-time" },
+ { "Size percentage bar", "size-percentage-bar" },
+ { "Files percentage bar", "files-percentage-bar" },
+ { "Days after creation", "days" },
+ { "Hours after creation", "hours" },
+ { "Minutes after creation", "minutes" },
+ { "Seconds after creation", "seconds" },
+ { "Milliseconds after creation", "milliseconds" },
+ { "Mount point / drive letter", "mountpoint" },
+ { "Mount from (device path)", "mount-from" },
+ { "Years integer after creation", "years" },
+ { "Days of year after creation", "days-of-year" },
+ { "Years fraction after creation", "years-fraction" },
+ { "Size free", "size-free" },
+ { "Size available", "size-available" },
+ }))
+};
diff --git a/src/modules/disk/disk.h b/src/modules/disk/disk.h
new file mode 100644
index 0000000..5f7820a
--- /dev/null
+++ b/src/modules/disk/disk.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DISK_MODULE_NAME "Disk"
+
+bool ffPrintDisk(FFDiskOptions* options);
+void ffInitDiskOptions(FFDiskOptions* options);
+void ffDestroyDiskOptions(FFDiskOptions* options);
+
+extern FFModuleBaseInfo ffDiskModuleInfo;
diff --git a/src/modules/disk/option.h b/src/modules/disk/option.h
new file mode 100644
index 0000000..b639db2
--- /dev/null
+++ b/src/modules/disk/option.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef enum FF_A_PACKED FFDiskVolumeType {
+ FF_DISK_VOLUME_TYPE_NONE = 0,
+ FF_DISK_VOLUME_TYPE_REGULAR_BIT = 1 << 0,
+ FF_DISK_VOLUME_TYPE_HIDDEN_BIT = 1 << 1,
+ FF_DISK_VOLUME_TYPE_EXTERNAL_BIT = 1 << 2,
+ FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT = 1 << 3,
+ FF_DISK_VOLUME_TYPE_UNKNOWN_BIT = 1 << 4,
+ FF_DISK_VOLUME_TYPE_READONLY_BIT = 1 << 5,
+ FF_DISK_VOLUME_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFDiskVolumeType;
+
+typedef enum FF_A_PACKED FFDiskCalcType {
+ FF_DISK_CALC_TYPE_FREE,
+ FF_DISK_CALC_TYPE_AVAILABLE,
+} FFDiskCalcType;
+
+typedef struct FFDiskOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf folders;
+ FFstrbuf hideFolders;
+ FFstrbuf hideFS;
+ FFDiskVolumeType showTypes;
+ FFDiskCalcType calcType;
+ FFPercentageModuleConfig percent;
+} FFDiskOptions;
+
+static_assert(sizeof(FFDiskOptions) <= FF_OPTION_MAX_SIZE, "FFDiskOptions size exceeds maximum allowed size");
diff --git a/src/modules/diskio/diskio.c b/src/modules/diskio/diskio.c
new file mode 100644
index 0000000..3ff31fa
--- /dev/null
+++ b/src/modules/diskio/diskio.c
@@ -0,0 +1,193 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/diskio/diskio.h"
+#include "modules/diskio/diskio.h"
+
+#define FF_DISKIO_DISPLAY_NAME "Disk IO"
+
+static int sortDevices(const FFDiskIOResult* left, const FFDiskIOResult* right) {
+ return ffStrbufComp(&left->name, &right->name);
+}
+
+static void formatKey(const FFDiskIOOptions* options, FFDiskIOResult* dev, uint32_t index, FFstrbuf* key) {
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufSetF(key, FF_DISKIO_DISPLAY_NAME " (%s)", dev->name.length ? dev->name.chars : dev->devPath.chars);
+ } else {
+ ffStrbufClear(key);
+ FF_PARSE_FORMAT_STRING_CHECKED(key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(dev->name, "name"),
+ FF_ARG(dev->devPath, "dev-path"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+}
+
+bool ffPrintDiskIO(FFDiskIOOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectDiskIO(&result, options);
+
+ if (error) {
+ ffPrintError(FF_DISKIO_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error);
+ return false;
+ }
+
+ ffListSort(&result, sizeof(FFDiskIOResult), (const void*) sortDevices);
+
+ uint32_t index = 0;
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY buffer2 = ffStrbufCreate();
+
+ FF_LIST_FOR_EACH (FFDiskIOResult, dev, result) {
+ formatKey(options, dev, result.length == 1 ? 0 : index + 1, &key);
+ ffStrbufClear(&buffer);
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ ffSizeAppendNum(dev->bytesRead, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffStrbufAppendS(&buffer, " (R) - ");
+
+ ffSizeAppendNum(dev->bytesWritten, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffStrbufAppendS(&buffer, " (W)");
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ ffSizeAppendNum(dev->bytesRead, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffStrbufClear(&buffer2);
+ ffSizeAppendNum(dev->bytesWritten, &buffer2);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer2, "/s");
+ }
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(buffer, "size-read"),
+ FF_ARG(buffer2, "size-written"),
+ FF_ARG(dev->name, "name"),
+ FF_ARG(dev->devPath, "dev-path"),
+ FF_ARG(dev->bytesRead, "bytes-read"),
+ FF_ARG(dev->bytesWritten, "bytes-written"),
+ FF_ARG(dev->readCount, "read-count"),
+ FF_ARG(dev->writeCount, "write-count"),
+ }));
+ }
+ ++index;
+ }
+
+ FF_LIST_FOR_EACH (FFDiskIOResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->devPath);
+ }
+
+ return true;
+}
+
+void ffParseDiskIOJsonObject(FFDiskIOOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "namePrefix")) {
+ ffStrbufSetJsonVal(&options->namePrefix, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "detectTotal")) {
+ options->detectTotal = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "waitTime")) {
+ options->waitTime = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ ffPrintError(FF_DISKIO_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDiskIOJsonConfig(FFDiskIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "namePrefix", &options->namePrefix);
+
+ yyjson_mut_obj_add_bool(doc, module, "detectTotal", options->detectTotal);
+
+ yyjson_mut_obj_add_uint(doc, module, "waitTime", options->waitTime);
+}
+
+bool ffGenerateDiskIOJsonResult(FFDiskIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectDiskIO(&result, options);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFDiskIOResult, dev, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &dev->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "devPath", &dev->devPath);
+ yyjson_mut_obj_add_uint(doc, obj, "bytesRead", dev->bytesRead);
+ yyjson_mut_obj_add_uint(doc, obj, "bytesWritten", dev->bytesWritten);
+ yyjson_mut_obj_add_uint(doc, obj, "readCount", dev->readCount);
+ yyjson_mut_obj_add_uint(doc, obj, "writeCount", dev->writeCount);
+ }
+
+ FF_LIST_FOR_EACH (FFDiskIOResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->devPath);
+ }
+
+ return true;
+}
+
+void ffInitDiskIOOptions(FFDiskIOOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰓅");
+
+ ffStrbufInit(&options->namePrefix);
+ options->detectTotal = false;
+ options->waitTime = 1000;
+}
+
+void ffDestroyDiskIOOptions(FFDiskIOOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->namePrefix);
+}
+
+FFModuleBaseInfo ffDiskIOModuleInfo = {
+ .name = FF_DISKIO_MODULE_NAME,
+ .description = "Print physical disk I/O throughput",
+ .initOptions = (void*) ffInitDiskIOOptions,
+ .destroyOptions = (void*) ffDestroyDiskIOOptions,
+ .parseJsonObject = (void*) ffParseDiskIOJsonObject,
+ .printModule = (void*) ffPrintDiskIO,
+ .generateJsonResult = (void*) ffGenerateDiskIOJsonResult,
+ .generateJsonConfig = (void*) ffGenerateDiskIOJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Size of data read [per second] (formatted)", "size-read" },
+ { "Size of data written [per second] (formatted)", "size-written" },
+ { "Device name", "name" },
+ { "Device raw file path", "dev-path" },
+ { "Size of data read [per second] (in bytes)", "bytes-read" },
+ { "Size of data written [per second] (in bytes)", "bytes-written" },
+ { "Number of reads", "read-count" },
+ { "Number of writes", "write-count" },
+ }))
+};
diff --git a/src/modules/diskio/diskio.h b/src/modules/diskio/diskio.h
new file mode 100644
index 0000000..7fba81c
--- /dev/null
+++ b/src/modules/diskio/diskio.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DISKIO_MODULE_NAME "DiskIO"
+
+void ffPrepareDiskIO(FFDiskIOOptions* options);
+
+bool ffPrintDiskIO(FFDiskIOOptions* options);
+void ffInitDiskIOOptions(FFDiskIOOptions* options);
+void ffDestroyDiskIOOptions(FFDiskIOOptions* options);
+
+extern FFModuleBaseInfo ffDiskIOModuleInfo;
diff --git a/src/modules/diskio/option.h b/src/modules/diskio/option.h
new file mode 100644
index 0000000..5d41704
--- /dev/null
+++ b/src/modules/diskio/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFDiskIOOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf namePrefix;
+ uint32_t waitTime;
+ bool detectTotal;
+} FFDiskIOOptions;
+
+static_assert(sizeof(FFDiskIOOptions) <= FF_OPTION_MAX_SIZE, "FFDiskIOOptions size exceeds maximum allowed size");
diff --git a/src/modules/display/display.c b/src/modules/display/display.c
new file mode 100644
index 0000000..b11a2aa
--- /dev/null
+++ b/src/modules/display/display.c
@@ -0,0 +1,457 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/displayserver/displayserver.h"
+#include "modules/display/display.h"
+
+#include <math.h>
+
+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);
+}
+
+bool 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 false;
+ }
+
+ if (options->order != FF_DISPLAY_ORDER_NONE) {
+ ffListSort((FFlist*) &dsResult->displays, sizeof(FFDisplayResult), (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 {
+ uint32_t scaledWidth = (result->width * 96 + result->dpi / 2) / result->dpi;
+ uint32_t scaledHeight = (result->height * 96 + result->dpi / 2) / result->dpi;
+ ffStrbufAppendF(&buffer, "%ix%i", scaledWidth, scaledHeight);
+ }
+
+ if (options->compactType & FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT) {
+ if (result->refreshRate > 0) {
+ const char* space = instance.config.display.freqSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "";
+ if (options->preciseRefreshRate) {
+ ffStrbufAppendF(&buffer, " @ %g%sHz", result->refreshRate, space);
+ } else {
+ ffStrbufAppendF(&buffer, " @ %i%sHz", (uint32_t) (result->refreshRate + 0.5), space);
+ }
+ }
+ ffStrbufAppendS(&buffer, ", ");
+ } else {
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ }
+ ffStrbufTrimRight(&buffer, ' ');
+ ffStrbufTrimRight(&buffer, ',');
+ ffStrbufPutTo(&buffer, stdout);
+ return true;
+ }
+
+ 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_ARG(moduleIndex, "index"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(displayType, "type"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ double inch = sqrt(result->physicalWidth * result->physicalWidth + result->physicalHeight * result->physicalHeight) / 25.4;
+ uint32_t scaledWidth = (result->width * 96 + result->dpi / 2) / result->dpi;
+ uint32_t scaledHeight = (result->height * 96 + result->dpi / 2) / result->dpi;
+ double scaleFactor = (double) result->dpi / 96.;
+
+ 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->dpi != 96) {
+ ffStrbufAppendS(&buffer, " @ ");
+ ffStrbufAppendDouble(&buffer, scaleFactor, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros == FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS);
+ ffStrbufAppendC(&buffer, 'x');
+ }
+
+ if (inch > 1) {
+ ffStrbufAppendF(&buffer, " in %i\"", (uint32_t) (inch + 0.5));
+ }
+
+ if (result->refreshRate > 0) {
+ ffStrbufAppendS(&buffer, ", ");
+ if (options->preciseRefreshRate) {
+ ffStrbufAppendDouble(&buffer, result->refreshRate, 3, false);
+ } else {
+ ffStrbufAppendSInt(&buffer, (int) (result->refreshRate + 0.5));
+ }
+ ffStrbufAppendS(&buffer, instance.config.display.freqSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_NEVER ? "Hz" : " Hz");
+ }
+
+ 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';
+ }
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(result->width, "width"),
+ FF_ARG(result->height, "height"),
+ FF_ARG(refreshRate, "refresh-rate"),
+ FF_ARG(scaledWidth, "scaled-width"),
+ FF_ARG(scaledHeight, "scaled-height"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(displayType, "type"),
+ FF_ARG(result->rotation, "rotation"),
+ FF_ARG(result->primary, "is-primary"),
+ FF_ARG(result->physicalWidth, "physical-width"),
+ FF_ARG(result->physicalHeight, "physical-height"),
+ FF_ARG(iInch, "inch"),
+ FF_ARG(iPpi, "ppi"),
+ FF_ARG(result->bitDepth, "bit-depth"),
+ FF_ARG(hdrEnabled, "hdr-enabled"),
+ FF_ARG(result->manufactureYear, "manufacture-year"),
+ FF_ARG(result->manufactureWeek, "manufacture-week"),
+ FF_ARG(buf, "serial"),
+ FF_ARG(result->platformApi, "platform-api"),
+ FF_ARG(hdrCompatible, "hdr-compatible"),
+ FF_ARG(scaleFactor, "scale-factor"),
+ FF_ARG(result->preferredWidth, "preferred-width"),
+ FF_ARG(result->preferredHeight, "preferred-height"),
+ FF_ARG(preferredRefreshRate, "preferred-refresh-rate"),
+ FF_ARG(result->dpi, "dpi"),
+ }));
+ }
+ }
+
+ return true;
+}
+
+void ffParseDisplayJsonObject(FFDisplayOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compactType")) {
+ if (yyjson_is_null(val)) {
+ options->compactType = FF_DISPLAY_COMPACT_TYPE_NONE;
+ } else {
+ 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", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->compactType = (FFDisplayCompactType) value;
+ }
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "preciseRefreshRate")) {
+ options->preciseRefreshRate = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "order")) {
+ if (yyjson_is_null(val)) {
+ options->order = FF_DISPLAY_ORDER_NONE;
+ } else {
+ 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", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->order = (FFDisplayOrder) value;
+ }
+ }
+ continue;
+ }
+
+ ffPrintError(FF_DISPLAY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDisplayJsonConfig(FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ 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;
+ }
+
+ yyjson_mut_obj_add_bool(doc, module, "preciseRefreshRate", options->preciseRefreshRate);
+
+ switch (options->order) {
+ case FF_DISPLAY_ORDER_NONE:
+ yyjson_mut_obj_add_null(doc, module, "order");
+ break;
+ case FF_DISPLAY_ORDER_ASC:
+ yyjson_mut_obj_add_str(doc, module, "order", "asc");
+ break;
+ case FF_DISPLAY_ORDER_DESC:
+ yyjson_mut_obj_add_str(doc, module, "order", "desc");
+ break;
+ }
+}
+
+bool ffGenerateDisplayJsonResult(FF_A_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 false;
+ }
+
+ 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);
+
+ if (item->drrStatus == FF_DISPLAY_DRR_STATUS_UNKNOWN) {
+ yyjson_mut_obj_add_null(doc, output, "drrStatus");
+ } else {
+ switch (item->drrStatus) {
+ case FF_DISPLAY_DRR_STATUS_DISABLED:
+ yyjson_mut_obj_add_str(doc, output, "drrStatus", "Disabled");
+ break;
+ case FF_DISPLAY_DRR_STATUS_ENABLED:
+ yyjson_mut_obj_add_str(doc, output, "drrStatus", "Enabled");
+ break;
+ default:
+ yyjson_mut_obj_add_str(doc, output, "drrStatus", "Unknown");
+ break;
+ }
+ }
+ yyjson_mut_obj_add_uint(doc, output, "dpi", item->dpi);
+
+ uint32_t scaledWidth = (item->width * 96 + item->dpi / 2) / item->dpi;
+ uint32_t scaledHeight = (item->height * 96 + item->dpi / 2) / item->dpi;
+ yyjson_mut_val* scaled = yyjson_mut_obj_add_obj(doc, obj, "scaled");
+ yyjson_mut_obj_add_uint(doc, scaled, "width", scaledWidth);
+ yyjson_mut_obj_add_uint(doc, scaled, "height", 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);
+ }
+
+ return true;
+}
+
+void ffInitDisplayOptions(FFDisplayOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰍹");
+ options->compactType = FF_DISPLAY_COMPACT_TYPE_NONE;
+ options->preciseRefreshRate = false;
+ options->order = FF_DISPLAY_ORDER_NONE;
+}
+
+void ffDestroyDisplayOptions(FFDisplayOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffDisplayModuleInfo = {
+ .name = FF_DISPLAY_MODULE_NAME,
+ .description = "Print resolutions, refresh rates, etc",
+ .initOptions = (void*) ffInitDisplayOptions,
+ .destroyOptions = (void*) ffDestroyDisplayOptions,
+ .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" },
+ { "DPI", "dpi" },
+ }))
+};
diff --git a/src/modules/display/display.h b/src/modules/display/display.h
new file mode 100644
index 0000000..a0a4b30
--- /dev/null
+++ b/src/modules/display/display.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DISPLAY_MODULE_NAME "Display"
+
+bool ffPrintDisplay(FFDisplayOptions* options);
+void ffInitDisplayOptions(FFDisplayOptions* options);
+void ffDestroyDisplayOptions(FFDisplayOptions* options);
+
+extern FFModuleBaseInfo ffDisplayModuleInfo;
diff --git a/src/modules/display/option.h b/src/modules/display/option.h
new file mode 100644
index 0000000..1ae73a4
--- /dev/null
+++ b/src/modules/display/option.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFDisplayCompactType {
+ FF_DISPLAY_COMPACT_TYPE_NONE = 0,
+ FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT = 1 << 0,
+ FF_DISPLAY_COMPACT_TYPE_SCALED_BIT = 1 << 1,
+ FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT = 1 << 2,
+ FF_DISPLAY_COMPACT_TYPE_UNSIGNED = UINT8_MAX,
+} FFDisplayCompactType;
+
+typedef enum FF_A_PACKED FFDisplayOrder {
+ FF_DISPLAY_ORDER_NONE,
+ FF_DISPLAY_ORDER_ASC,
+ FF_DISPLAY_ORDER_DESC,
+} FFDisplayOrder;
+
+typedef struct FFDisplayOptions {
+ FFModuleArgs moduleArgs;
+
+ FFDisplayCompactType compactType;
+ bool preciseRefreshRate;
+ FFDisplayOrder order;
+} FFDisplayOptions;
+
+static_assert(sizeof(FFDisplayOptions) <= FF_OPTION_MAX_SIZE, "FFDisplayOptions size exceeds maximum allowed size");
diff --git a/src/modules/dns/dns.c b/src/modules/dns/dns.c
new file mode 100644
index 0000000..d7c90c4
--- /dev/null
+++ b/src/modules/dns/dns.c
@@ -0,0 +1,148 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/dns/dns.h"
+#include "modules/dns/dns.h"
+
+bool ffPrintDNS(FFDNSOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectDNS(options, &result);
+
+ if (error) {
+ ffPrintError(FF_DNS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_DNS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "NO DNS servers detected");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ FF_LIST_FOR_EACH (FFstrbuf, item, result) {
+ if (!ffStrbufContainC(item, '.')) {
+ continue; // IPv4
+ }
+ if (buf.length) {
+ ffStrbufAppendC(&buf, ' ');
+ }
+ ffStrbufAppend(&buf, item);
+ }
+ FF_LIST_FOR_EACH (FFstrbuf, item, result) {
+ if (!ffStrbufContainC(item, ':')) {
+ continue; // IPv6
+ }
+ if (buf.length) {
+ ffStrbufAppendC(&buf, ' ');
+ }
+ ffStrbufAppend(&buf, item);
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_DNS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ ffStrbufPutTo(&buf, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_DNS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(buf, "result"),
+ }));
+ }
+
+ FF_LIST_FOR_EACH (FFstrbuf, item, result) {
+ ffStrbufDestroy(item);
+ }
+
+ return true;
+}
+
+void ffParseDNSJsonObject(FFDNSOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showType")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "both", FF_DNS_TYPE_BOTH },
+ { "ipv4", FF_DNS_TYPE_IPV4_BIT },
+ { "ipv6", FF_DNS_TYPE_IPV6_BIT },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_DNS_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->showType = (FFDNSShowType) value;
+ }
+ continue;
+ }
+
+ ffPrintError(FF_DNS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateDNSJsonConfig(FFDNSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ switch ((uint8_t) options->showType) {
+ case FF_DNS_TYPE_IPV4_BIT:
+ yyjson_mut_obj_add_str(doc, module, "showType", "ipv4");
+ break;
+ case FF_DNS_TYPE_IPV6_BIT:
+ yyjson_mut_obj_add_str(doc, module, "showType", "ipv6");
+ break;
+ case FF_DNS_TYPE_BOTH:
+ yyjson_mut_obj_add_str(doc, module, "showType", "both");
+ break;
+ }
+}
+
+bool ffGenerateDNSJsonResult(FFDNSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectDNS(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFstrbuf, item, result) {
+ yyjson_mut_arr_add_strbuf(doc, arr, item);
+ }
+
+ FF_LIST_FOR_EACH (FFstrbuf, item, result) {
+ ffStrbufDestroy(item);
+ }
+
+ return true;
+}
+
+void ffInitDNSOptions(FFDNSOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰇖");
+
+ options->showType = FF_DNS_TYPE_BOTH;
+}
+
+void ffDestroyDNSOptions(FFDNSOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffDNSModuleInfo = {
+ .name = FF_DNS_MODULE_NAME,
+ .description = "Print configured DNS servers",
+ .initOptions = (void*) ffInitDNSOptions,
+ .destroyOptions = (void*) ffDestroyDNSOptions,
+ .parseJsonObject = (void*) ffParseDNSJsonObject,
+ .printModule = (void*) ffPrintDNS,
+ .generateJsonResult = (void*) ffGenerateDNSJsonResult,
+ .generateJsonConfig = (void*) ffGenerateDNSJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "DNS result", "result" },
+ }))
+};
diff --git a/src/modules/dns/dns.h b/src/modules/dns/dns.h
new file mode 100644
index 0000000..aec4e85
--- /dev/null
+++ b/src/modules/dns/dns.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_DNS_MODULE_NAME "DNS"
+
+bool ffPrintDNS(FFDNSOptions* options);
+void ffInitDNSOptions(FFDNSOptions* options);
+void ffDestroyDNSOptions(FFDNSOptions* options);
+
+extern FFModuleBaseInfo ffDNSModuleInfo;
diff --git a/src/modules/dns/option.h b/src/modules/dns/option.h
new file mode 100644
index 0000000..e20bbec
--- /dev/null
+++ b/src/modules/dns/option.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFDNSShowType {
+ FF_DNS_TYPE_IPV4_BIT = 1,
+ FF_DNS_TYPE_IPV6_BIT = 2,
+ FF_DNS_TYPE_BOTH = FF_DNS_TYPE_IPV4_BIT | FF_DNS_TYPE_IPV6_BIT,
+ FF_DNS_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFDNSShowType;
+
+typedef struct FFDNSOptions {
+ FFModuleArgs moduleArgs;
+
+ FFDNSShowType showType;
+} FFDNSOptions;
+
+static_assert(sizeof(FFDNSOptions) <= FF_OPTION_MAX_SIZE, "FFDNSOptions size exceeds maximum allowed size");
diff --git a/src/modules/editor/editor.c b/src/modules/editor/editor.c
new file mode 100644
index 0000000..23b1937
--- /dev/null
+++ b/src/modules/editor/editor.c
@@ -0,0 +1,121 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/libc/libc.h"
+#include "detection/editor/editor.h"
+#include "modules/editor/editor.h"
+
+bool ffPrintEditor(FFEditorOptions* options) {
+ FFEditorResult result = {
+ .type = "Unknown",
+ .name = ffStrbufCreate(),
+ .path = ffStrbufCreate(),
+ .exe = ffStrbufCreate(),
+ .version = ffStrbufCreate(),
+ };
+ const char* error = ffDetectEditor(&result);
+
+ if (error) {
+ ffPrintError(FF_EDITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_EDITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (result.exe.length) {
+ ffStrbufWriteTo(&result.exe, stdout);
+ if (result.version.length) {
+ printf(" %s", result.version.chars);
+ }
+ } else {
+ ffStrbufWriteTo(&result.name, stdout);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_EDITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.type, "type"),
+ FF_ARG(result.name, "name"),
+ FF_ARG(result.exe, "exe-name"),
+ FF_ARG(result.path, "path"),
+ FF_ARG(result.version, "version"),
+ }));
+ }
+
+ ffStrbufDestroy(&result.name);
+ ffStrbufDestroy(&result.path);
+ ffStrbufDestroy(&result.exe);
+ ffStrbufDestroy(&result.version);
+
+ return true;
+}
+
+void ffParseEditorJsonObject(FFEditorOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_EDITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateEditorJsonConfig(FFEditorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateEditorJsonResult(FF_A_UNUSED FFEditorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFEditorResult result = {
+ .name = ffStrbufCreate(),
+ .path = ffStrbufCreate(),
+ .version = ffStrbufCreate(),
+ };
+
+ const char* error = ffDetectEditor(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_str(doc, obj, "type", result.type);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &result.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "path", &result.path);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exe", &result.exe);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+
+ ffStrbufDestroy(&result.name);
+ ffStrbufDestroy(&result.path);
+ ffStrbufDestroy(&result.exe);
+ ffStrbufDestroy(&result.version);
+
+ return true;
+}
+
+void ffInitEditorOptions(FFEditorOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󱞎");
+}
+
+void ffDestroyEditorOptions(FFEditorOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffEditorModuleInfo = {
+ .name = FF_EDITOR_MODULE_NAME,
+ .description = "Print information about the default editor ($VISUAL or $EDITOR)",
+ .initOptions = (void*) ffInitEditorOptions,
+ .destroyOptions = (void*) ffDestroyEditorOptions,
+ .parseJsonObject = (void*) ffParseEditorJsonObject,
+ .printModule = (void*) ffPrintEditor,
+ .generateJsonResult = (void*) ffGenerateEditorJsonResult,
+ .generateJsonConfig = (void*) ffGenerateEditorJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Type (Visual / Editor)", "type" },
+ { "Name", "name" },
+ { "Exe name of real path", "exe-name" },
+ { "Full path of real path", "path" },
+ { "Version", "version" },
+ }))
+};
diff --git a/src/modules/editor/editor.h b/src/modules/editor/editor.h
new file mode 100644
index 0000000..293d0a6
--- /dev/null
+++ b/src/modules/editor/editor.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_EDITOR_MODULE_NAME "Editor"
+
+bool ffPrintEditor(FFEditorOptions* options);
+void ffInitEditorOptions(FFEditorOptions* options);
+void ffDestroyEditorOptions(FFEditorOptions* options);
+
+extern FFModuleBaseInfo ffEditorModuleInfo;
diff --git a/src/modules/editor/option.h b/src/modules/editor/option.h
new file mode 100644
index 0000000..e812ca9
--- /dev/null
+++ b/src/modules/editor/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFEditorOptions {
+ FFModuleArgs moduleArgs;
+} FFEditorOptions;
+
+static_assert(sizeof(FFEditorOptions) <= FF_OPTION_MAX_SIZE, "FFEditorOptions size exceeds maximum allowed size");
diff --git a/src/modules/font/font.c b/src/modules/font/font.c
new file mode 100644
index 0000000..a32a7e8
--- /dev/null
+++ b/src/modules/font/font.c
@@ -0,0 +1,113 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/font/font.h"
+#include "modules/font/font.h"
+
+bool ffPrintFont(FFFontOptions* options) {
+ bool success = false;
+ FFFontResult font;
+ for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) {
+ ffStrbufInit(&font.fonts[i]);
+ }
+ ffStrbufInit(&font.display);
+
+ const char* error = ffDetectFont(&font);
+
+ if (error) {
+ ffPrintError(FF_FONT_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ } else {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_FONT_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&font.display, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_FONT_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(font.fonts[0], "font1"),
+ FF_ARG(font.fonts[1], "font2"),
+ FF_ARG(font.fonts[2], "font3"),
+ FF_ARG(font.fonts[3], "font4"),
+ FF_ARG(font.display, "combined"),
+ }));
+ }
+
+ success = true;
+ }
+
+ ffStrbufDestroy(&font.display);
+ for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) {
+ ffStrbufDestroy(&font.fonts[i]);
+ }
+
+ return success;
+}
+
+void ffParseFontJsonObject(FFFontOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_FONT_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateFontJsonConfig(FFFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateFontJsonResult(FF_A_UNUSED FFFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFFontResult font;
+ for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) {
+ ffStrbufInit(&font.fonts[i]);
+ }
+ ffStrbufInit(&font.display);
+
+ const char* error = ffDetectFont(&font);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "display", &font.display);
+ yyjson_mut_val* fontsArr = yyjson_mut_obj_add_arr(doc, obj, "fonts");
+ for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) {
+ yyjson_mut_arr_add_strbuf(doc, fontsArr, &font.fonts[i]);
+ }
+ success = true;
+ }
+
+ ffStrbufDestroy(&font.display);
+ for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) {
+ ffStrbufDestroy(&font.fonts[i]);
+ }
+
+ return success;
+}
+
+void ffInitFontOptions(FFFontOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyFontOptions(FFFontOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffFontModuleInfo = {
+ .name = FF_FONT_MODULE_NAME,
+ .description = "Print system font names",
+ .initOptions = (void*) ffInitFontOptions,
+ .destroyOptions = (void*) ffDestroyFontOptions,
+ .parseJsonObject = (void*) ffParseFontJsonObject,
+ .printModule = (void*) ffPrintFont,
+ .generateJsonResult = (void*) ffGenerateFontJsonResult,
+ .generateJsonConfig = (void*) ffGenerateFontJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Font 1", "font1" },
+ { "Font 2", "font2" },
+ { "Font 3", "font3" },
+ { "Font 4", "font4" },
+ { "Combined fonts for display", "combined" },
+ }))
+};
diff --git a/src/modules/font/font.h b/src/modules/font/font.h
new file mode 100644
index 0000000..b793b9f
--- /dev/null
+++ b/src/modules/font/font.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_FONT_MODULE_NAME "Font"
+
+bool ffPrintFont(FFFontOptions* options);
+void ffInitFontOptions(FFFontOptions* options);
+void ffDestroyFontOptions(FFFontOptions* options);
+
+extern FFModuleBaseInfo ffFontModuleInfo;
diff --git a/src/modules/font/option.h b/src/modules/font/option.h
new file mode 100644
index 0000000..b2e2753
--- /dev/null
+++ b/src/modules/font/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFFontOptions {
+ FFModuleArgs moduleArgs;
+} FFFontOptions;
+
+static_assert(sizeof(FFFontOptions) <= FF_OPTION_MAX_SIZE, "FFFontOptions size exceeds maximum allowed size");
diff --git a/src/modules/gamepad/gamepad.c b/src/modules/gamepad/gamepad.c
new file mode 100644
index 0000000..d34dcca
--- /dev/null
+++ b/src/modules/gamepad/gamepad.c
@@ -0,0 +1,206 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/gamepad/gamepad.h"
+#include "modules/gamepad/gamepad.h"
+
+static void printDevice(FFGamepadOptions* options, const FFGamepadDevice* device, uint8_t index) {
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_GAMEPAD_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ bool showBatteryLevel = device->battery > 0 && device->battery <= 100;
+
+ if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_BAR_BIT)) {
+ ffPercentAppendBar(&buffer, device->battery, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&buffer, ' ');
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppend(&buffer, &device->name);
+ }
+
+ if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_NUM_BIT)) {
+ if (buffer.length) {
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ ffPercentAppendNum(&buffer, device->battery, options->percent, buffer.length > 0, &options->moduleArgs);
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentageNum, device->battery, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentageBar, device->battery, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_GAMEPAD_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(device->name, "name"),
+ FF_ARG(device->serial, "serial"),
+ FF_ARG(percentageNum, "battery-percentage"),
+ FF_ARG(percentageBar, "battery-percentage-bar"),
+ }));
+ }
+}
+
+bool ffPrintGamepad(FFGamepadOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectGamepad(&result);
+
+ if (error) {
+ ffPrintError(FF_GAMEPAD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (!result.length) {
+ ffPrintError(FF_GAMEPAD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No devices detected");
+ return false;
+ }
+
+ FF_LIST_AUTO_DESTROY filtered = ffListCreate();
+ FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ if (!ignored) {
+ FFGamepadDevice** ptr = FF_LIST_ADD(FFGamepadDevice*, filtered);
+ *ptr = device;
+ }
+ }
+
+ bool ret = true;
+ if (!filtered.length) {
+ ffPrintError(FF_GAMEPAD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "All devices are ignored");
+ ret = false;
+ } else {
+ uint8_t index = 0;
+ FF_LIST_FOR_EACH (FFGamepadDevice*, pdevice, filtered) {
+ FFGamepadDevice* device = *pdevice;
+ printDevice(options, device, filtered.length > 1 ? ++index : 0);
+ }
+
+ FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+ }
+
+ return ret;
+}
+
+void ffParseGamepadJsonObject(FFGamepadOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ignores")) {
+ yyjson_val* elem;
+ size_t eidx, emax;
+ yyjson_arr_foreach (val, eidx, emax, elem) {
+ if (yyjson_is_str(elem)) {
+ FFstrbuf* strbuf = FF_LIST_ADD(FFstrbuf, options->ignores);
+ ffStrbufInitJsonVal(strbuf, elem);
+ }
+ }
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_GAMEPAD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateGamepadJsonConfig(FFGamepadOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ if (options->ignores.length > 0) {
+ yyjson_mut_val* ignores = yyjson_mut_obj_add_arr(doc, module, "ignores");
+ FF_LIST_FOR_EACH (FFstrbuf, strbuf, options->ignores) {
+ yyjson_mut_arr_append(ignores, yyjson_mut_strncpy(doc, strbuf->chars, strbuf->length));
+ }
+ }
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateGamepadJsonResult(FF_A_UNUSED FFGamepadOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectGamepad(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &device->serial);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &device->name);
+
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ yyjson_mut_obj_add_bool(doc, obj, "ignored", ignored);
+ }
+
+ FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+
+ return true;
+}
+
+void ffInitGamepadOptions(FFGamepadOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰺵");
+
+ ffListInit(&options->ignores);
+ options->percent = (FFPercentageModuleConfig) { 50, 20, 0 };
+}
+
+void ffDestroyGamepadOptions(FFGamepadOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+
+ FF_LIST_FOR_EACH (FFstrbuf, str, options->ignores) {
+ ffStrbufDestroy(str);
+ }
+ ffListDestroy(&options->ignores);
+}
+
+FFModuleBaseInfo ffGamepadModuleInfo = {
+ .name = FF_GAMEPAD_MODULE_NAME,
+ .description = "List connected gamepads",
+ .initOptions = (void*) ffInitGamepadOptions,
+ .destroyOptions = (void*) ffDestroyGamepadOptions,
+ .parseJsonObject = (void*) ffParseGamepadJsonObject,
+ .printModule = (void*) ffPrintGamepad,
+ .generateJsonResult = (void*) ffGenerateGamepadJsonResult,
+ .generateJsonConfig = (void*) ffGenerateGamepadJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name", "name" },
+ { "Serial number", "serial" },
+ { "Battery percentage num", "battery-percentage" },
+ { "Battery percentage bar", "battery-percentage-bar" },
+ }))
+};
diff --git a/src/modules/gamepad/gamepad.h b/src/modules/gamepad/gamepad.h
new file mode 100644
index 0000000..395c90b
--- /dev/null
+++ b/src/modules/gamepad/gamepad.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_GAMEPAD_MODULE_NAME "Gamepad"
+
+bool ffPrintGamepad(FFGamepadOptions* options);
+void ffInitGamepadOptions(FFGamepadOptions* options);
+void ffDestroyGamepadOptions(FFGamepadOptions* options);
+
+extern FFModuleBaseInfo ffGamepadModuleInfo;
diff --git a/src/modules/gamepad/option.h b/src/modules/gamepad/option.h
new file mode 100644
index 0000000..64728c0
--- /dev/null
+++ b/src/modules/gamepad/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/FFlist.h"
+
+typedef struct FFGamepadOptions {
+ FFModuleArgs moduleArgs;
+
+ FFlist ignores; // List of FFstrbuf
+ FFPercentageModuleConfig percent;
+} FFGamepadOptions;
+
+static_assert(sizeof(FFGamepadOptions) <= FF_OPTION_MAX_SIZE, "FFGamepadOptions size exceeds maximum allowed size");
diff --git a/src/modules/gpu/gpu.c b/src/modules/gpu/gpu.c
new file mode 100644
index 0000000..5be8442
--- /dev/null
+++ b/src/modules/gpu/gpu.c
@@ -0,0 +1,488 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/temps.h"
+#include "common/size.h"
+#include "common/frequency.h"
+#include "common/strutil.h"
+#include "detection/host/host.h"
+#include "detection/gpu/gpu.h"
+#include "modules/gpu/gpu.h"
+
+#include <stdlib.h>
+
+static void printGPUResult(FFGPUOptions* options, uint8_t index, const FFGPUResult* gpu) {
+ const char* type;
+ switch (gpu->type) {
+ case FF_GPU_TYPE_INTEGRATED:
+ type = "Integrated";
+ break;
+ case FF_GPU_TYPE_DISCRETE:
+ type = "Discrete";
+ break;
+ default:
+ type = "Unknown";
+ break;
+ }
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_GPU_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate();
+
+ if (gpu->vendor.length > 0 && !ffStrbufStartsWithIgnCase(&gpu->name, &gpu->vendor)) {
+ ffStrbufAppend(&output, &gpu->vendor);
+ ffStrbufAppendC(&output, ' ');
+ }
+
+ ffStrbufAppend(&output, &gpu->name);
+
+ if (gpu->coreCount != FF_GPU_CORE_COUNT_UNSET) {
+ ffStrbufAppendF(&output, " (%d)", gpu->coreCount);
+ }
+
+ if (gpu->frequency > 0) {
+ ffStrbufAppendS(&output, " @ ");
+ ffFreqAppendNum(gpu->frequency, &output);
+ }
+
+ if (gpu->temperature != FF_GPU_TEMP_UNSET) {
+ ffStrbufAppendS(&output, " - ");
+ ffTempsAppendNum(gpu->temperature, &output, options->tempConfig, &options->moduleArgs);
+ }
+
+ if (gpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET && gpu->dedicated.total != 0) {
+ ffStrbufAppendS(&output, " (");
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ ffSizeAppendNum(gpu->dedicated.used, &output);
+ ffStrbufAppendS(&output, " / ");
+ }
+ ffSizeAppendNum(gpu->dedicated.total, &output);
+ }
+ if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ double percent = (double) gpu->dedicated.used / (double) gpu->dedicated.total * 100.0;
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffStrbufAppendS(&output, ", ");
+ ffPercentAppendNum(&output, percent, options->percent, false, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffStrbufAppendS(&output, " ");
+ ffPercentAppendBar(&output, percent, options->percent, &options->moduleArgs);
+ }
+ }
+ ffStrbufAppendC(&output, ')');
+ }
+
+ if (gpu->type != FF_GPU_TYPE_UNKNOWN) {
+ ffStrbufAppendF(&output, " [%s]", type);
+ }
+
+ ffStrbufPutTo(&output, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY tempStr = ffStrbufCreate();
+ ffTempsAppendNum(gpu->temperature, &tempStr, options->tempConfig, &options->moduleArgs);
+ FF_STRBUF_AUTO_DESTROY dTotal = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY dUsed = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY dPercentNum = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY dPercentBar = ffStrbufCreate();
+ if (gpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET) {
+ ffSizeAppendNum(gpu->dedicated.total, &dTotal);
+ }
+ if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ ffSizeAppendNum(gpu->dedicated.used, &dUsed);
+ }
+ if (gpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET && gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ double percent = (double) gpu->dedicated.used / (double) gpu->dedicated.total * 100.0;
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&dPercentNum, percent, options->percent, false, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&dPercentBar, percent, options->percent, &options->moduleArgs);
+ }
+ }
+
+ FF_STRBUF_AUTO_DESTROY sTotal = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY sUsed = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY sPercentNum = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY sPercentBar = ffStrbufCreate();
+ if (gpu->shared.total != FF_GPU_VMEM_SIZE_UNSET) {
+ ffSizeAppendNum(gpu->shared.total, &sTotal);
+ }
+ if (gpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) {
+ ffSizeAppendNum(gpu->shared.used, &sUsed);
+ }
+ if (gpu->shared.total != FF_GPU_VMEM_SIZE_UNSET && gpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) {
+ double percent = (double) gpu->shared.used / (double) gpu->shared.total * 100.0;
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&sPercentNum, percent, options->percent, false, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&sPercentBar, percent, options->percent, &options->moduleArgs);
+ }
+ }
+
+ FF_STRBUF_AUTO_DESTROY frequency = ffStrbufCreate();
+ ffFreqAppendNum(gpu->frequency, &frequency);
+
+ FF_STRBUF_AUTO_DESTROY coreUsageNum = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY coreUsageBar = ffStrbufCreate();
+ if (gpu->coreUsage != FF_GPU_CORE_USAGE_UNSET) {
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&coreUsageNum, gpu->coreUsage, options->percent, false, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&coreUsageBar, gpu->coreUsage, options->percent, &options->moduleArgs);
+ }
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_GPU_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(gpu->vendor, "vendor"),
+ FF_ARG(gpu->name, "name"),
+ FF_ARG(gpu->driver, "driver"),
+ FF_ARG(tempStr, "temperature"),
+ FF_ARG(gpu->coreCount, "core-count"),
+ FF_ARG(type, "type"),
+ FF_ARG(dTotal, "dedicated-total"),
+ FF_ARG(dUsed, "dedicated-used"),
+ FF_ARG(sTotal, "shared-total"),
+ FF_ARG(sUsed, "shared-used"),
+ FF_ARG(gpu->platformApi, "platform-api"),
+ FF_ARG(frequency, "frequency"),
+ FF_ARG(index, "index"),
+ FF_ARG(dPercentNum, "dedicated-percentage-num"),
+ FF_ARG(dPercentBar, "dedicated-percentage-bar"),
+ FF_ARG(sPercentNum, "shared-percentage-num"),
+ FF_ARG(sPercentBar, "shared-percentage-bar"),
+ FF_ARG(coreUsageNum, "core-usage-num"),
+ FF_ARG(coreUsageBar, "core-usage-bar"),
+ FF_ARG(gpu->memoryType, "memory-type"),
+ }));
+ }
+}
+
+bool ffPrintGPU(FFGPUOptions* options) {
+ FF_LIST_AUTO_DESTROY gpus = ffListCreate();
+ const char* error = ffDetectGPU(options, &gpus);
+ if (error) {
+ ffPrintError(FF_GPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ FF_LIST_AUTO_DESTROY selectedGPUs = ffListCreateA(sizeof(const FFGPUResult*), gpus.length);
+
+ FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
+ if (gpu->type == FF_GPU_TYPE_UNKNOWN && options->hideType == FF_GPU_TYPE_UNKNOWN) {
+ continue;
+ }
+
+ if (gpu->type == FF_GPU_TYPE_INTEGRATED && options->hideType == FF_GPU_TYPE_INTEGRATED) {
+ continue;
+ }
+
+ if (gpu->type == FF_GPU_TYPE_DISCRETE && options->hideType == FF_GPU_TYPE_DISCRETE) {
+ continue;
+ }
+
+ *FF_LIST_ADD(const FFGPUResult*, selectedGPUs) = gpu;
+ }
+
+ uint32_t i = 0;
+ FF_LIST_FOR_EACH (const FFGPUResult*, pgpu, selectedGPUs) {
+ printGPUResult(options, selectedGPUs.length == 1 ? 0 : (uint8_t) (i + 1), *pgpu);
+ ++i;
+ }
+
+ FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
+ ffStrbufDestroy(&gpu->vendor);
+ ffStrbufDestroy(&gpu->name);
+ ffStrbufDestroy(&gpu->driver);
+ ffStrbufDestroy(&gpu->platformApi);
+ ffStrbufDestroy(&gpu->memoryType);
+ }
+
+ if (selectedGPUs.length == 0) {
+ ffPrintError(FF_GPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, (gpus.length > 0 ? "GPUs found but all hidden by hideType option" : "No GPUs detected"));
+ return false;
+ }
+
+ return true;
+}
+
+void ffParseGPUJsonObject(FFGPUOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffTempsParseJsonObject(key, val, &options->temp, &options->tempConfig)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "driverSpecific")) {
+ options->driverSpecific = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "detectionMethod")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]){
+ { "auto", FF_GPU_DETECTION_METHOD_AUTO },
+ { "pci", FF_GPU_DETECTION_METHOD_PCI },
+ { "vulkan", FF_GPU_DETECTION_METHOD_VULKAN },
+ { "opencl", FF_GPU_DETECTION_METHOD_OPENCL },
+ { "opengl", FF_GPU_DETECTION_METHOD_OPENGL },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_GPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->detectionMethod = (FFGPUDetectionMethod) value;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "hideType")) {
+ if (yyjson_is_null(val)) {
+ options->hideType = FF_GPU_TYPE_NONE;
+ } else {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]){
+ { "none", FF_GPU_TYPE_NONE },
+ { "unknown", FF_GPU_TYPE_UNKNOWN },
+ { "integrated", FF_GPU_TYPE_INTEGRATED },
+ { "discrete", FF_GPU_TYPE_DISCRETE },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_GPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->hideType = (FFGPUType) value;
+ }
+ }
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_GPU_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateGPUJsonConfig(FFGPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "driverSpecific", options->driverSpecific);
+
+ switch (options->detectionMethod) {
+ case FF_GPU_DETECTION_METHOD_AUTO:
+ yyjson_mut_obj_add_str(doc, module, "detectionMethod", "auto");
+ break;
+ case FF_GPU_DETECTION_METHOD_PCI:
+ yyjson_mut_obj_add_str(doc, module, "detectionMethod", "pci");
+ break;
+ case FF_GPU_DETECTION_METHOD_VULKAN:
+ yyjson_mut_obj_add_str(doc, module, "detectionMethod", "vulkan");
+ break;
+ case FF_GPU_DETECTION_METHOD_OPENCL:
+ yyjson_mut_obj_add_str(doc, module, "detectionMethod", "opencl");
+ break;
+ case FF_GPU_DETECTION_METHOD_OPENGL:
+ yyjson_mut_obj_add_str(doc, module, "detectionMethod", "opengl");
+ break;
+ }
+
+ ffTempsGenerateJsonConfig(doc, module, options->temp, options->tempConfig);
+
+ switch (options->hideType) {
+ case FF_GPU_TYPE_NONE:
+ yyjson_mut_obj_add_str(doc, module, "hideType", "none");
+ break;
+ case FF_GPU_TYPE_UNKNOWN:
+ yyjson_mut_obj_add_str(doc, module, "hideType", "unknown");
+ break;
+ case FF_GPU_TYPE_INTEGRATED:
+ yyjson_mut_obj_add_str(doc, module, "hideType", "integrated");
+ break;
+ case FF_GPU_TYPE_DISCRETE:
+ yyjson_mut_obj_add_str(doc, module, "hideType", "discrete");
+ break;
+ }
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateGPUJsonResult(FFGPUOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY gpus = ffListCreate();
+ const char* error = ffDetectGPU(options, &gpus);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+
+ if (gpu->index != FF_GPU_INDEX_UNSET) {
+ yyjson_mut_obj_add_uint(doc, obj, "index", gpu->index);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "index");
+ }
+
+ if (gpu->coreCount != FF_GPU_CORE_COUNT_UNSET) {
+ yyjson_mut_obj_add_int(doc, obj, "coreCount", gpu->coreCount);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "coreCount");
+ }
+
+ if (gpu->coreUsage != FF_GPU_CORE_USAGE_UNSET) {
+ yyjson_mut_obj_add_real(doc, obj, "coreUsage", gpu->coreUsage);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "coreUsage");
+ }
+
+ yyjson_mut_val* memoryObj = yyjson_mut_obj_add_obj(doc, obj, "memory");
+
+ yyjson_mut_val* dedicatedObj = yyjson_mut_obj_add_obj(doc, memoryObj, "dedicated");
+ if (gpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedObj, "total", gpu->dedicated.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedObj, "total");
+ }
+
+ if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedObj, "used", gpu->dedicated.used);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedObj, "used");
+ }
+
+ yyjson_mut_val* sharedObj = yyjson_mut_obj_add_obj(doc, memoryObj, "shared");
+ if (gpu->shared.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedObj, "total", gpu->shared.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedObj, "total");
+ }
+ if (gpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedObj, "used", gpu->shared.used);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedObj, "used");
+ }
+
+ if (gpu->memoryType.length) {
+ yyjson_mut_obj_add_strbuf(doc, memoryObj, "type", &gpu->memoryType);
+ } else {
+ yyjson_mut_obj_add_null(doc, memoryObj, "type");
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "driver", &gpu->driver);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &gpu->name);
+
+ if (gpu->temperature != FF_GPU_TEMP_UNSET) {
+ yyjson_mut_obj_add_real(doc, obj, "temperature", gpu->temperature);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "temperature");
+ }
+
+ const char* type;
+ switch (gpu->type) {
+ case FF_GPU_TYPE_INTEGRATED:
+ type = "Integrated";
+ break;
+ case FF_GPU_TYPE_DISCRETE:
+ type = "Discrete";
+ break;
+ default:
+ type = NULL;
+ break;
+ }
+ if (type) {
+ yyjson_mut_obj_add_str(doc, obj, "type", type);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "type");
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &gpu->vendor);
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "platformApi", &gpu->platformApi);
+
+ if (gpu->frequency != FF_GPU_FREQUENCY_UNSET) {
+ yyjson_mut_obj_add_uint(doc, obj, "frequency", gpu->frequency);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "frequency");
+ }
+
+ yyjson_mut_obj_add_uint(doc, obj, "deviceId", gpu->deviceId);
+ }
+
+ FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
+ ffStrbufDestroy(&gpu->vendor);
+ ffStrbufDestroy(&gpu->name);
+ ffStrbufDestroy(&gpu->driver);
+ ffStrbufDestroy(&gpu->platformApi);
+ ffStrbufDestroy(&gpu->memoryType);
+ }
+
+ return true;
+}
+
+void ffInitGPUOptions(FFGPUOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰾲");
+
+ options->driverSpecific = false;
+ options->detectionMethod =
+#if defined(__x86_64__) || defined(__i386__)
+ FF_GPU_DETECTION_METHOD_PCI
+#else
+ FF_GPU_DETECTION_METHOD_AUTO
+#endif
+ ;
+ options->temp = false;
+ options->hideType = FF_GPU_TYPE_NONE;
+ options->tempConfig = (FFColorRangeConfig){ 60, 80 };
+ options->percent = (FFPercentageModuleConfig){ 50, 80, 0 };
+}
+
+void ffDestroyGPUOptions(FFGPUOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffGPUModuleInfo = {
+ .name = FF_GPU_MODULE_NAME,
+ .description = "Print GPU names, memory sizes, types, etc",
+ .initOptions = (void*) ffInitGPUOptions,
+ .destroyOptions = (void*) ffDestroyGPUOptions,
+ .parseJsonObject = (void*) ffParseGPUJsonObject,
+ .printModule = (void*) ffPrintGPU,
+ .generateJsonResult = (void*) ffGenerateGPUJsonResult,
+ .generateJsonConfig = (void*) ffGenerateGPUJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){
+ { "GPU vendor", "vendor" },
+ { "GPU name", "name" },
+ { "GPU driver", "driver" },
+ { "GPU temperature", "temperature" },
+ { "GPU core count", "core-count" },
+ { "GPU type", "type" },
+ { "GPU total dedicated memory", "dedicated-total" },
+ { "GPU used dedicated memory", "dedicated-used" },
+ { "GPU total shared memory", "shared-total" },
+ { "GPU used shared memory", "shared-used" },
+ { "The platform API used when detecting the GPU", "platform-api" },
+ { "Current frequency in GHz", "frequency" },
+ { "GPU vendor specific index", "index" },
+ { "Dedicated memory usage percentage num", "dedicated-percentage-num" },
+ { "Dedicated memory usage percentage bar", "dedicated-percentage-bar" },
+ { "Shared memory usage percentage num", "shared-percentage-num" },
+ { "Shared memory usage percentage bar", "shared-percentage-bar" },
+ { "Core usage percentage num", "core-usage-num" },
+ { "Core usage percentage bar", "core-usage-bar" },
+ { "Memory type (Windows only)", "memory-type" },
+ })),
+};
diff --git a/src/modules/gpu/gpu.h b/src/modules/gpu/gpu.h
new file mode 100644
index 0000000..e2a6f14
--- /dev/null
+++ b/src/modules/gpu/gpu.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_GPU_MODULE_NAME "GPU"
+
+bool ffPrintGPU(FFGPUOptions* options);
+void ffInitGPUOptions(FFGPUOptions* options);
+void ffDestroyGPUOptions(FFGPUOptions* options);
+
+extern FFModuleBaseInfo ffGPUModuleInfo;
diff --git a/src/modules/gpu/option.h b/src/modules/gpu/option.h
new file mode 100644
index 0000000..7ec0df3
--- /dev/null
+++ b/src/modules/gpu/option.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef enum FF_A_PACKED FFGPUType {
+ FF_GPU_TYPE_NONE, // Indicates no specific GPU type. Useful as a hide filter only.
+ FF_GPU_TYPE_UNKNOWN, // Indicates an unknown or unrecognized GPU type.
+ FF_GPU_TYPE_INTEGRATED,
+ FF_GPU_TYPE_DISCRETE,
+} FFGPUType;
+
+typedef enum FF_A_PACKED FFGPUDetectionMethod {
+ FF_GPU_DETECTION_METHOD_AUTO,
+ FF_GPU_DETECTION_METHOD_PCI,
+ FF_GPU_DETECTION_METHOD_VULKAN,
+ FF_GPU_DETECTION_METHOD_OPENCL,
+ FF_GPU_DETECTION_METHOD_OPENGL,
+} FFGPUDetectionMethod;
+
+typedef struct FFGPUOptions {
+ FFModuleArgs moduleArgs;
+
+ FFGPUType hideType;
+ FFGPUDetectionMethod detectionMethod;
+ bool temp;
+ bool driverSpecific;
+ FFColorRangeConfig tempConfig;
+ FFPercentageModuleConfig percent;
+} FFGPUOptions;
+
+static_assert(sizeof(FFGPUOptions) <= FF_OPTION_MAX_SIZE, "FFGPUOptions size exceeds maximum allowed size");
diff --git a/src/modules/host/host.c b/src/modules/host/host.c
new file mode 100644
index 0000000..2a53cd3
--- /dev/null
+++ b/src/modules/host/host.c
@@ -0,0 +1,156 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/host/host.h"
+#include "modules/host/host.h"
+
+bool ffPrintHost(FFHostOptions* options) {
+ bool success = false;
+ FFHostResult host;
+ ffStrbufInit(&host.family);
+ ffStrbufInit(&host.name);
+ ffStrbufInit(&host.version);
+ ffStrbufInit(&host.sku);
+ ffStrbufInit(&host.serial);
+ ffStrbufInit(&host.uuid);
+ ffStrbufInit(&host.vendor);
+
+ const char* error = ffDetectHost(&host);
+ if (error) {
+ ffPrintError(FF_HOST_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (host.name.length == 0 && host.family.length == 0) {
+ ffPrintError(FF_HOST_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "neither product_family nor product_name is set by O.E.M.");
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_HOST_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate();
+
+ if (host.name.length > 0) {
+ ffStrbufAppend(&output, &host.name);
+ } else {
+ ffStrbufAppend(&output, &host.family);
+ }
+
+ if (host.version.length > 0) {
+ ffStrbufAppendF(&output, " (%s)", host.version.chars);
+ }
+
+ ffStrbufPutTo(&output, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_HOST_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(host.family, "family"),
+ FF_ARG(host.name, "name"),
+ FF_ARG(host.version, "version"),
+ FF_ARG(host.sku, "sku"),
+ FF_ARG(host.vendor, "vendor"),
+ FF_ARG(host.serial, "serial"),
+ FF_ARG(host.uuid, "uuid"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&host.family);
+ ffStrbufDestroy(&host.name);
+ ffStrbufDestroy(&host.version);
+ ffStrbufDestroy(&host.sku);
+ ffStrbufDestroy(&host.serial);
+ ffStrbufDestroy(&host.uuid);
+ ffStrbufDestroy(&host.vendor);
+
+ return success;
+}
+
+void ffParseHostJsonObject(FFHostOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_HOST_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateHostJsonConfig(FFHostOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateHostJsonResult(FF_A_UNUSED FFHostOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFHostResult host;
+ ffStrbufInit(&host.family);
+ ffStrbufInit(&host.name);
+ ffStrbufInit(&host.version);
+ ffStrbufInit(&host.sku);
+ ffStrbufInit(&host.serial);
+ ffStrbufInit(&host.uuid);
+ ffStrbufInit(&host.vendor);
+
+ const char* error = ffDetectHost(&host);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ if (host.family.length == 0 && host.name.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "neither product_family nor product_name is set by O.E.M.");
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "family", &host.family);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &host.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &host.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "sku", &host.sku);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &host.vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &host.serial);
+ yyjson_mut_obj_add_strbuf(doc, obj, "uuid", &host.uuid);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&host.family);
+ ffStrbufDestroy(&host.name);
+ ffStrbufDestroy(&host.version);
+ ffStrbufDestroy(&host.sku);
+ ffStrbufDestroy(&host.serial);
+ ffStrbufDestroy(&host.uuid);
+ ffStrbufDestroy(&host.vendor);
+
+ return success;
+}
+
+void ffInitHostOptions(FFHostOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰌢");
+}
+
+void ffDestroyHostOptions(FFHostOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffHostModuleInfo = {
+ .name = FF_HOST_MODULE_NAME,
+ .description = "Print your computer's product name",
+ .initOptions = (void*) ffInitHostOptions,
+ .destroyOptions = (void*) ffDestroyHostOptions,
+ .parseJsonObject = (void*) ffParseHostJsonObject,
+ .printModule = (void*) ffPrintHost,
+ .generateJsonResult = (void*) ffGenerateHostJsonResult,
+ .generateJsonConfig = (void*) ffGenerateHostJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Product family", "family" },
+ { "Product name", "name" },
+ { "Product version", "version" },
+ { "Product sku", "sku" },
+ { "Product vendor", "vendor" },
+ { "Product serial number", "serial" },
+ { "Product uuid", "uuid" },
+ }))
+};
diff --git a/src/modules/host/host.h b/src/modules/host/host.h
new file mode 100644
index 0000000..1f98fd4
--- /dev/null
+++ b/src/modules/host/host.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_HOST_MODULE_NAME "Host"
+
+bool ffPrintHost(FFHostOptions* options);
+void ffInitHostOptions(FFHostOptions* options);
+void ffDestroyHostOptions(FFHostOptions* options);
+
+extern FFModuleBaseInfo ffHostModuleInfo;
diff --git a/src/modules/host/option.h b/src/modules/host/option.h
new file mode 100644
index 0000000..e46c0eb
--- /dev/null
+++ b/src/modules/host/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFHostOptions {
+ FFModuleArgs moduleArgs;
+} FFHostOptions;
+
+static_assert(sizeof(FFHostOptions) <= FF_OPTION_MAX_SIZE, "FFHostOptions size exceeds maximum allowed size");
diff --git a/src/modules/icons/icons.c b/src/modules/icons/icons.c
new file mode 100644
index 0000000..d3764ad
--- /dev/null
+++ b/src/modules/icons/icons.c
@@ -0,0 +1,109 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/icons/icons.h"
+#include "modules/icons/icons.h"
+
+bool ffPrintIcons(FFIconsOptions* options) {
+ bool success = false;
+ FFIconsResult result = {
+ .icons1 = ffStrbufCreate(),
+ .icons2 = ffStrbufCreate(),
+ };
+ const char* error = ffDetectIcons(&result);
+
+ if (error) {
+ ffPrintError(FF_ICONS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_ICONS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (result.icons1.length) {
+ ffStrbufWriteTo(&result.icons1, stdout);
+ }
+ if (result.icons2.length) {
+ if (result.icons1.length) {
+ fputs(", ", stdout);
+ }
+ ffStrbufWriteTo(&result.icons2, stdout);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_ICONS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.icons1, "icons1"),
+ FF_ARG(result.icons2, "icons2"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.icons1);
+ ffStrbufDestroy(&result.icons2);
+
+ return success;
+}
+
+void ffParseIconsJsonObject(FFIconsOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_ICONS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateIconsJsonConfig(FFIconsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateIconsJsonResult(FF_A_UNUSED FFIconsOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFIconsResult result = {
+ .icons1 = ffStrbufCreate(),
+ .icons2 = ffStrbufCreate()
+ };
+ const char* error = ffDetectIcons(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ yyjson_mut_val* icons = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, icons, "icons1", &result.icons1);
+ yyjson_mut_obj_add_strbuf(doc, icons, "icons2", &result.icons2);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.icons1);
+ ffStrbufDestroy(&result.icons2);
+
+ return success;
+}
+
+void ffInitIconsOptions(FFIconsOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyIconsOptions(FFIconsOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffIconsModuleInfo = {
+ .name = FF_ICONS_MODULE_NAME,
+ .description = "Print icon style name",
+ .initOptions = (void*) ffInitIconsOptions,
+ .destroyOptions = (void*) ffDestroyIconsOptions,
+ .parseJsonObject = (void*) ffParseIconsJsonObject,
+ .printModule = (void*) ffPrintIcons,
+ .generateJsonResult = (void*) ffGenerateIconsJsonResult,
+ .generateJsonConfig = (void*) ffGenerateIconsJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Icons part 1", "icons1" },
+ { "Icons part 2", "icons2" },
+ }))
+};
diff --git a/src/modules/icons/icons.h b/src/modules/icons/icons.h
new file mode 100644
index 0000000..a583a22
--- /dev/null
+++ b/src/modules/icons/icons.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_ICONS_MODULE_NAME "Icons"
+
+bool ffPrintIcons(FFIconsOptions* options);
+void ffInitIconsOptions(FFIconsOptions* options);
+void ffDestroyIconsOptions(FFIconsOptions* options);
+
+extern FFModuleBaseInfo ffIconsModuleInfo;
diff --git a/src/modules/icons/option.h b/src/modules/icons/option.h
new file mode 100644
index 0000000..29b82ab
--- /dev/null
+++ b/src/modules/icons/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFIconsOptions {
+ FFModuleArgs moduleArgs;
+} FFIconsOptions;
+
+static_assert(sizeof(FFIconsOptions) <= FF_OPTION_MAX_SIZE, "FFIconsOptions size exceeds maximum allowed size");
diff --git a/src/modules/initsystem/initsystem.c b/src/modules/initsystem/initsystem.c
new file mode 100644
index 0000000..3c3f367
--- /dev/null
+++ b/src/modules/initsystem/initsystem.c
@@ -0,0 +1,120 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/initsystem/initsystem.h"
+#include "modules/initsystem/initsystem.h"
+
+#define FF_INITSYSTEM_DISPLAY_NAME "Init System"
+
+bool ffPrintInitSystem(FFInitSystemOptions* options) {
+ bool success = false;
+ FFInitSystemResult result = {
+ .name = ffStrbufCreate(),
+ .exe = ffStrbufCreate(),
+ .version = ffStrbufCreate(),
+ .pid = 1,
+ };
+
+ const char* error = ffDetectInitSystem(&result);
+
+ if (error) {
+ ffPrintError(FF_INITSYSTEM_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_INITSYSTEM_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result.name, stdout);
+ if (result.version.length) {
+ printf(" %s\n", result.version.chars);
+ } else {
+ putchar('\n');
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_INITSYSTEM_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.name, "name"),
+ FF_ARG(result.exe, "exe"),
+ FF_ARG(result.version, "version"),
+ FF_ARG(result.pid, "pid"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.name);
+ ffStrbufDestroy(&result.exe);
+ ffStrbufDestroy(&result.version);
+
+ return success;
+}
+
+void ffParseInitSystemJsonObject(FFInitSystemOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_INITSYSTEM_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateInitSystemJsonConfig(FFInitSystemOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateInitSystemJsonResult(FF_A_UNUSED FFInitSystemOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFInitSystemResult result = {
+ .name = ffStrbufCreate(),
+ .exe = ffStrbufCreate(),
+ .version = ffStrbufCreate(),
+ .pid = 1,
+ };
+
+ const char* error = ffDetectInitSystem(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &result.name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exe", &result.exe);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+ yyjson_mut_obj_add_uint(doc, obj, "pid", result.pid);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.name);
+ ffStrbufDestroy(&result.exe);
+ ffStrbufDestroy(&result.version);
+ return success;
+}
+
+void ffInitInitSystemOptions(FFInitSystemOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰿄");
+}
+
+void ffDestroyInitSystemOptions(FFInitSystemOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffInitSystemModuleInfo = {
+ .name = FF_INITSYSTEM_MODULE_NAME,
+ .description = "Print init system (pid 1) name and version",
+ .initOptions = (void*) ffInitInitSystemOptions,
+ .destroyOptions = (void*) ffDestroyInitSystemOptions,
+ .parseJsonObject = (void*) ffParseInitSystemJsonObject,
+ .printModule = (void*) ffPrintInitSystem,
+ .generateJsonResult = (void*) ffGenerateInitSystemJsonResult,
+ .generateJsonConfig = (void*) ffGenerateInitSystemJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Init system name", "name" },
+ { "Init system exe path", "exe" },
+ { "Init system version path", "version" },
+ { "Init system pid", "pid" },
+ }))
+};
diff --git a/src/modules/initsystem/initsystem.h b/src/modules/initsystem/initsystem.h
new file mode 100644
index 0000000..381b955
--- /dev/null
+++ b/src/modules/initsystem/initsystem.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_INITSYSTEM_MODULE_NAME "InitSystem"
+
+bool ffPrintInitSystem(FFInitSystemOptions* options);
+void ffInitInitSystemOptions(FFInitSystemOptions* options);
+void ffDestroyInitSystemOptions(FFInitSystemOptions* options);
+
+extern FFModuleBaseInfo ffInitSystemModuleInfo;
diff --git a/src/modules/initsystem/option.h b/src/modules/initsystem/option.h
new file mode 100644
index 0000000..f592849
--- /dev/null
+++ b/src/modules/initsystem/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFInitSystemOptions {
+ FFModuleArgs moduleArgs;
+} FFInitSystemOptions;
+
+static_assert(sizeof(FFInitSystemOptions) <= FF_OPTION_MAX_SIZE, "FFInitSystemOptions size exceeds maximum allowed size");
diff --git a/src/modules/kernel/kernel.c b/src/modules/kernel/kernel.c
new file mode 100644
index 0000000..c405cbf
--- /dev/null
+++ b/src/modules/kernel/kernel.c
@@ -0,0 +1,81 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "modules/kernel/kernel.h"
+
+bool ffPrintKernel(FFKernelOptions* options) {
+ const FFPlatformSysinfo* info = &instance.state.platform.sysinfo;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_KERNEL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ printf("%s %s\n", info->name.chars, info->release.chars);
+ } else {
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+ ffSizeAppendNum(info->pageSize, &str);
+ FF_PRINT_FORMAT_CHECKED(FF_KERNEL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(info->name, "sysname"),
+ FF_ARG(info->release, "release"),
+ FF_ARG(info->version, "version"),
+ FF_ARG(info->architecture, "arch"),
+ FF_ARG(str, "page-size"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseKernelJsonObject(FFKernelOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_KERNEL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateKernelJsonConfig(FFKernelOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateKernelJsonResult(FF_A_UNUSED FFKernelOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFPlatformSysinfo* info = &instance.state.platform.sysinfo;
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "architecture", &info->architecture);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &info->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "release", &info->release);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &info->version);
+ yyjson_mut_obj_add_uint(doc, obj, "pageSize", info->pageSize);
+
+ return true;
+}
+
+void ffInitKernelOptions(FFKernelOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyKernelOptions(FFKernelOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffKernelModuleInfo = {
+ .name = FF_KERNEL_MODULE_NAME,
+ .description = "Print system kernel version",
+ .initOptions = (void*) ffInitKernelOptions,
+ .destroyOptions = (void*) ffDestroyKernelOptions,
+ .parseJsonObject = (void*) ffParseKernelJsonObject,
+ .printModule = (void*) ffPrintKernel,
+ .generateJsonResult = (void*) ffGenerateKernelJsonResult,
+ .generateJsonConfig = (void*) ffGenerateKernelJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Sysname", "sysname" },
+ { "Release", "release" },
+ { "Version", "version" },
+ { "Architecture", "arch" },
+ { "Display version", "display-version" },
+ { "Page size", "page-size" },
+ }))
+};
diff --git a/src/modules/kernel/kernel.h b/src/modules/kernel/kernel.h
new file mode 100644
index 0000000..010ec54
--- /dev/null
+++ b/src/modules/kernel/kernel.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_KERNEL_MODULE_NAME "Kernel"
+
+bool ffPrintKernel(FFKernelOptions* options);
+void ffInitKernelOptions(FFKernelOptions* options);
+void ffDestroyKernelOptions(FFKernelOptions* options);
+
+extern FFModuleBaseInfo ffKernelModuleInfo;
diff --git a/src/modules/kernel/option.h b/src/modules/kernel/option.h
new file mode 100644
index 0000000..a6520a9
--- /dev/null
+++ b/src/modules/kernel/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFKernelOptions {
+ FFModuleArgs moduleArgs;
+} FFKernelOptions;
+
+static_assert(sizeof(FFKernelOptions) <= FF_OPTION_MAX_SIZE, "FFKernelOptions size exceeds maximum allowed size");
diff --git a/src/modules/keyboard/keyboard.c b/src/modules/keyboard/keyboard.c
new file mode 100644
index 0000000..09c3cd5
--- /dev/null
+++ b/src/modules/keyboard/keyboard.c
@@ -0,0 +1,164 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/keyboard/keyboard.h"
+#include "modules/keyboard/keyboard.h"
+
+static void printDevice(FFKeyboardOptions* options, const FFKeyboardDevice* device, uint8_t index) {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_KEYBOARD_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&device->name, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_KEYBOARD_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(device->name, "name"),
+ FF_ARG(device->serial, "serial"),
+ }));
+ }
+}
+
+bool ffPrintKeyboard(FFKeyboardOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectKeyboard(&result);
+
+ if (error) {
+ ffPrintError(FF_KEYBOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (!result.length) {
+ ffPrintError(FF_KEYBOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No devices detected");
+ return false;
+ }
+
+ FF_LIST_AUTO_DESTROY filtered = ffListCreate();
+ FF_LIST_FOR_EACH (FFKeyboardDevice, device, result) {
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ if (!ignored) {
+ FFKeyboardDevice** ptr = FF_LIST_ADD(FFKeyboardDevice*, filtered);
+ *ptr = device;
+ }
+ }
+
+ bool ret = true;
+ if (!filtered.length) {
+ ffPrintError(FF_KEYBOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "All devices are ignored");
+ ret = false;
+ } else {
+ uint8_t index = 0;
+ FF_LIST_FOR_EACH (FFKeyboardDevice*, pdevice, filtered) {
+ FFKeyboardDevice* device = *pdevice;
+ printDevice(options, device, filtered.length > 1 ? ++index : 0);
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+ }
+
+ return ret;
+}
+
+void ffParseKeyboardJsonObject(FFKeyboardOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ignores")) {
+ yyjson_val* elem;
+ size_t eidx, emax;
+ yyjson_arr_foreach (val, eidx, emax, elem) {
+ if (yyjson_is_str(elem)) {
+ FFstrbuf* strbuf = FF_LIST_ADD(FFstrbuf, options->ignores);
+ ffStrbufInitJsonVal(strbuf, elem);
+ }
+ }
+ continue;
+ }
+
+ ffPrintError(FF_KEYBOARD_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateKeyboardJsonConfig(FFKeyboardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ if (options->ignores.length > 0) {
+ yyjson_mut_val* ignores = yyjson_mut_obj_add_arr(doc, module, "ignores");
+ FF_LIST_FOR_EACH (FFstrbuf, strbuf, options->ignores) {
+ yyjson_mut_arr_append(ignores, yyjson_mut_strncpy(doc, strbuf->chars, strbuf->length));
+ }
+ }
+}
+
+bool ffGenerateKeyboardJsonResult(FF_A_UNUSED FFKeyboardOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectKeyboard(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFKeyboardDevice, device, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &device->serial);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &device->name);
+
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ yyjson_mut_obj_add_bool(doc, obj, "ignored", ignored);
+ }
+
+ FF_LIST_FOR_EACH (FFKeyboardDevice, device, result) {
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+
+ return true;
+}
+
+void ffInitKeyboardOptions(FFKeyboardOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ ffListInit(&options->ignores);
+}
+
+void ffDestroyKeyboardOptions(FFKeyboardOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+
+ FF_LIST_FOR_EACH (FFstrbuf, str, options->ignores) {
+ ffStrbufDestroy(str);
+ }
+ ffListDestroy(&options->ignores);
+}
+
+FFModuleBaseInfo ffKeyboardModuleInfo = {
+ .name = FF_KEYBOARD_MODULE_NAME,
+ .description = "List connected keyboards",
+ .initOptions = (void*) ffInitKeyboardOptions,
+ .destroyOptions = (void*) ffDestroyKeyboardOptions,
+ .parseJsonObject = (void*) ffParseKeyboardJsonObject,
+ .printModule = (void*) ffPrintKeyboard,
+ .generateJsonResult = (void*) ffGenerateKeyboardJsonResult,
+ .generateJsonConfig = (void*) ffGenerateKeyboardJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name", "name" },
+ { "Serial number", "serial" },
+ }))
+};
diff --git a/src/modules/keyboard/keyboard.h b/src/modules/keyboard/keyboard.h
new file mode 100644
index 0000000..bd6e69c
--- /dev/null
+++ b/src/modules/keyboard/keyboard.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_KEYBOARD_MODULE_NAME "Keyboard"
+
+bool ffPrintKeyboard(FFKeyboardOptions* options);
+void ffInitKeyboardOptions(FFKeyboardOptions* options);
+void ffDestroyKeyboardOptions(FFKeyboardOptions* options);
+
+extern FFModuleBaseInfo ffKeyboardModuleInfo;
diff --git a/src/modules/keyboard/option.h b/src/modules/keyboard/option.h
new file mode 100644
index 0000000..5d691d5
--- /dev/null
+++ b/src/modules/keyboard/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/FFlist.h"
+
+typedef struct FFKeyboardOptions {
+ FFModuleArgs moduleArgs;
+
+ FFlist ignores; // List of FFstrbuf
+} FFKeyboardOptions;
+
+static_assert(sizeof(FFKeyboardOptions) <= FF_OPTION_MAX_SIZE, "FFKeyboardOptions size exceeds maximum allowed size");
diff --git a/src/modules/lm/lm.c b/src/modules/lm/lm.c
new file mode 100644
index 0000000..a1317d2
--- /dev/null
+++ b/src/modules/lm/lm.c
@@ -0,0 +1,122 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/lm/lm.h"
+#include "modules/lm/lm.h"
+
+bool ffPrintLM(FFLMOptions* options) {
+ bool success = false;
+ FFLMResult result;
+ ffStrbufInit(&result.service);
+ ffStrbufInit(&result.type);
+ ffStrbufInit(&result.version);
+ const char* error = ffDetectLM(&result);
+
+ if (error) {
+ ffPrintError(FF_LM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ goto exit;
+ }
+
+ if (result.service.length == 0) {
+ ffPrintError(FF_LM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No LM service found");
+ goto exit;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_LM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result.service, stdout);
+ if (result.version.length) {
+ printf(" %s", result.version.chars);
+ }
+ if (result.type.length) {
+ printf(" (%s)", result.type.chars);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_LM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.service, "service"),
+ FF_ARG(result.type, "type"),
+ FF_ARG(result.version, "version"),
+ }));
+ }
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.service);
+ ffStrbufDestroy(&result.type);
+ ffStrbufDestroy(&result.version);
+
+ return success;
+}
+
+void ffParseLMJsonObject(FFLMOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_LM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateLMJsonConfig(FFLMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateLMJsonResult(FF_A_UNUSED FFLMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFLMResult result;
+ ffStrbufInit(&result.service);
+ ffStrbufInit(&result.type);
+ ffStrbufInit(&result.version);
+ const char* error = ffDetectLM(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ goto exit;
+ }
+
+ if (result.service.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No LM service found");
+ goto exit;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "service", &result.service);
+ yyjson_mut_obj_add_strbuf(doc, obj, "type", &result.type);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+ success = true;
+
+exit:
+ ffStrbufDestroy(&result.service);
+ ffStrbufDestroy(&result.type);
+ ffStrbufDestroy(&result.version);
+
+ return success;
+}
+
+void ffInitLMOptions(FFLMOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰧨");
+}
+
+void ffDestroyLMOptions(FFLMOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffLMModuleInfo = {
+ .name = FF_LM_MODULE_NAME,
+ .description = "Print login manager (desktop manager) name and version",
+ .initOptions = (void*) ffInitLMOptions,
+ .destroyOptions = (void*) ffDestroyLMOptions,
+ .parseJsonObject = (void*) ffParseLMJsonObject,
+ .printModule = (void*) ffPrintLM,
+ .generateJsonResult = (void*) ffGenerateLMJsonResult,
+ .generateJsonConfig = (void*) ffGenerateLMJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "LM service", "service" },
+ { "LM type", "type" },
+ { "LM version", "version" },
+ }))
+};
diff --git a/src/modules/lm/lm.h b/src/modules/lm/lm.h
new file mode 100644
index 0000000..1ec33b6
--- /dev/null
+++ b/src/modules/lm/lm.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_LM_MODULE_NAME "LM"
+
+bool ffPrintLM(FFLMOptions* options);
+void ffInitLMOptions(FFLMOptions* options);
+void ffDestroyLMOptions(FFLMOptions* options);
+
+extern FFModuleBaseInfo ffLMModuleInfo;
diff --git a/src/modules/lm/option.h b/src/modules/lm/option.h
new file mode 100644
index 0000000..918ae9b
--- /dev/null
+++ b/src/modules/lm/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFLMOptions {
+ FFModuleArgs moduleArgs;
+} FFLMOptions;
+
+static_assert(sizeof(FFLMOptions) <= FF_OPTION_MAX_SIZE, "FFLMOptions size exceeds maximum allowed size");
diff --git a/src/modules/loadavg/loadavg.c b/src/modules/loadavg/loadavg.c
new file mode 100644
index 0000000..32ced12
--- /dev/null
+++ b/src/modules/loadavg/loadavg.c
@@ -0,0 +1,164 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/strutil.h"
+#include "detection/loadavg/loadavg.h"
+#include "detection/cpu/cpu.h"
+#include "modules/loadavg/loadavg.h"
+
+bool ffPrintLoadavg(FFLoadavgOptions* options) {
+ double result[3] = { 0.0 / 0.0, 0.0 / 0.0, 0.0 / 0.0 };
+
+ const char* error = ffDetectLoadavg(result);
+ if (error) {
+ ffPrintError(FF_LOADAVG_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ if (options->compact) {
+ ffPrintLogoAndKey(FF_LOADAVG_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ printf("%.*f, %.*f, %.*f\n", options->ndigits, result[0], options->ndigits, result[1], options->ndigits, result[2]);
+ } else {
+ FFCPUResult cpu = {
+ .temperature = FF_CPU_TEMP_UNSET,
+ .frequencyMax = 0,
+ .frequencyBase = 0,
+ .name = ffStrbufCreate(),
+ .vendor = ffStrbufCreate(),
+ };
+ ffDetectCPU(&(FFCPUOptions) {}, &cpu);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ for (uint32_t index = 0; index < 3; index++) {
+ uint32_t duration = index == 0 ? 1 : index == 1 ? 5
+ : 15;
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufSetF(&buffer, "%s (%d min)", FF_LOADAVG_MODULE_NAME, duration);
+ } else {
+ ffStrbufClear(&buffer);
+ FF_PARSE_FORMAT_STRING_CHECKED(&buffer, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(duration, "duration"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ ffPrintLogoAndKey(buffer.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ ffStrbufClear(&buffer);
+ double percent = result[index] * 100 / cpu.coresOnline;
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&buffer, percent, options->percent, &options->moduleArgs);
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ if (buffer.length > 0) {
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ ffStrbufAppendF(&buffer, "%.*f", options->ndigits, result[index]);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ if (buffer.length > 0) {
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ ffPercentAppendNum(&buffer, percent, options->percent, buffer.length > 0, &options->moduleArgs);
+ }
+
+ ffStrbufPutTo(&buffer, stdout);
+ }
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_LOADAVG_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result[0], "loadavg1"),
+ FF_ARG(result[1], "loadavg2"),
+ FF_ARG(result[2], "loadavg3"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseLoadavgJsonObject(FFLoadavgOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ndigits")) {
+ options->ndigits = (uint8_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compact")) {
+ options->compact = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_LOADAVG_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateLoadavgJsonConfig(FFLoadavgOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_uint(doc, module, "ndigits", options->ndigits);
+
+ yyjson_mut_obj_add_bool(doc, module, "compact", options->compact);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateLoadavgJsonResult(FF_A_UNUSED FFLoadavgOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ double result[3] = { 0.0 / 0.0, 0.0 / 0.0, 0.0 / 0.0 };
+
+ const char* error = ffDetectLoadavg(result);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ for (size_t i = 0; i < 3; i++) {
+ yyjson_mut_arr_add_real(doc, arr, result[i]);
+ }
+
+ return true;
+}
+
+void ffInitLoadavgOptions(FFLoadavgOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+ options->ndigits = 2;
+ options->compact = true;
+}
+
+void ffDestroyLoadavgOptions(FFLoadavgOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffLoadavgModuleInfo = {
+ .name = FF_LOADAVG_MODULE_NAME,
+ .description = "Print system load averages",
+ .initOptions = (void*) ffInitLoadavgOptions,
+ .destroyOptions = (void*) ffDestroyLoadavgOptions,
+ .parseJsonObject = (void*) ffParseLoadavgJsonObject,
+ .printModule = (void*) ffPrintLoadavg,
+ .generateJsonResult = (void*) ffGenerateLoadavgJsonResult,
+ .generateJsonConfig = (void*) ffGenerateLoadavgJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Load average over 1min", "loadavg1" },
+ { "Load average over 5min", "loadavg2" },
+ { "Load average over 15min", "loadavg3" },
+ }))
+};
diff --git a/src/modules/loadavg/loadavg.h b/src/modules/loadavg/loadavg.h
new file mode 100644
index 0000000..9af2786
--- /dev/null
+++ b/src/modules/loadavg/loadavg.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_LOADAVG_MODULE_NAME "Loadavg"
+
+bool ffPrintLoadavg(FFLoadavgOptions* options);
+void ffInitLoadavgOptions(FFLoadavgOptions* options);
+void ffDestroyLoadavgOptions(FFLoadavgOptions* options);
+
+extern FFModuleBaseInfo ffLoadavgModuleInfo;
diff --git a/src/modules/loadavg/option.h b/src/modules/loadavg/option.h
new file mode 100644
index 0000000..b66d422
--- /dev/null
+++ b/src/modules/loadavg/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFLoadavgOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+ uint8_t ndigits;
+ bool compact;
+} FFLoadavgOptions;
+
+static_assert(sizeof(FFLoadavgOptions) <= FF_OPTION_MAX_SIZE, "FFLoadavgOptions size exceeds maximum allowed size");
diff --git a/src/modules/locale/locale.c b/src/modules/locale/locale.c
new file mode 100644
index 0000000..ef97acf
--- /dev/null
+++ b/src/modules/locale/locale.c
@@ -0,0 +1,81 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "detection/locale/locale.h"
+#include "modules/locale/locale.h"
+
+bool ffPrintLocale(FFLocaleOptions* options) {
+ FF_STRBUF_AUTO_DESTROY locale = ffStrbufCreate();
+
+ const char* error = ffDetectLocale(&locale);
+ if (error) {
+ ffPrintError(FF_LOCALE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_LOCALE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&locale, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_LOCALE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(locale, "result") }));
+ }
+
+ return true;
+}
+
+void ffParseLocaleJsonObject(FFLocaleOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_LOCALE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateLocaleJsonConfig(FFLocaleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateLocaleJsonResult(FF_A_UNUSED FFLocaleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_STRBUF_AUTO_DESTROY locale = ffStrbufCreate();
+
+ const char* error = ffDetectLocale(&locale);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ if (locale.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No locale found");
+ return false;
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, module, "result", &locale);
+
+ return true;
+}
+
+void ffInitLocaleOptions(FFLocaleOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyLocaleOptions(FFLocaleOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffLocaleModuleInfo = {
+ .name = FF_LOCALE_MODULE_NAME,
+ .description = "Print system locale name",
+ .initOptions = (void*) ffInitLocaleOptions,
+ .destroyOptions = (void*) ffDestroyLocaleOptions,
+ .parseJsonObject = (void*) ffParseLocaleJsonObject,
+ .printModule = (void*) ffPrintLocale,
+ .generateJsonResult = (void*) ffGenerateLocaleJsonResult,
+ .generateJsonConfig = (void*) ffGenerateLocaleJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Locale code", "result" },
+ }))
+};
diff --git a/src/modules/locale/locale.h b/src/modules/locale/locale.h
new file mode 100644
index 0000000..fd86e42
--- /dev/null
+++ b/src/modules/locale/locale.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_LOCALE_MODULE_NAME "Locale"
+
+bool ffPrintLocale(FFLocaleOptions* options);
+void ffInitLocaleOptions(FFLocaleOptions* options);
+void ffDestroyLocaleOptions(FFLocaleOptions* options);
+
+extern FFModuleBaseInfo ffLocaleModuleInfo;
diff --git a/src/modules/locale/option.h b/src/modules/locale/option.h
new file mode 100644
index 0000000..19cc9f2
--- /dev/null
+++ b/src/modules/locale/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFLocaleOptions {
+ FFModuleArgs moduleArgs;
+} FFLocaleOptions;
+
+static_assert(sizeof(FFLocaleOptions) <= FF_OPTION_MAX_SIZE, "FFLocaleOptions size exceeds maximum allowed size");
diff --git a/src/modules/localip/localip.c b/src/modules/localip/localip.c
new file mode 100644
index 0000000..ef4a110
--- /dev/null
+++ b/src/modules/localip/localip.c
@@ -0,0 +1,443 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/localip/localip.h"
+#include "modules/localip/localip.h"
+
+#define FF_LOCALIP_DISPLAY_NAME "Local IP"
+#pragma GCC diagnostic ignored "-Wsign-conversion"
+
+static int sortIps(const FFLocalIpResult* left, const FFLocalIpResult* right) {
+ return ffStrbufComp(&left->name, &right->name);
+}
+
+static void formatKey(const FFLocalIpOptions* options, FFLocalIpResult* ip, uint32_t index, FFstrbuf* key) {
+ if (options->moduleArgs.key.length == 0) {
+ if (!ip->name.length) {
+ ffStrbufSetF(&ip->name, "unknown %u", (unsigned) index);
+ }
+
+ ffStrbufSetF(key, FF_LOCALIP_DISPLAY_NAME " (%s)", ip->name.chars);
+ } else {
+ ffStrbufClear(key);
+ FF_PARSE_FORMAT_STRING_CHECKED(key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(ip->name, "ifname"),
+ FF_ARG(ip->mac, "mac"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+}
+
+static void appendSpeed(FFLocalIpResult* ip, FFstrbuf* strbuf) {
+ if (ip->speed >= 1000000) {
+ ffStrbufAppendDouble(strbuf, ip->speed / 1e6, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros == FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS);
+ ffStrbufAppendS(strbuf, " Tbps");
+ } else if (ip->speed >= 1000) {
+ ffStrbufAppendDouble(strbuf, ip->speed / 1e3, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros == FF_FRACTION_TRAILING_ZEROS_TYPE_ALWAYS);
+ ffStrbufAppendS(strbuf, " Gbps");
+ } else {
+ ffStrbufAppendF(strbuf, "%u Mbps", (unsigned) ip->speed);
+ }
+}
+
+static void printIp(FFLocalIpResult* ip, bool markDefaultRoute, FFstrbuf* buffer) {
+ if (ip->ipv4.length) {
+ ffStrbufAppend(buffer, &ip->ipv4);
+ }
+ if (ip->ipv6.length) {
+ if (buffer->length) {
+ ffStrbufAppendC(buffer, ' ');
+ }
+ ffStrbufAppend(buffer, &ip->ipv6);
+ }
+ if (ip->mac.length) {
+ if (buffer->length) {
+ ffStrbufAppendF(buffer, " (%s)", ip->mac.chars);
+ } else {
+ ffStrbufAppend(buffer, &ip->mac);
+ }
+ }
+ if (ip->mtu > 0 || ip->speed > 0) {
+ bool flag = buffer->length > 0;
+ if (flag) {
+ ffStrbufAppendS(buffer, " [");
+ }
+ if (ip->speed > 0) {
+ if (ip->mtu > 0) {
+ ffStrbufAppendS(buffer, "Speed ");
+ }
+ appendSpeed(ip, buffer);
+ if (ip->mtu > 0) {
+ ffStrbufAppendS(buffer, " / MTU ");
+ }
+ }
+ if (ip->mtu > 0) {
+ ffStrbufAppendF(buffer, "%u", (unsigned) ip->mtu);
+ }
+ if (flag) {
+ ffStrbufAppendC(buffer, ']');
+ }
+ }
+ if (ip->flags.length) {
+ bool flag = buffer->length > 0;
+ if (flag) {
+ ffStrbufAppendS(buffer, " <");
+ }
+ ffStrbufAppend(buffer, &ip->flags);
+ if (flag) {
+ ffStrbufAppendC(buffer, '>');
+ }
+ }
+ if (markDefaultRoute && ip->defaultRoute) {
+ ffStrbufAppendS(buffer, " *");
+ }
+}
+
+bool ffPrintLocalIp(FFLocalIpOptions* options) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectLocalIps(options, &results);
+
+ if (error) {
+ ffPrintError(FF_LOCALIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (results.length == 0) {
+ ffPrintError(FF_LOCALIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Failed to detect any IPs");
+ return false;
+ }
+
+ ffListSort(&results, sizeof(FFLocalIpResult), (const void*) sortIps);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+
+ if (options->showType & FF_LOCALIP_TYPE_COMPACT_BIT) {
+ ffPrintLogoAndKey(FF_LOCALIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_LIST_FOR_EACH (FFLocalIpResult, ip, results) {
+ if (buffer.length) {
+ ffStrbufAppendS(&buffer, " - ");
+ }
+ printIp(ip, false, &buffer);
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ ffStrbufClear(&buffer);
+ } else {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ uint32_t index = 0;
+
+ FF_LIST_FOR_EACH (FFLocalIpResult, ip, results) {
+ formatKey(options, ip, results.length == 1 ? 0 : index + 1, &key);
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ printIp(ip, !(options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT), &buffer);
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ if (ip->speed > 0) {
+ appendSpeed(ip, &buffer);
+ }
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(ip->ipv4, "ipv4"),
+ FF_ARG(ip->ipv6, "ipv6"),
+ FF_ARG(ip->mac, "mac"),
+ FF_ARG(ip->name, "ifname"),
+ FF_ARG(ip->defaultRoute, "is-default-route"),
+ FF_ARG(ip->mtu, "mtu"),
+ FF_ARG(buffer, "speed"),
+ FF_ARG(ip->flags, "flags"),
+ }));
+ }
+ ++index;
+ ffStrbufClear(&buffer);
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFLocalIpResult, ip, results) {
+ ffStrbufDestroy(&ip->name);
+ ffStrbufDestroy(&ip->ipv4);
+ ffStrbufDestroy(&ip->ipv6);
+ ffStrbufDestroy(&ip->mac);
+ ffStrbufDestroy(&ip->flags);
+ }
+
+ return true;
+}
+
+void ffParseLocalIpJsonObject(FFLocalIpOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showIpv4")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_IPV4_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_IPV4_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showIpv6")) {
+ if (yyjson_is_bool(val)) {
+ options->ipv6Type = FF_LOCALIP_IPV6_TYPE_AUTO;
+ if (unsafe_yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_IPV6_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_IPV6_BIT;
+ }
+ } else if (yyjson_is_str(val)) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "auto", FF_LOCALIP_IPV6_TYPE_AUTO },
+ { "gua", FF_LOCALIP_IPV6_TYPE_GUA_BIT },
+ { "ula", FF_LOCALIP_IPV6_TYPE_ULA_BIT },
+ { "lla", FF_LOCALIP_IPV6_TYPE_LLA_BIT },
+ { "unknown", FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_LOCALIP_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->showType |= FF_LOCALIP_TYPE_IPV6_BIT;
+ options->ipv6Type = (FFLocalIpIpv6Type) value;
+ }
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showMac")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_MAC_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_MAC_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showLoop")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_LOOP_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_LOOP_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showPrefixLen")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_PREFIX_LEN_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_PREFIX_LEN_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showMtu")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_MTU_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_MTU_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showSpeed")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_SPEED_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_SPEED_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showFlags")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_FLAGS_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_FLAGS_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compact")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_COMPACT_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_COMPACT_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "defaultRouteOnly")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showAllIps")) {
+ if (yyjson_get_bool(val)) {
+ options->showType |= FF_LOCALIP_TYPE_ALL_IPS_BIT;
+ } else {
+ options->showType &= ~FF_LOCALIP_TYPE_ALL_IPS_BIT;
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "namePrefix")) {
+ ffStrbufSetJsonVal(&options->namePrefix, val);
+ continue;
+ }
+
+ ffPrintError(FF_LOCALIP_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateLocalIpJsonConfig(FFLocalIpOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "showIpv4", !!(options->showType & FF_LOCALIP_TYPE_IPV4_BIT));
+
+ if (options->ipv6Type == FF_LOCALIP_IPV6_TYPE_AUTO) {
+ yyjson_mut_obj_add_bool(doc, module, "showIpv6", !!(options->showType & FF_LOCALIP_TYPE_IPV6_BIT));
+ } else {
+ const char* str = NULL;
+ switch (options->ipv6Type) {
+ case FF_LOCALIP_IPV6_TYPE_GUA_BIT:
+ str = "gua";
+ break;
+ case FF_LOCALIP_IPV6_TYPE_ULA_BIT:
+ str = "ula";
+ break;
+ case FF_LOCALIP_IPV6_TYPE_LLA_BIT:
+ str = "lla";
+ break;
+ case FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT:
+ str = "unknown";
+ break;
+ default:
+ str = "auto";
+ break;
+ }
+ yyjson_mut_obj_add_str(doc, module, "showIpv6", str);
+ }
+
+ yyjson_mut_obj_add_bool(doc, module, "showMac", !!(options->showType & FF_LOCALIP_TYPE_MAC_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showLoop", !!(options->showType & FF_LOCALIP_TYPE_LOOP_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showPrefixLen", !!(options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showMtu", !!(options->showType & FF_LOCALIP_TYPE_MTU_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showSpeed", !!(options->showType & FF_LOCALIP_TYPE_SPEED_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showFlags", !!(options->showType & FF_LOCALIP_TYPE_FLAGS_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "compact", !!(options->showType & FF_LOCALIP_TYPE_COMPACT_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "defaultRouteOnly", !!(options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT));
+
+ yyjson_mut_obj_add_bool(doc, module, "showAllIps", !!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT));
+
+ yyjson_mut_obj_add_strbuf(doc, module, "namePrefix", &options->namePrefix);
+}
+
+bool ffGenerateLocalIpJsonResult(FF_A_UNUSED FFLocalIpOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectLocalIps(options, &results);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFLocalIpResult, ip, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &ip->name);
+ if (options->showType & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_IPV6_BIT)) {
+ yyjson_mut_val* defaultRoute = yyjson_mut_obj_add_obj(doc, obj, "defaultRoute");
+ if (options->showType & FF_LOCALIP_TYPE_IPV4_BIT) {
+ yyjson_mut_obj_add_bool(doc, defaultRoute, "ipv4", !!(ip->defaultRoute & FF_LOCALIP_TYPE_IPV4_BIT));
+ }
+ if (options->showType & FF_LOCALIP_TYPE_IPV6_BIT) {
+ yyjson_mut_obj_add_bool(doc, defaultRoute, "ipv6", !!(ip->defaultRoute & FF_LOCALIP_TYPE_IPV6_BIT));
+ }
+ }
+ if (options->showType & FF_LOCALIP_TYPE_IPV4_BIT) {
+ yyjson_mut_obj_add_strbuf(doc, obj, "ipv4", &ip->ipv4);
+ }
+ if (options->showType & FF_LOCALIP_TYPE_IPV6_BIT) {
+ yyjson_mut_obj_add_strbuf(doc, obj, "ipv6", &ip->ipv6);
+ }
+ if (options->showType & FF_LOCALIP_TYPE_MAC_BIT) {
+ yyjson_mut_obj_add_strbuf(doc, obj, "mac", &ip->mac);
+ }
+ if (options->showType & FF_LOCALIP_TYPE_MTU_BIT) {
+ yyjson_mut_obj_add_int(doc, obj, "mtu", ip->mtu);
+ }
+ if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) {
+ yyjson_mut_obj_add_int(doc, obj, "speed", ip->speed);
+ }
+ if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) {
+ yyjson_mut_obj_add_strbuf(doc, obj, "flags", &ip->flags);
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFLocalIpResult, ip, results) {
+ ffStrbufDestroy(&ip->name);
+ ffStrbufDestroy(&ip->ipv4);
+ ffStrbufDestroy(&ip->ipv6);
+ ffStrbufDestroy(&ip->mac);
+ ffStrbufDestroy(&ip->flags);
+ }
+
+ return true;
+}
+
+void ffInitLocalIpOptions(FFLocalIpOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰩟");
+
+ options->showType = FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_PREFIX_LEN_BIT
+#if !__ANDROID__ /*Permission denied*/
+ | FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT
+#endif
+ ;
+ options->ipv6Type = FF_LOCALIP_IPV6_TYPE_AUTO;
+ ffStrbufInit(&options->namePrefix);
+}
+
+void ffDestroyLocalIpOptions(FFLocalIpOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->namePrefix);
+}
+
+FFModuleBaseInfo ffLocalIPModuleInfo = {
+ .name = FF_LOCALIP_MODULE_NAME,
+ .description = "List local IP addresses (IPv4 or IPv6), MAC addresses, etc",
+ .initOptions = (void*) ffInitLocalIpOptions,
+ .destroyOptions = (void*) ffDestroyLocalIpOptions,
+ .parseJsonObject = (void*) ffParseLocalIpJsonObject,
+ .printModule = (void*) ffPrintLocalIp,
+ .generateJsonResult = (void*) ffGenerateLocalIpJsonResult,
+ .generateJsonConfig = (void*) ffGenerateLocalIpJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "IPv4 address", "ipv4" },
+ { "IPv6 address", "ipv6" },
+ { "MAC address", "mac" },
+ { "Interface name", "ifname" },
+ { "Is default route", "is-default-route" },
+ { "MTU size in bytes", "mtu" },
+ { "Link speed (formatted)", "speed" },
+ { "Interface flags", "flags" },
+ }))
+};
diff --git a/src/modules/localip/localip.h b/src/modules/localip/localip.h
new file mode 100644
index 0000000..f9e9c4e
--- /dev/null
+++ b/src/modules/localip/localip.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_LOCALIP_MODULE_NAME "LocalIp"
+
+bool ffPrintLocalIp(FFLocalIpOptions* options);
+void ffInitLocalIpOptions(FFLocalIpOptions* options);
+void ffDestroyLocalIpOptions(FFLocalIpOptions* options);
+
+extern FFModuleBaseInfo ffLocalIPModuleInfo;
diff --git a/src/modules/localip/option.h b/src/modules/localip/option.h
new file mode 100644
index 0000000..0eae247
--- /dev/null
+++ b/src/modules/localip/option.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFLocalIpType {
+ FF_LOCALIP_TYPE_NONE,
+ FF_LOCALIP_TYPE_LOOP_BIT = 1 << 0,
+ FF_LOCALIP_TYPE_IPV4_BIT = 1 << 1,
+ FF_LOCALIP_TYPE_IPV6_BIT = 1 << 2,
+ FF_LOCALIP_TYPE_MAC_BIT = 1 << 3,
+ FF_LOCALIP_TYPE_PREFIX_LEN_BIT = 1 << 4,
+ FF_LOCALIP_TYPE_MTU_BIT = 1 << 5,
+ FF_LOCALIP_TYPE_SPEED_BIT = 1 << 6,
+ FF_LOCALIP_TYPE_FLAGS_BIT = 1 << 7,
+
+ FF_LOCALIP_TYPE_COMPACT_BIT = 1 << 10,
+ FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT = 1 << 11,
+ FF_LOCALIP_TYPE_ALL_IPS_BIT = 1 << 12,
+ FF_LOCALIP_TYPE_FORCE_UNSIGNED = UINT16_MAX,
+} FFLocalIpType;
+static_assert(sizeof(FFLocalIpType) == sizeof(uint16_t), "");
+
+typedef enum FF_A_PACKED FFLocalIpIpv6Type {
+ FF_LOCALIP_IPV6_TYPE_NONE = 0b00000000,
+ FF_LOCALIP_IPV6_TYPE_GUA_BIT = 0b00000001,
+ FF_LOCALIP_IPV6_TYPE_ULA_BIT = 0b00000010,
+ FF_LOCALIP_IPV6_TYPE_LLA_BIT = 0b00000100,
+ FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT = 0b00001000, // IPv4-mapped, loopback, etc.
+ FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT = 0b01000000, // Temporary, duplicated, etc.
+ FF_LOCALIP_IPV6_TYPE_PREFERRED_BIT = 0b10000000, // PREFER_SOURCE (manually set)
+ FF_LOCALIP_IPV6_TYPE_TYPE_MASK = 0b00011111,
+ FF_LOCALIP_IPV6_TYPE_AUTO = 0b11111111, // Used for detect option
+} FFLocalIpIpv6Type;
+static_assert(sizeof(FFLocalIpIpv6Type) == sizeof(uint8_t), "");
+
+typedef struct FFLocalIpOptions {
+ FFModuleArgs moduleArgs;
+
+ FFLocalIpType showType;
+ FFLocalIpIpv6Type ipv6Type;
+ FFstrbuf namePrefix;
+} FFLocalIpOptions;
+
+static_assert(sizeof(FFLocalIpOptions) <= FF_OPTION_MAX_SIZE, "FFLocalIpOptions size exceeds maximum allowed size");
diff --git a/src/modules/logo/logo.c b/src/modules/logo/logo.c
new file mode 100644
index 0000000..54a0f97
--- /dev/null
+++ b/src/modules/logo/logo.c
@@ -0,0 +1,87 @@
+#include "common/printing.h"
+#include "logo/logo.h"
+#include "modules/logo/logo.h"
+#include "options/logo.h"
+
+bool ffPrintLogo(FF_A_UNUSED FFLogoOptions* options) {
+ ffPrintError(FF_LOGO_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_DEFAULT, "Supported in JSON format only");
+ return false;
+}
+
+void ffParseLogoJsonObject(FF_A_UNUSED FFLogoOptions* options, FF_A_UNUSED yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
+ continue;
+ }
+
+ ffPrintError(FF_LOGO_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+bool ffGenerateLogoJsonResult(FF_A_UNUSED FFLogoOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFLogoSize size = FF_LOGO_SIZE_UNKNOWN;
+ FFOptionsLogo* logoOptions = &instance.config.logo;
+ if (logoOptions->type == FF_LOGO_TYPE_SMALL) {
+ size = FF_LOGO_SIZE_SMALL;
+ } else if (logoOptions->type != FF_LOGO_TYPE_BUILTIN && logoOptions->type != FF_LOGO_TYPE_AUTO) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Only 'builtin' and 'small' logo types are supported");
+ return false;
+ }
+
+ const FFlogo* logo = logoOptions->source.length > 0
+ ? ffLogoGetBuiltinForName(&logoOptions->source, size)
+ : ffLogoGetBuiltinDetected(size);
+
+ if (!logo) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No built-in logo found for the specified name/size");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ yyjson_mut_obj_add_str(doc, obj, "lines", logo->lines);
+
+ yyjson_mut_val* namesArr = yyjson_mut_obj_add_arr(doc, obj, "names");
+ for (size_t i = 0; i < FASTFETCH_LOGO_MAX_NAMES && logo->names[i]; i++) {
+ yyjson_mut_arr_add_str(doc, namesArr, logo->names[i]);
+ }
+
+ yyjson_mut_val* colorsArr = yyjson_mut_obj_add_arr(doc, obj, "colors");
+ for (size_t i = 0; i < FASTFETCH_LOGO_MAX_COLORS && logo->colors[i]; i++) {
+ yyjson_mut_arr_add_str(doc, colorsArr, logo->colors[i]);
+ }
+
+ yyjson_mut_obj_add_str(doc, obj, "colorKeys", logo->colorKeys);
+ yyjson_mut_obj_add_str(doc, obj, "colorTitle", logo->colorTitle);
+
+ yyjson_mut_val* typeArr = yyjson_mut_obj_add_arr(doc, obj, "type");
+ if (logo->type & FF_LOGO_LINE_TYPE_NORMAL) {
+ yyjson_mut_arr_add_str(doc, typeArr, "normal");
+ }
+ if (logo->type & FF_LOGO_LINE_TYPE_SMALL_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "small");
+ }
+ if (logo->type & FF_LOGO_LINE_TYPE_ALTER_BIT) {
+ yyjson_mut_arr_add_str(doc, typeArr, "alter");
+ }
+
+ return true;
+}
+
+void ffInitLogoOptions(FF_A_UNUSED FFLogoOptions* options) {
+}
+
+void ffDestroyLogoOptions(FF_A_UNUSED FFLogoOptions* options) {
+}
+
+FFModuleBaseInfo ffLogoModuleInfo = {
+ .name = FF_LOGO_MODULE_NAME,
+ .description = "Query built-in logo for JSON output",
+ .initOptions = (void*) ffInitLogoOptions,
+ .destroyOptions = (void*) ffDestroyLogoOptions,
+ .parseJsonObject = (void*) ffParseLogoJsonObject,
+ .printModule = (void*) ffPrintLogo,
+ .generateJsonResult = (void*) ffGenerateLogoJsonResult,
+};
diff --git a/src/modules/logo/logo.h b/src/modules/logo/logo.h
new file mode 100644
index 0000000..7f48e2d
--- /dev/null
+++ b/src/modules/logo/logo.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_LOGO_MODULE_NAME "Logo"
+
+bool ffPrintLogo(FFLogoOptions* options);
+void ffInitLogoOptions(FFLogoOptions* options);
+void ffDestroyLogoOptions(FFLogoOptions* options);
+
+extern FFModuleBaseInfo ffLogoModuleInfo;
diff --git a/src/modules/logo/option.h b/src/modules/logo/option.h
new file mode 100644
index 0000000..605cdfe
--- /dev/null
+++ b/src/modules/logo/option.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFLogoOptions {
+} FFLogoOptions;
+
+static_assert(sizeof(FFLogoOptions) <= FF_OPTION_MAX_SIZE, "FFLogoOptions size exceeds maximum allowed size");
diff --git a/src/modules/media/media.c b/src/modules/media/media.c
new file mode 100644
index 0000000..0876a9d
--- /dev/null
+++ b/src/modules/media/media.c
@@ -0,0 +1,277 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "detection/media/media.h"
+#include "modules/media/media.h"
+
+#include <ctype.h>
+
+static inline bool shouldIgnoreChar(char c) {
+ return isblank(c) || c == '-' || c == '.';
+}
+
+static bool artistInSongTitle(const FFstrbuf* song, const FFstrbuf* artist) {
+ uint32_t artistIndex = 0;
+ uint32_t songIndex = 0;
+
+ while (true) {
+ while (shouldIgnoreChar(song->chars[songIndex])) {
+ ++songIndex;
+ }
+
+ while (shouldIgnoreChar(artist->chars[artistIndex])) {
+ ++artistIndex;
+ }
+
+ if (artist->chars[artistIndex] == '\0') {
+ return true;
+ }
+
+ if (song->chars[songIndex] == '\0') {
+ return false;
+ }
+
+ if (tolower(song->chars[songIndex]) != tolower(artist->chars[artistIndex])) {
+ return false;
+ }
+
+ ++artistIndex;
+ ++songIndex;
+ }
+
+ // Unreachable
+ return false;
+}
+
+static void appendProgress(FFstrbuf* buffer, const FFMediaResult* media) {
+ uint32_t sLen = media->length / 1000, sPos = media->position / 1000;
+ if (sLen > 60 * 60) {
+ ffStrbufAppendF(buffer, "%02u:%02u:%02u / %02u:%02u:%02u", sPos / 3600, (sPos % 3600) / 60, sPos % 60, sLen / 3600, (sLen % 3600) / 60, sLen % 60);
+ } else {
+ ffStrbufAppendF(buffer, "%02u:%02u / %02u:%02u", sPos / 60, sPos % 60, sLen / 60, sLen % 60);
+ }
+}
+
+bool ffPrintMedia(FFMediaOptions* options) {
+ const FFMediaResult* media = ffDetectMedia(false);
+
+ if (media->error.length > 0) {
+ ffPrintError(FF_MEDIA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", media->error.chars);
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY songPretty = ffStrbufCreateCopy(&media->song);
+ const char* removeStrings[] = {
+ "(Official Music Video)",
+ "(Official Video)",
+ "(Music Video)",
+ "(Official HD Video)",
+ "[Official Music Video]",
+ "[Official Video]",
+ "[Music Video]",
+ "[Official HD Video]",
+ "| Official Music Video",
+ "| Official Video",
+ "| Music Video",
+ "| Official HD Video",
+ "[Official Audio]",
+ "[Audio]",
+ "(Audio)",
+ "| Official Audio",
+ "| Audio",
+ "| OFFICIAL AUDIO",
+ "(Lyric Video)",
+ "(Official Lyric Video)",
+ "(Lyrics)",
+ "[Lyric Video]",
+ "[Official Lyric Video]",
+ "[Lyrics]",
+ "| Lyric Video",
+ "| Official Lyric Video",
+ "| Lyrics",
+ };
+ ffStrbufRemoveStrings(&songPretty, ARRAY_SIZE(removeStrings), removeStrings);
+ ffStrbufTrimRight(&songPretty, ' ');
+
+ if (songPretty.length == 0) {
+ ffStrbufAppend(&songPretty, &media->song);
+ }
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ // We don't expose artistPretty to the format, as it might be empty (when the think that the artist is already in the song title)
+ FF_STRBUF_AUTO_DESTROY artistPretty = ffStrbufCreateCopy(&media->artist);
+ ffStrbufRemoveIgnCaseEndS(&artistPretty, " - Topic");
+ ffStrbufRemoveIgnCaseEndS(&artistPretty, "VEVO");
+ ffStrbufTrimRight(&artistPretty, ' ');
+
+ if (artistInSongTitle(&songPretty, &artistPretty)) {
+ ffStrbufClear(&artistPretty);
+ }
+
+ ffPrintLogoAndKey(FF_MEDIA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ if (artistPretty.length > 0) {
+ ffStrbufWriteTo(&artistPretty, stdout);
+ fputs(" - ", stdout);
+ }
+
+ if (media->length > 0) {
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppendS(&songPretty, " - ");
+ appendProgress(&songPretty, media);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffStrbufAppendC(&songPretty, ' ');
+ ffPercentAppendNum(
+ &songPretty,
+ media->position * 100.0 / media->length,
+ options->percent,
+ true,
+ &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffStrbufAppendC(&songPretty, ' ');
+ ffPercentAppendBar(
+ &songPretty,
+ media->position * 100.0 / media->length,
+ options->percent,
+ &options->moduleArgs);
+ }
+ }
+
+ if (media->status.length > 0) {
+ ffStrbufAppendF(&songPretty, " [%s]", media->status.chars);
+ }
+
+ ffStrbufPutTo(&songPretty, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY progress = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (media->length > 0) {
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ appendProgress(&progress, media);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(
+ &percentageNum,
+ media->position * 100.0 / media->length,
+ options->percent,
+ false,
+ &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(
+ &percentageBar,
+ media->position * 100.0 / media->length,
+ options->percent,
+ &options->moduleArgs);
+ }
+ }
+ FF_PRINT_FORMAT_CHECKED(FF_MEDIA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]){
+ FF_ARG(songPretty, "combined"),
+ FF_ARG(media->song, "title"),
+ FF_ARG(media->artist, "artist"),
+ FF_ARG(media->album, "album"),
+ FF_ARG(media->status, "status"),
+ FF_ARG(progress, "progress"),
+ FF_ARG(percentageNum, "progress-num"),
+ FF_ARG(percentageBar, "progress-bar"),
+ FF_ARG(media->player, "player-name"),
+ FF_ARG(media->playerId, "player-id"),
+ FF_ARG(media->url, "url"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseMediaJsonObject(FFMediaOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_MEDIA_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateMediaJsonConfig(FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateMediaJsonResult(FF_A_UNUSED FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFMediaResult* media = ffDetectMedia(false);
+
+ if (media->error.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "error", &media->error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ yyjson_mut_val* song = yyjson_mut_obj_add_obj(doc, obj, "song");
+ yyjson_mut_obj_add_strbuf(doc, song, "name", &media->song);
+ yyjson_mut_obj_add_strbuf(doc, song, "artist", &media->artist);
+ yyjson_mut_obj_add_strbuf(doc, song, "album", &media->album);
+ yyjson_mut_obj_add_strbuf(doc, song, "status", &media->status);
+ yyjson_mut_obj_add_uint(doc, song, "length", media->length);
+ yyjson_mut_obj_add_uint(doc, song, "position", media->position);
+ if (media->cover.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, song, "cover", &media->cover);
+ } else {
+ yyjson_mut_obj_add_null(doc, song, "cover");
+ }
+
+ yyjson_mut_val* player = yyjson_mut_obj_add_obj(doc, obj, "player");
+ yyjson_mut_obj_add_strbuf(doc, player, "name", &media->player);
+ yyjson_mut_obj_add_strbuf(doc, player, "id", &media->playerId);
+ yyjson_mut_obj_add_strbuf(doc, player, "url", &media->url);
+
+ return true;
+}
+
+void ffInitMediaOptions(FFMediaOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->percent = (FFPercentageModuleConfig){ 100, 100, 0 };
+}
+
+void ffDestroyMediaOptions(FFMediaOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffMediaModuleInfo = {
+ .name = FF_MEDIA_MODULE_NAME,
+ .description = "Print the name of the currently playing song",
+ .initOptions = (void*) ffInitMediaOptions,
+ .destroyOptions = (void*) ffDestroyMediaOptions,
+ .parseJsonObject = (void*) ffParseMediaJsonObject,
+ .printModule = (void*) ffPrintMedia,
+ .generateJsonResult = (void*) ffGenerateMediaJsonResult,
+ .generateJsonConfig = (void*) ffGenerateMediaJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]){
+ { "Pretty media name", "combined" },
+ { "Media name", "title" },
+ { "Artist name", "artist" },
+ { "Album name", "album" },
+ { "Status", "status" },
+ { "Progress in text", "progress" },
+ { "Progress in percentage (number)", "progress-num" },
+ { "Progress in percentage (bar)", "progress-bar" },
+ { "Player name", "player-name" },
+ { "Player ID", "player-id" },
+ { "URL", "url" },
+ }))
+};
diff --git a/src/modules/media/media.h b/src/modules/media/media.h
new file mode 100644
index 0000000..8320227
--- /dev/null
+++ b/src/modules/media/media.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_MEDIA_MODULE_NAME "Media"
+
+bool ffPrintMedia(FFMediaOptions* options);
+void ffInitMediaOptions(FFMediaOptions* options);
+void ffDestroyMediaOptions(FFMediaOptions* options);
+
+extern FFModuleBaseInfo ffMediaModuleInfo;
diff --git a/src/modules/media/option.h b/src/modules/media/option.h
new file mode 100644
index 0000000..0207832
--- /dev/null
+++ b/src/modules/media/option.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFMediaOptions {
+ FFModuleArgs moduleArgs;
+ FFPercentageModuleConfig percent;
+} FFMediaOptions;
+
+static_assert(sizeof(FFMediaOptions) <= FF_OPTION_MAX_SIZE, "FFMediaOptions size exceeds maximum allowed size");
diff --git a/src/modules/memory/memory.c b/src/modules/memory/memory.c
new file mode 100644
index 0000000..4fbdf75
--- /dev/null
+++ b/src/modules/memory/memory.c
@@ -0,0 +1,135 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/memory/memory.h"
+#include "modules/memory/memory.h"
+
+bool ffPrintMemory(FFMemoryOptions* options) {
+ FFMemoryResult storage = {};
+ const char* error = ffDetectMemory(&storage);
+
+ if (error) {
+ ffPrintError(FF_MEMORY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
+ ffSizeAppendNum(storage.bytesUsed, &usedPretty);
+
+ FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
+ ffSizeAppendNum(storage.bytesTotal, &totalPretty);
+
+ double percentage = storage.bytesTotal == 0
+ ? 0
+ : (double) storage.bytesUsed / (double) storage.bytesTotal * 100.0;
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_MEMORY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (storage.bytesTotal == 0) {
+ puts("Disabled");
+ } else {
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, percentage, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppendF(&str, "%s / %s ", usedPretty.chars, totalPretty.chars);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&str, percentage, options->percent, str.length > 0, &options->moduleArgs);
+ }
+
+ ffStrbufTrimRight(&str, ' ');
+ ffStrbufPutTo(&str, stdout);
+ }
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentageNum, percentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentageBar, percentage, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_MEMORY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(usedPretty, "used"),
+ FF_ARG(totalPretty, "total"),
+ FF_ARG(percentageNum, "percentage"),
+ FF_ARG(percentageBar, "percentage-bar"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseMemoryJsonObject(FFMemoryOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_MEMORY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateMemoryJsonConfig(FFMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateMemoryJsonResult(FF_A_UNUSED FFMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFMemoryResult storage = {};
+ const char* error = ffDetectMemory(&storage);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_uint(doc, obj, "total", storage.bytesTotal);
+ yyjson_mut_obj_add_uint(doc, obj, "used", storage.bytesUsed);
+
+ return true;
+}
+
+void ffInitMemoryOptions(FFMemoryOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+}
+
+void ffDestroyMemoryOptions(FFMemoryOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffMemoryModuleInfo = {
+ .name = FF_MEMORY_MODULE_NAME,
+ .description = "Print system memory usage information",
+ .initOptions = (void*) ffInitMemoryOptions,
+ .destroyOptions = (void*) ffDestroyMemoryOptions,
+ .parseJsonObject = (void*) ffParseMemoryJsonObject,
+ .printModule = (void*) ffPrintMemory,
+ .generateJsonResult = (void*) ffGenerateMemoryJsonResult,
+ .generateJsonConfig = (void*) ffGenerateMemoryJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Used size", "used" },
+ { "Total size", "total" },
+ { "Percentage used (num)", "percentage" },
+ { "Percentage used (bar)", "percentage-bar" },
+ }))
+};
diff --git a/src/modules/memory/memory.h b/src/modules/memory/memory.h
new file mode 100644
index 0000000..7ed026f
--- /dev/null
+++ b/src/modules/memory/memory.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_MEMORY_MODULE_NAME "Memory"
+
+bool ffPrintMemory(FFMemoryOptions* options);
+void ffInitMemoryOptions(FFMemoryOptions* options);
+void ffDestroyMemoryOptions(FFMemoryOptions* options);
+
+extern FFModuleBaseInfo ffMemoryModuleInfo;
diff --git a/src/modules/memory/option.h b/src/modules/memory/option.h
new file mode 100644
index 0000000..e612a98
--- /dev/null
+++ b/src/modules/memory/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFMemoryOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+} FFMemoryOptions;
+
+static_assert(sizeof(FFMemoryOptions) <= FF_OPTION_MAX_SIZE, "FFMemoryOptions size exceeds maximum allowed size");
diff --git a/src/modules/modules.c b/src/modules/modules.c
new file mode 100644
index 0000000..8378711
--- /dev/null
+++ b/src/modules/modules.c
@@ -0,0 +1,364 @@
+#include "modules/modules.h"
+
+// Note:
+// FF_MODULE_DISABLE_<module> only controls if the module is registered,
+// the module code itself is still compiled.
+// We rely `LTO` to remove the unused code (only enabled in Release mode)
+
+static FFModuleBaseInfo* A[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* B[] = {
+#if !FF_MODULE_DISABLE_BATTERY
+ &ffBatteryModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BIOS
+ &ffBiosModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BLUETOOTH
+ &ffBluetoothModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BLUETOOTHRADIO
+ &ffBluetoothRadioModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BOARD
+ &ffBoardModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BOOTMGR
+ &ffBootmgrModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BREAK
+ &ffBreakModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BRIGHTNESS
+ &ffBrightnessModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_BTRFS
+ &ffBtrfsModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* C[] = {
+#if !FF_MODULE_DISABLE_CAMERA
+ &ffCameraModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CHASSIS
+ &ffChassisModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CODEC
+ &ffCodecModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_COMMAND
+ &ffCommandModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_COLORS
+ &ffColorsModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CPU
+ &ffCPUModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CPUCACHE
+ &ffCPUCacheModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CPUUSAGE
+ &ffCPUUsageModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CURSOR
+ &ffCursorModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_CUSTOM
+ &ffCustomModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* D[] = {
+#if !FF_MODULE_DISABLE_DATETIME
+ &ffDateTimeModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_DE
+ &ffDEModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_DISPLAY
+ &ffDisplayModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_DISK
+ &ffDiskModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_DISKIO
+ &ffDiskIOModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_DNS
+ &ffDNSModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* E[] = {
+#if !FF_MODULE_DISABLE_EDITOR
+ &ffEditorModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* F[] = {
+#if !FF_MODULE_DISABLE_FONT
+ &ffFontModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* G[] = {
+#if !FF_MODULE_DISABLE_GAMEPAD
+ &ffGamepadModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_GPU
+ &ffGPUModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* H[] = {
+#if !FF_MODULE_DISABLE_HOST
+ &ffHostModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* I[] = {
+#if !FF_MODULE_DISABLE_ICONS
+ &ffIconsModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_INITSYSTEM
+ &ffInitSystemModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* J[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* K[] = {
+#if !FF_MODULE_DISABLE_KERNEL
+ &ffKernelModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_KEYBOARD
+ &ffKeyboardModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* L[] = {
+#if !FF_MODULE_DISABLE_LM
+ &ffLMModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_LOADAVG
+ &ffLoadavgModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_LOCALE
+ &ffLocaleModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_LOCALIP
+ &ffLocalIPModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_LOGO
+ &ffLogoModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* M[] = {
+#if !FF_MODULE_DISABLE_MEDIA
+ &ffMediaModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_MEMORY
+ &ffMemoryModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_MONITOR
+ &ffMonitorModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_MOUSE
+ &ffMouseModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* N[] = {
+#if !FF_MODULE_DISABLE_NETIO
+ &ffNetIOModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* O[] = {
+#if !FF_MODULE_DISABLE_OPENCL
+ &ffOpenCLModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_OPENGL
+ &ffOpenGLModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_OS
+ &ffOSModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* P[] = {
+#if !FF_MODULE_DISABLE_PACKAGES
+ &ffPackagesModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_PHYSICALDISK
+ &ffPhysicalDiskModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_PHYSICALMEMORY
+ &ffPhysicalMemoryModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_PLAYER
+ &ffPlayerModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_POWERADAPTER
+ &ffPowerAdapterModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_PROCESSES
+ &ffProcessesModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_PUBLICIP
+ &ffPublicIPModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* Q[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* R[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* S[] = {
+#if !FF_MODULE_DISABLE_SEPARATOR
+ &ffSeparatorModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_SHELL
+ &ffShellModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_SOUND
+ &ffSoundModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_SWAP
+ &ffSwapModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* T[] = {
+#if !FF_MODULE_DISABLE_TERMINAL
+ &ffTerminalModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_TERMINALFONT
+ &ffTerminalFontModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_TERMINALSIZE
+ &ffTerminalSizeModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_TERMINALTHEME
+ &ffTerminalThemeModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_TITLE
+ &ffTitleModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_THEME
+ &ffThemeModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_TPM
+ &ffTPMModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* U[] = {
+#if !FF_MODULE_DISABLE_UPTIME
+ &ffUptimeModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_USERS
+ &ffUsersModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* V[] = {
+#if !FF_MODULE_DISABLE_VERSION
+ &ffVersionModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_VULKAN
+ &ffVulkanModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* W[] = {
+#if !FF_MODULE_DISABLE_WALLPAPER
+ &ffWallpaperModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_WEATHER
+ &ffWeatherModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_WM
+ &ffWMModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_WIFI
+ &ffWifiModuleInfo,
+#endif
+#if !FF_MODULE_DISABLE_WMTHEME
+ &ffWMThemeModuleInfo,
+#endif
+ NULL,
+};
+
+static FFModuleBaseInfo* X[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* Y[] = {
+ NULL,
+};
+
+static FFModuleBaseInfo* Z[] = {
+#if !FF_MODULE_DISABLE_ZPOOL
+ &ffZpoolModuleInfo,
+#endif
+ NULL,
+};
+
+FFModuleBaseInfo** ffModuleInfos[] = {
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+};
diff --git a/src/modules/modules.h b/src/modules/modules.h
new file mode 100644
index 0000000..37ee242
--- /dev/null
+++ b/src/modules/modules.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include "modules/battery/battery.h"
+#include "modules/bios/bios.h"
+#include "modules/bluetooth/bluetooth.h"
+#include "modules/bluetoothradio/bluetoothradio.h"
+#include "modules/brightness/brightness.h"
+#include "modules/board/board.h"
+#include "modules/bootmgr/bootmgr.h"
+#include "modules/break/break.h"
+#include "modules/btrfs/btrfs.h"
+#include "modules/camera/camera.h"
+#include "modules/chassis/chassis.h"
+#include "modules/cpu/cpu.h"
+#include "modules/cpucache/cpucache.h"
+#include "modules/cpuusage/cpuusage.h"
+#include "modules/command/command.h"
+#include "modules/codec/codec.h"
+#include "modules/colors/colors.h"
+#include "modules/cursor/cursor.h"
+#include "modules/custom/custom.h"
+#include "modules/datetime/datetime.h"
+#include "modules/disk/disk.h"
+#include "modules/diskio/diskio.h"
+#include "modules/display/display.h"
+#include "modules/de/de.h"
+#include "modules/dns/dns.h"
+#include "modules/editor/editor.h"
+#include "modules/font/font.h"
+#include "modules/gamepad/gamepad.h"
+#include "modules/gpu/gpu.h"
+#include "modules/host/host.h"
+#include "modules/icons/icons.h"
+#include "modules/initsystem/initsystem.h"
+#include "modules/kernel/kernel.h"
+#include "modules/keyboard/keyboard.h"
+#include "modules/lm/lm.h"
+#include "modules/loadavg/loadavg.h"
+#include "modules/locale/locale.h"
+#include "modules/localip/localip.h"
+#include "modules/logo/logo.h"
+#include "modules/media/media.h"
+#include "modules/memory/memory.h"
+#include "modules/monitor/monitor.h"
+#include "modules/mouse/mouse.h"
+#include "modules/netio/netio.h"
+#include "modules/opengl/opengl.h"
+#include "modules/opencl/opencl.h"
+#include "modules/os/os.h"
+#include "modules/packages/packages.h"
+#include "modules/physicaldisk/physicaldisk.h"
+#include "modules/physicalmemory/physicalmemory.h"
+#include "modules/player/player.h"
+#include "modules/poweradapter/poweradapter.h"
+#include "modules/processes/processes.h"
+#include "modules/publicip/publicip.h"
+#include "modules/separator/separator.h"
+#include "modules/shell/shell.h"
+#include "modules/sound/sound.h"
+#include "modules/swap/swap.h"
+#include "modules/terminal/terminal.h"
+#include "modules/terminalfont/terminalfont.h"
+#include "modules/terminalsize/terminalsize.h"
+#include "modules/terminaltheme/terminaltheme.h"
+#include "modules/theme/theme.h"
+#include "modules/title/title.h"
+#include "modules/tpm/tpm.h"
+#include "modules/uptime/uptime.h"
+#include "modules/users/users.h"
+#include "modules/version/version.h"
+#include "modules/vulkan/vulkan.h"
+#include "modules/wallpaper/wallpaper.h"
+#include "modules/weather/weather.h"
+#include "modules/wifi/wifi.h"
+#include "modules/wm/wm.h"
+#include "modules/wmtheme/wmtheme.h"
+#include "modules/zpool/zpool.h"
diff --git a/src/modules/monitor/monitor.c b/src/modules/monitor/monitor.c
new file mode 100644
index 0000000..105ee67
--- /dev/null
+++ b/src/modules/monitor/monitor.c
@@ -0,0 +1,137 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/displayserver/displayserver.h"
+#include "modules/monitor/monitor.h"
+
+#include <math.h>
+
+bool ffPrintMonitor(FFMonitorOptions* options) {
+ const FFDisplayServerResult* result = ffConnectDisplayServer();
+
+ if (!result->displays.length) {
+ ffPrintError(FF_MONITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No display detected");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ uint32_t index = 0;
+ FF_LIST_FOR_EACH (FFDisplayResult, display, result->displays) {
+ double inch = sqrt(display->physicalWidth * display->physicalWidth + display->physicalHeight * display->physicalHeight) / 25.4;
+ double ppi = sqrt(display->width * display->width + display->height * display->height) / inch;
+ bool hdrCompatible = display->hdrStatus == FF_DISPLAY_HDR_STATUS_SUPPORTED || display->hdrStatus == FF_DISPLAY_HDR_STATUS_ENABLED;
+
+ ffStrbufClear(&key);
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufAppendS(&key, FF_MONITOR_MODULE_NAME);
+ if (display->name.length > 0) {
+ ffStrbufAppendF(&key, " (%s)", display->name.chars);
+ }
+ } else {
+ uint32_t moduleIndex = result->displays.length == 1 ? 0 : index + 1;
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(moduleIndex, "index"),
+ FF_ARG(display->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ printf("%ux%u px", display->width, display->height);
+ if (display->refreshRate > 0) {
+ printf(" @ %g Hz", ((int) (display->refreshRate * 1000 + 0.5)) / 1000.0);
+ }
+ if (inch > 0) {
+ printf(" - %ux%u mm (%.2f inches, %.2f ppi)", display->physicalWidth, display->physicalHeight, inch, ppi);
+ }
+ if (hdrCompatible) {
+ fputs(" [HDR Compatible]", stdout);
+ }
+ putchar('\n');
+ } else {
+ char buf[32];
+ if (display->serial) {
+ const uint8_t* nums = (uint8_t*) &display->serial;
+ snprintf(buf, sizeof(buf), "%2X-%2X-%2X-%2X", nums[0], nums[1], nums[2], nums[3]);
+ } else {
+ buf[0] = '\0';
+ }
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(display->name, "name"),
+ FF_ARG(display->width, "width"),
+ FF_ARG(display->height, "height"),
+ FF_ARG(display->physicalWidth, "physical-width"),
+ FF_ARG(display->physicalHeight, "physical-height"),
+ FF_ARG(inch, "inch"),
+ FF_ARG(ppi, "ppi"),
+ FF_ARG(display->manufactureYear, "manufacture-year"),
+ FF_ARG(display->manufactureWeek, "manufacture-week"),
+ FF_ARG(buf, "serial"),
+ FF_ARG(display->refreshRate, "refresh-rate"),
+ FF_ARG(hdrCompatible, "hdr-compatible"),
+ }));
+ }
+
+ ffStrbufDestroy(&display->name);
+ ++index;
+ }
+
+ return true;
+}
+
+void ffParseMonitorJsonObject(FFMonitorOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_MONITOR_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateMonitorJsonConfig(FFMonitorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateMonitorJsonResult(FF_A_UNUSED FFMonitorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Monitor module is an alias of Display module");
+ return false;
+}
+
+void ffInitMonitorOptions(FFMonitorOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰹑");
+}
+
+void ffDestroyMonitorOptions(FFMonitorOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffMonitorModuleInfo = {
+ .name = FF_MONITOR_MODULE_NAME,
+ .description = "Same as Display module, but with a different default output format",
+ .initOptions = (void*) ffInitMonitorOptions,
+ .destroyOptions = (void*) ffDestroyMonitorOptions,
+ .parseJsonObject = (void*) ffParseMonitorJsonObject,
+ .printModule = (void*) ffPrintMonitor,
+ .generateJsonResult = (void*) ffGenerateMonitorJsonResult,
+ .generateJsonConfig = (void*) ffGenerateMonitorJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Display name", "name" },
+ { "Native resolution width in pixels", "width" },
+ { "Native resolution height in pixels", "height" },
+ { "Physical width in millimeters", "physical-width" },
+ { "Physical height in millimeters", "physical-height" },
+ { "Physical diagonal length in inches", "inch" },
+ { "Pixels per inch (PPI)", "ppi" },
+ { "Year of manufacturing", "manufacture-year" },
+ { "Nth week of manufacturing in the year", "manufacture-week" },
+ { "Serial number", "serial" },
+ { "Maximum refresh rate in Hz", "refresh-rate" },
+ { "True if the display is HDR compatible", "hdr-compatible" },
+ }))
+};
diff --git a/src/modules/monitor/monitor.h b/src/modules/monitor/monitor.h
new file mode 100644
index 0000000..82f9491
--- /dev/null
+++ b/src/modules/monitor/monitor.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_MONITOR_MODULE_NAME "Monitor"
+
+bool ffPrintMonitor(FFMonitorOptions* options);
+void ffInitMonitorOptions(FFMonitorOptions* options);
+void ffDestroyMonitorOptions(FFMonitorOptions* options);
+
+extern FFModuleBaseInfo ffMonitorModuleInfo;
diff --git a/src/modules/monitor/option.h b/src/modules/monitor/option.h
new file mode 100644
index 0000000..46f4859
--- /dev/null
+++ b/src/modules/monitor/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFMonitorOptions {
+ FFModuleArgs moduleArgs;
+} FFMonitorOptions;
+
+static_assert(sizeof(FFMonitorOptions) <= FF_OPTION_MAX_SIZE, "FFMonitorOptions size exceeds maximum allowed size");
diff --git a/src/modules/mouse/mouse.c b/src/modules/mouse/mouse.c
new file mode 100644
index 0000000..abc4678
--- /dev/null
+++ b/src/modules/mouse/mouse.c
@@ -0,0 +1,167 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/mouse/mouse.h"
+#include "modules/mouse/mouse.h"
+
+static void printDevice(FFMouseOptions* options, const FFMouseDevice* device, uint8_t index) {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_MOUSE_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&device->name, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_MOUSE_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(device->name, "name"),
+ FF_ARG(device->serial, "serial"),
+ }));
+ }
+}
+
+bool ffPrintMouse(FFMouseOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectMouse(&result);
+
+ if (error) {
+ ffPrintError(FF_MOUSE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (!result.length) {
+ ffPrintError(FF_MOUSE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No devices detected");
+ return false;
+ }
+
+ FF_LIST_AUTO_DESTROY filtered = ffListCreate();
+ FF_LIST_FOR_EACH (FFMouseDevice, device, result) {
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ if (!ignored) {
+ FFMouseDevice** ptr = FF_LIST_ADD(FFMouseDevice*, filtered);
+ *ptr = device;
+ }
+ }
+
+ bool ret = true;
+ if (!filtered.length) {
+ ffPrintError(FF_MOUSE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "All devices are ignored");
+ ret = false;
+ } else {
+ uint8_t index = 0;
+ FF_LIST_FOR_EACH (FFMouseDevice*, pdevice, filtered) {
+ FFMouseDevice* device = *pdevice;
+ printDevice(options, device, filtered.length > 1 ? ++index : 0);
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFMouseDevice, device, result) {
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+
+ return ret;
+}
+
+void ffParseMouseJsonObject(FFMouseOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ignores")) {
+ yyjson_val* elem;
+ size_t eidx, emax;
+ yyjson_arr_foreach (val, eidx, emax, elem) {
+ if (yyjson_is_str(elem)) {
+ FFstrbuf* strbuf = FF_LIST_ADD(FFstrbuf, options->ignores);
+ ffStrbufInitJsonVal(strbuf, elem);
+ }
+ }
+ continue;
+ }
+
+ ffPrintError(FF_MOUSE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateMouseJsonConfig(FFMouseOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ if (options->ignores.length > 0) {
+ yyjson_mut_val* ignores = yyjson_mut_obj_add_arr(doc, module, "ignores");
+ FF_LIST_FOR_EACH (FFstrbuf, strbuf, options->ignores) {
+ yyjson_mut_arr_append(ignores, yyjson_mut_strncpy(doc, strbuf->chars, strbuf->length));
+ }
+ }
+}
+
+bool ffGenerateMouseJsonResult(FF_A_UNUSED FFMouseOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectMouse(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFMouseDevice, device, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &device->serial);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &device->name);
+
+ bool ignored = false;
+ FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
+ if (ffStrbufStartsWithIgnCase(&device->name, ignore)) {
+ ignored = true;
+ break;
+ }
+ }
+ yyjson_mut_obj_add_bool(doc, obj, "ignored", ignored);
+ }
+
+ FF_LIST_FOR_EACH (FFMouseDevice, device, result) {
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->name);
+ }
+
+ return true;
+}
+
+void ffInitMouseOptions(FFMouseOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰍽");
+
+ ffListInit(&options->ignores);
+}
+
+void ffDestroyMouseOptions(FFMouseOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+
+ FF_LIST_FOR_EACH (FFstrbuf, str, options->ignores) {
+ ffStrbufDestroy(str);
+ }
+ ffListDestroy(&options->ignores);
+}
+
+FFModuleBaseInfo ffMouseModuleInfo = {
+ .name = FF_MOUSE_MODULE_NAME,
+ .description = "List connected mice",
+ .initOptions = (void*) ffInitMouseOptions,
+ .destroyOptions = (void*) ffDestroyMouseOptions,
+ .parseJsonObject = (void*) ffParseMouseJsonObject,
+ .printModule = (void*) ffPrintMouse,
+ .generateJsonResult = (void*) ffGenerateMouseJsonResult,
+ .generateJsonConfig = (void*) ffGenerateMouseJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Mouse name", "name" },
+ { "Mouse serial number", "serial" },
+ }))
+};
diff --git a/src/modules/mouse/mouse.h b/src/modules/mouse/mouse.h
new file mode 100644
index 0000000..afbc20f
--- /dev/null
+++ b/src/modules/mouse/mouse.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_MOUSE_MODULE_NAME "Mouse"
+
+bool ffPrintMouse(FFMouseOptions* options);
+void ffInitMouseOptions(FFMouseOptions* options);
+void ffDestroyMouseOptions(FFMouseOptions* options);
+
+extern FFModuleBaseInfo ffMouseModuleInfo;
diff --git a/src/modules/mouse/option.h b/src/modules/mouse/option.h
new file mode 100644
index 0000000..ec2c26e
--- /dev/null
+++ b/src/modules/mouse/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/FFlist.h"
+
+typedef struct FFMouseOptions {
+ FFModuleArgs moduleArgs;
+
+ FFlist ignores; // List of FFstrbuf
+} FFMouseOptions;
+
+static_assert(sizeof(FFMouseOptions) <= FF_OPTION_MAX_SIZE, "FFMouseOptions size exceeds maximum allowed size");
diff --git a/src/modules/netio/netio.c b/src/modules/netio/netio.c
new file mode 100644
index 0000000..17047ff
--- /dev/null
+++ b/src/modules/netio/netio.c
@@ -0,0 +1,224 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/netio/netio.h"
+#include "modules/netio/netio.h"
+
+#define FF_NETIO_DISPLAY_NAME "Network IO"
+
+static int sortInfs(const FFNetIOResult* left, const FFNetIOResult* right) {
+ return ffStrbufComp(&left->name, &right->name);
+}
+
+static void formatKey(const FFNetIOOptions* options, FFNetIOResult* inf, uint32_t index, FFstrbuf* key) {
+ if (options->moduleArgs.key.length == 0) {
+ if (!inf->name.length) {
+ ffStrbufSetF(&inf->name, "unknown %u", (unsigned) index);
+ }
+
+ ffStrbufSetF(key, FF_NETIO_DISPLAY_NAME " (%s)", inf->name.chars);
+ } else {
+ ffStrbufClear(key);
+ FF_PARSE_FORMAT_STRING_CHECKED(key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(inf->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+}
+
+bool ffPrintNetIO(FFNetIOOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectNetIO(&result, options);
+
+ if (error) {
+ ffPrintError(FF_NETIO_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ ffListSort(&result, sizeof(FFNetIOResult), (const void*) sortInfs);
+
+ uint32_t index = 0;
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY buffer2 = ffStrbufCreate();
+
+ FF_LIST_FOR_EACH (FFNetIOResult, inf, result) {
+ formatKey(options, inf, result.length == 1 ? 0 : index + 1, &key);
+ ffStrbufClear(&buffer);
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ ffSizeAppendNum(inf->rxBytes, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffStrbufAppendS(&buffer, " (IN) - ");
+
+ ffSizeAppendNum(inf->txBytes, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffStrbufAppendS(&buffer, " (OUT)");
+
+ if (inf->defaultRoute && !options->defaultRouteOnly) {
+ ffStrbufAppendS(&buffer, " *");
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ ffStrbufClear(&buffer2);
+ ffSizeAppendNum(inf->rxBytes, &buffer);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer, "/s");
+ }
+ ffSizeAppendNum(inf->txBytes, &buffer2);
+ if (!options->detectTotal) {
+ ffStrbufAppendS(&buffer2, "/s");
+ }
+
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(buffer, "rx-size"),
+ FF_ARG(buffer2, "tx-size"),
+ FF_ARG(inf->name, "ifname"),
+ FF_ARG(inf->defaultRoute, "is-default-route"),
+ FF_ARG(inf->txBytes, "tx-bytes"),
+ FF_ARG(inf->rxBytes, "rx-bytes"),
+ FF_ARG(inf->txPackets, "tx-packets"),
+ FF_ARG(inf->rxPackets, "rx-packets"),
+ FF_ARG(inf->rxErrors, "rx-errors"),
+ FF_ARG(inf->txErrors, "tx-errors"),
+ FF_ARG(inf->rxDrops, "rx-drops"),
+ FF_ARG(inf->txDrops, "tx-drops"),
+ }));
+ }
+ ++index;
+ }
+
+ FF_LIST_FOR_EACH (FFNetIOResult, inf, result) {
+ ffStrbufDestroy(&inf->name);
+ }
+
+ return true;
+}
+
+void ffParseNetIOJsonObject(FFNetIOOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "namePrefix")) {
+ ffStrbufSetJsonVal(&options->namePrefix, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "defaultRouteOnly")) {
+ options->defaultRouteOnly = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "detectTotal")) {
+ options->detectTotal = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "waitTime")) {
+ options->waitTime = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ ffPrintError(FF_NETIO_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateNetIOJsonConfig(FFNetIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "namePrefix", &options->namePrefix);
+
+ yyjson_mut_obj_add_bool(doc, module, "defaultRouteOnly", options->defaultRouteOnly);
+
+ yyjson_mut_obj_add_bool(doc, module, "detectTotal", options->detectTotal);
+
+ yyjson_mut_obj_add_uint(doc, module, "waitTime", options->waitTime);
+}
+
+bool ffGenerateNetIOJsonResult(FFNetIOOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectNetIO(&result, options);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFNetIOResult, counter, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &counter->name);
+ yyjson_mut_obj_add_bool(doc, obj, "defaultRoute", counter->defaultRoute);
+ yyjson_mut_obj_add_uint(doc, obj, "txBytes", counter->txBytes);
+ yyjson_mut_obj_add_uint(doc, obj, "rxBytes", counter->rxBytes);
+ yyjson_mut_obj_add_uint(doc, obj, "txPackets", counter->txPackets);
+ yyjson_mut_obj_add_uint(doc, obj, "rxPackets", counter->rxPackets);
+ yyjson_mut_obj_add_uint(doc, obj, "rxErrors", counter->rxErrors);
+ yyjson_mut_obj_add_uint(doc, obj, "txErrors", counter->txErrors);
+ yyjson_mut_obj_add_uint(doc, obj, "rxDrops", counter->rxDrops);
+ yyjson_mut_obj_add_uint(doc, obj, "txDrops", counter->txDrops);
+ }
+
+ FF_LIST_FOR_EACH (FFNetIOResult, inf, result) {
+ ffStrbufDestroy(&inf->name);
+ }
+
+ return true;
+}
+
+void ffInitNetIOOptions(FFNetIOOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰾆");
+
+ ffStrbufInit(&options->namePrefix);
+ options->defaultRouteOnly =
+#if __ANDROID__
+ false
+#else
+ true
+#endif
+ ;
+ options->detectTotal = false;
+ options->waitTime = 1000;
+}
+
+void ffDestroyNetIOOptions(FFNetIOOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->namePrefix);
+}
+
+FFModuleBaseInfo ffNetIOModuleInfo = {
+ .name = FF_NETIO_MODULE_NAME,
+ .description = "Print network I/O throughput",
+ .initOptions = (void*) ffInitNetIOOptions,
+ .destroyOptions = (void*) ffDestroyNetIOOptions,
+ .parseJsonObject = (void*) ffParseNetIOJsonObject,
+ .printModule = (void*) ffPrintNetIO,
+ .generateJsonResult = (void*) ffGenerateNetIOJsonResult,
+ .generateJsonConfig = (void*) ffGenerateNetIOJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Size of data received [per second] (formatted)", "rx-size" },
+ { "Size of data sent [per second] (formatted)", "tx-size" },
+ { "Interface name", "ifname" },
+ { "Is default route", "is-default-route" },
+ { "Size of data received [per second] (in bytes)", "rx-bytes" },
+ { "Size of data sent [per second] (in bytes)", "tx-bytes" },
+ { "Number of packets received [per second]", "rx-packets" },
+ { "Number of packets sent [per second]", "tx-packets" },
+ { "Number of errors received [per second]", "rx-errors" },
+ { "Number of errors sent [per second]", "tx-errors" },
+ { "Number of packets dropped when receiving [per second]", "rx-drops" },
+ { "Number of packets dropped when sending [per second]", "tx-drops" },
+ }))
+};
diff --git a/src/modules/netio/netio.h b/src/modules/netio/netio.h
new file mode 100644
index 0000000..653b7e9
--- /dev/null
+++ b/src/modules/netio/netio.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_NETIO_MODULE_NAME "NetIO"
+
+void ffPrepareNetIO(FFNetIOOptions* options);
+
+bool ffPrintNetIO(FFNetIOOptions* options);
+void ffInitNetIOOptions(FFNetIOOptions* options);
+void ffDestroyNetIOOptions(FFNetIOOptions* options);
+
+extern FFModuleBaseInfo ffNetIOModuleInfo;
diff --git a/src/modules/netio/option.h b/src/modules/netio/option.h
new file mode 100644
index 0000000..f5552c1
--- /dev/null
+++ b/src/modules/netio/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFNetIOOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf namePrefix;
+ uint32_t waitTime;
+ bool defaultRouteOnly;
+ bool detectTotal;
+} FFNetIOOptions;
+
+static_assert(sizeof(FFNetIOOptions) <= FF_OPTION_MAX_SIZE, "FFNetIOOptions size exceeds maximum allowed size");
diff --git a/src/modules/opencl/opencl.c b/src/modules/opencl/opencl.c
new file mode 100644
index 0000000..d69b3fe
--- /dev/null
+++ b/src/modules/opencl/opencl.c
@@ -0,0 +1,136 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/opencl/opencl.h"
+#include "detection/gpu/gpu.h"
+#include "modules/opencl/opencl.h"
+
+bool ffPrintOpenCL(FFOpenCLOptions* options) {
+ FFOpenCLResult* result = ffDetectOpenCL();
+
+ if (result->error != NULL) {
+ ffPrintError(FF_OPENCL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", result->error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_OPENCL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&result->version, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_OPENCL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->version, "version"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(result->vendor, "vendor"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseOpenCLJsonObject(FFOpenCLOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_OPENCL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateOpenCLJsonConfig(FFOpenCLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateOpenCLJsonResult(FF_A_UNUSED FFOpenCLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFOpenCLResult* result = ffDetectOpenCL();
+
+ if (result->error != NULL) {
+ yyjson_mut_obj_add_str(doc, module, "error", result->error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result->version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &result->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &result->vendor);
+
+ yyjson_mut_val* gpus = yyjson_mut_obj_add_arr(doc, obj, "gpus");
+ FF_LIST_FOR_EACH (FFGPUResult, gpu, result->gpus) {
+ yyjson_mut_val* gpuObj = yyjson_mut_arr_add_obj(doc, gpus);
+ yyjson_mut_obj_add_str(doc, gpuObj, "type", gpu->type == FF_GPU_TYPE_UNKNOWN ? "Unknown" : gpu->type == FF_GPU_TYPE_INTEGRATED ? "Integrated"
+ : "Discrete");
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "vendor", &gpu->vendor);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "name", &gpu->name);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "driver", &gpu->driver);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "platformApi", &gpu->platformApi);
+ if (gpu->coreCount != FF_GPU_CORE_COUNT_UNSET) {
+ yyjson_mut_obj_add_int(doc, gpuObj, "coreCount", gpu->coreCount);
+ } else {
+ yyjson_mut_obj_add_null(doc, gpuObj, "coreCount");
+ }
+
+ yyjson_mut_obj_add_uint(doc, gpuObj, "frequency", gpu->frequency);
+
+ yyjson_mut_val* memoryObj = yyjson_mut_obj_add_obj(doc, gpuObj, "memory");
+
+ {
+ yyjson_mut_val* dedicatedMemory = yyjson_mut_obj_add_obj(doc, memoryObj, "dedicated");
+ if (gpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedMemory, "total", gpu->dedicated.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedMemory, "total");
+ }
+
+ if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", gpu->dedicated.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedMemory, "used");
+ }
+ }
+
+ {
+ yyjson_mut_val* sharedMemory = yyjson_mut_obj_add_obj(doc, memoryObj, "shared");
+ if (gpu->shared.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedMemory, "total", gpu->shared.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedMemory, "total");
+ }
+
+ if (gpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedMemory, "used", gpu->shared.used);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedMemory, "used");
+ }
+ }
+
+ yyjson_mut_obj_add_uint(doc, gpuObj, "deviceId", gpu->deviceId);
+ }
+
+ return true;
+}
+
+void ffInitOpenCLOptions(FFOpenCLOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyOpenCLOptions(FFOpenCLOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffOpenCLModuleInfo = {
+ .name = FF_OPENCL_MODULE_NAME,
+ .description = "Print the highest OpenCL version supported by the GPU",
+ .initOptions = (void*) ffInitOpenCLOptions,
+ .destroyOptions = (void*) ffDestroyOpenCLOptions,
+ .parseJsonObject = (void*) ffParseOpenCLJsonObject,
+ .printModule = (void*) ffPrintOpenCL,
+ .generateJsonResult = (void*) ffGenerateOpenCLJsonResult,
+ .generateJsonConfig = (void*) ffGenerateOpenCLJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Platform version", "version" },
+ { "Platform name", "name" },
+ { "Platform vendor", "vendor" },
+ }))
+};
diff --git a/src/modules/opencl/opencl.h b/src/modules/opencl/opencl.h
new file mode 100644
index 0000000..aafab36
--- /dev/null
+++ b/src/modules/opencl/opencl.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_OPENCL_MODULE_NAME "OpenCL"
+
+bool ffPrintOpenCL(FFOpenCLOptions* options);
+void ffInitOpenCLOptions(FFOpenCLOptions* options);
+void ffDestroyOpenCLOptions(FFOpenCLOptions* options);
+
+extern FFModuleBaseInfo ffOpenCLModuleInfo;
diff --git a/src/modules/opencl/option.h b/src/modules/opencl/option.h
new file mode 100644
index 0000000..aa624c6
--- /dev/null
+++ b/src/modules/opencl/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFOpenCLOptions {
+ FFModuleArgs moduleArgs;
+} FFOpenCLOptions;
+
+static_assert(sizeof(FFOpenCLOptions) <= FF_OPTION_MAX_SIZE, "FFOpenCLOptions size exceeds maximum allowed size");
diff --git a/src/modules/opengl/opengl.c b/src/modules/opengl/opengl.c
new file mode 100644
index 0000000..0754cb2
--- /dev/null
+++ b/src/modules/opengl/opengl.c
@@ -0,0 +1,145 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/opengl/opengl.h"
+#include "modules/opengl/opengl.h"
+
+bool ffPrintOpenGL(FFOpenGLOptions* options) {
+ bool success = false;
+ FFOpenGLResult result;
+ ffStrbufInit(&result.version);
+ ffStrbufInit(&result.renderer);
+ ffStrbufInit(&result.vendor);
+ ffStrbufInit(&result.slv);
+ ffStrbufInit(&result.library);
+
+ const char* error = ffDetectOpenGL(options, &result);
+ if (error) {
+ ffPrintError(FF_OPENGL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ } else {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_OPENGL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ puts(result.version.chars);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_OPENGL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.version, "version"),
+ FF_ARG(result.renderer, "renderer"),
+ FF_ARG(result.vendor, "vendor"),
+ FF_ARG(result.slv, "slv"),
+ FF_ARG(result.library, "library"),
+ }));
+ }
+ success = true;
+ }
+
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.renderer);
+ ffStrbufDestroy(&result.vendor);
+ ffStrbufDestroy(&result.slv);
+ ffStrbufDestroy(&result.library);
+
+ return success;
+}
+
+void ffParseOpenGLJsonObject(FFOpenGLOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "library")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "auto", FF_OPENGL_LIBRARY_AUTO },
+ { "egl", FF_OPENGL_LIBRARY_EGL },
+ { "glx", FF_OPENGL_LIBRARY_GLX },
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_OPENGL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->library = (FFOpenGLLibrary) value;
+ }
+ continue;
+ }
+
+ ffPrintError(FF_OPENGL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateOpenGLJsonConfig(FFOpenGLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ switch (options->library) {
+ case FF_OPENGL_LIBRARY_AUTO:
+ yyjson_mut_obj_add_str(doc, module, "library", "auto");
+ break;
+ case FF_OPENGL_LIBRARY_EGL:
+ yyjson_mut_obj_add_str(doc, module, "library", "egl");
+ break;
+ case FF_OPENGL_LIBRARY_GLX:
+ yyjson_mut_obj_add_str(doc, module, "library", "glx");
+ break;
+ }
+}
+
+bool ffGenerateOpenGLJsonResult(FF_A_UNUSED FFOpenGLOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFOpenGLResult result;
+ ffStrbufInit(&result.version);
+ ffStrbufInit(&result.renderer);
+ ffStrbufInit(&result.vendor);
+ ffStrbufInit(&result.slv);
+ ffStrbufInit(&result.library);
+
+ const char* error = ffDetectOpenGL(options, &result);
+ if (error != NULL) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "renderer", &result.renderer);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &result.vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "slv", &result.slv);
+ yyjson_mut_obj_add_strbuf(doc, obj, "library", &result.library);
+ success = true;
+ }
+
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.renderer);
+ ffStrbufDestroy(&result.vendor);
+ ffStrbufDestroy(&result.slv);
+ ffStrbufDestroy(&result.library);
+
+ return success;
+}
+
+void ffInitOpenGLOptions(FFOpenGLOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->library = FF_OPENGL_LIBRARY_AUTO;
+}
+
+void ffDestroyOpenGLOptions(FFOpenGLOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffOpenGLModuleInfo = {
+ .name = FF_OPENGL_MODULE_NAME,
+ .description = "Print the highest OpenGL version supported by the GPU",
+ .initOptions = (void*) ffInitOpenGLOptions,
+ .destroyOptions = (void*) ffDestroyOpenGLOptions,
+ .parseJsonObject = (void*) ffParseOpenGLJsonObject,
+ .printModule = (void*) ffPrintOpenGL,
+ .generateJsonResult = (void*) ffGenerateOpenGLJsonResult,
+ .generateJsonConfig = (void*) ffGenerateOpenGLJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "OpenGL version", "version" },
+ { "OpenGL renderer", "renderer" },
+ { "OpenGL vendor", "vendor" },
+ { "OpenGL shading language version", "slv" },
+ { "OpenGL library used", "library" },
+ }))
+};
diff --git a/src/modules/opengl/opengl.h b/src/modules/opengl/opengl.h
new file mode 100644
index 0000000..8caffa5
--- /dev/null
+++ b/src/modules/opengl/opengl.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_OPENGL_MODULE_NAME "OpenGL"
+
+bool ffPrintOpenGL(FFOpenGLOptions* options);
+void ffInitOpenGLOptions(FFOpenGLOptions* options);
+void ffDestroyOpenGLOptions(FFOpenGLOptions* options);
+
+extern FFModuleBaseInfo ffOpenGLModuleInfo;
diff --git a/src/modules/opengl/option.h b/src/modules/opengl/option.h
new file mode 100644
index 0000000..869c6ec
--- /dev/null
+++ b/src/modules/opengl/option.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFOpenGLLibrary {
+ FF_OPENGL_LIBRARY_AUTO,
+ FF_OPENGL_LIBRARY_EGL,
+ FF_OPENGL_LIBRARY_GLX,
+} FFOpenGLLibrary;
+
+typedef struct FFOpenGLOptions {
+ FFModuleArgs moduleArgs;
+
+ FFOpenGLLibrary library;
+} FFOpenGLOptions;
+
+static_assert(sizeof(FFOpenGLOptions) <= FF_OPTION_MAX_SIZE, "FFOpenGLOptions size exceeds maximum allowed size");
diff --git a/src/modules/options.h b/src/modules/options.h
new file mode 100644
index 0000000..7cf98c1
--- /dev/null
+++ b/src/modules/options.h
@@ -0,0 +1,77 @@
+#pragma once
+
+// For "fastfetch.h"
+
+#include "modules/battery/option.h"
+#include "modules/bios/option.h"
+#include "modules/bluetooth/option.h"
+#include "modules/bluetoothradio/option.h"
+#include "modules/board/option.h"
+#include "modules/bootmgr/option.h"
+#include "modules/break/option.h"
+#include "modules/brightness/option.h"
+#include "modules/btrfs/option.h"
+#include "modules/camera/option.h"
+#include "modules/chassis/option.h"
+#include "modules/cpu/option.h"
+#include "modules/cpucache/option.h"
+#include "modules/cpuusage/option.h"
+#include "modules/colors/option.h"
+#include "modules/cursor/option.h"
+#include "modules/custom/option.h"
+#include "modules/command/option.h"
+#include "modules/datetime/option.h"
+#include "modules/de/option.h"
+#include "modules/disk/option.h"
+#include "modules/diskio/option.h"
+#include "modules/display/option.h"
+#include "modules/dns/option.h"
+#include "modules/editor/option.h"
+#include "modules/font/option.h"
+#include "modules/host/option.h"
+#include "modules/gamepad/option.h"
+#include "modules/gpu/option.h"
+#include "modules/icons/option.h"
+#include "modules/initsystem/option.h"
+#include "modules/kernel/option.h"
+#include "modules/keyboard/option.h"
+#include "modules/loadavg/option.h"
+#include "modules/locale/option.h"
+#include "modules/lm/option.h"
+#include "modules/localip/option.h"
+#include "modules/media/option.h"
+#include "modules/memory/option.h"
+#include "modules/monitor/option.h"
+#include "modules/mouse/option.h"
+#include "modules/netio/option.h"
+#include "modules/opengl/option.h"
+#include "modules/opencl/option.h"
+#include "modules/os/option.h"
+#include "modules/packages/option.h"
+#include "modules/physicaldisk/option.h"
+#include "modules/physicalmemory/option.h"
+#include "modules/player/option.h"
+#include "modules/poweradapter/option.h"
+#include "modules/processes/option.h"
+#include "modules/publicip/option.h"
+#include "modules/separator/option.h"
+#include "modules/shell/option.h"
+#include "modules/sound/option.h"
+#include "modules/swap/option.h"
+#include "modules/terminal/option.h"
+#include "modules/terminalfont/option.h"
+#include "modules/terminalsize/option.h"
+#include "modules/terminaltheme/option.h"
+#include "modules/theme/option.h"
+#include "modules/title/option.h"
+#include "modules/tpm/option.h"
+#include "modules/uptime/option.h"
+#include "modules/users/option.h"
+#include "modules/version/option.h"
+#include "modules/vulkan/option.h"
+#include "modules/wallpaper/option.h"
+#include "modules/weather/option.h"
+#include "modules/wifi/option.h"
+#include "modules/wm/option.h"
+#include "modules/wmtheme/option.h"
+#include "modules/zpool/option.h"
diff --git a/src/modules/os/option.h b/src/modules/os/option.h
new file mode 100644
index 0000000..46c7a1b
--- /dev/null
+++ b/src/modules/os/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFOSOptions {
+ FFModuleArgs moduleArgs;
+} FFOSOptions;
+
+static_assert(sizeof(FFOSOptions) <= FF_OPTION_MAX_SIZE, "FFOSOptions size exceeds maximum allowed size");
diff --git a/src/modules/os/os.c b/src/modules/os/os.c
new file mode 100644
index 0000000..e402699
--- /dev/null
+++ b/src/modules/os/os.c
@@ -0,0 +1,197 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/option.h"
+#include "common/strutil.h"
+#include "detection/os/os.h"
+#include "modules/os/os.h"
+
+#include <ctype.h>
+
+static void buildOutputDefault(const FFOSResult* os, FFstrbuf* result) {
+ // Create the basic output
+ if (os->name.length > 0) {
+ ffStrbufAppend(result, &os->name);
+ } else if (os->prettyName.length > 0) {
+ ffStrbufAppend(result, &os->prettyName);
+ } else if (os->id.length > 0) {
+ ffStrbufAppend(result, &os->id);
+ } else {
+ ffStrbufAppend(result, &instance.state.platform.sysinfo.name);
+ }
+
+ // Append code name if it is missing
+ if (os->codename.length > 0 && !ffStrbufContainIgnCase(result, &os->codename)) {
+ ffStrbufAppendC(result, ' ');
+ ffStrbufAppend(result, &os->codename);
+ }
+
+ // Append version if it is missing
+ if (os->versionID.length > 0 && !ffStrbufContainIgnCase(result, &os->versionID)) {
+ ffStrbufAppendC(result, ' ');
+ ffStrbufAppend(result, &os->versionID);
+ } else if (os->versionID.length == 0 && os->version.length > 0 && !ffStrbufContainIgnCase(result, &os->version)) {
+ ffStrbufAppendC(result, ' ');
+ ffStrbufAppend(result, &os->version);
+ }
+
+ // Append variant if it is missing
+ if (os->variant.length > 0 && !ffStrbufContainIgnCase(result, &os->variant)) {
+ ffStrbufAppendS(result, " (");
+ ffStrbufAppend(result, &os->variant);
+ ffStrbufAppendC(result, ')');
+ } else if (os->variant.length == 0 && os->variantID.length > 0 && !ffStrbufContainIgnCase(result, &os->variantID)) {
+ ffStrbufAppendS(result, " (");
+ ffStrbufAppend(result, &os->variantID);
+ ffStrbufAppendC(result, ')');
+ }
+}
+
+bool ffPrintOS(FFOSOptions* options) {
+ const FFOSResult* os = ffDetectOS();
+
+ if (os->name.length == 0 && os->prettyName.length == 0 && os->id.length == 0) {
+ ffPrintError(FF_OS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Could not detect OS");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufSetStatic(&key, FF_OS_MODULE_NAME);
+ } else {
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(instance.state.platform.sysinfo.name, "sysname"),
+ FF_ARG(os->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+
+ if (os->prettyName.length > 0) {
+ ffStrbufAppend(&result, &os->prettyName);
+ } else {
+ buildOutputDefault(os, &result);
+ }
+
+ // Append architecture if it is missing
+ if (!ffStrbufContainIgnCase(&result, &instance.state.platform.sysinfo.architecture)) {
+ ffStrbufAppendC(&result, ' ');
+ ffStrbufAppend(&result, &instance.state.platform.sysinfo.architecture);
+ }
+
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ ffStrbufPutTo(&result, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(instance.state.platform.sysinfo.name, "sysname"),
+ FF_ARG(os->name, "name"),
+ FF_ARG(os->prettyName, "pretty-name"),
+ FF_ARG(os->id, "id"),
+ FF_ARG(os->idLike, "id-like"),
+ FF_ARG(os->variant, "variant"),
+ FF_ARG(os->variantID, "variant-id"),
+ FF_ARG(os->version, "version"),
+ FF_ARG(os->versionID, "version-id"),
+ FF_ARG(os->codename, "codename"),
+ FF_ARG(os->buildID, "build-id"),
+ FF_ARG(instance.state.platform.sysinfo.architecture, "arch"),
+ FF_ARG(instance.state.platform.sysinfo.release, "kernel-release"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseOSJsonObject(FFOSOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_OS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateOSJsonConfig(FFOSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateOSJsonResult(FF_A_UNUSED FFOSOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFOSResult* os = ffDetectOS();
+
+ if (os->name.length == 0 && os->prettyName.length == 0 && os->id.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Could not detect OS");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "buildID", &os->buildID);
+ yyjson_mut_obj_add_strbuf(doc, obj, "codename", &os->codename);
+ yyjson_mut_obj_add_strbuf(doc, obj, "id", &os->id);
+ yyjson_mut_obj_add_strbuf(doc, obj, "idLike", &os->idLike);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &os->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "prettyName", &os->prettyName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "variant", &os->variant);
+ yyjson_mut_obj_add_strbuf(doc, obj, "variantID", &os->variantID);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &os->version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "versionID", &os->versionID);
+
+ return true;
+}
+
+void ffInitOSOptions(FFOSOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs,
+#ifdef _WIN32
+ ""
+#elif __APPLE__
+ ""
+#elif __FreeBSD__
+ "󰣠"
+#elif __ANDROID__
+ ""
+#elif __linux__
+ ""
+#elif __sun
+ ""
+#elif __OpenBSD__
+ ""
+#elif __Haiku__
+ ""
+#else
+ "󰢻"
+#endif
+ );
+}
+
+void ffDestroyOSOptions(FFOSOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffOSModuleInfo = {
+ .name = FF_OS_MODULE_NAME,
+ .description = "Print the OS or Linux distribution name and version",
+ .initOptions = (void*) ffInitOSOptions,
+ .destroyOptions = (void*) ffDestroyOSOptions,
+ .parseJsonObject = (void*) ffParseOSJsonObject,
+ .printModule = (void*) ffPrintOS,
+ .generateJsonResult = (void*) ffGenerateOSJsonResult,
+ .generateJsonConfig = (void*) ffGenerateOSJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Name of the kernel", "sysname" },
+ { "Name of the OS", "name" },
+ { "Pretty name of the OS, if available", "pretty-name" },
+ { "ID of the OS", "id" },
+ { "ID like of the OS", "id-like" },
+ { "Variant of the OS", "variant" },
+ { "Variant ID of the OS", "variant-id" },
+ { "Version of the OS", "version" },
+ { "Version ID of the OS", "version-id" },
+ { "Version codename of the OS", "codename" },
+ { "Build ID of the OS", "build-id" },
+ { "Architecture of the OS", "arch" },
+ }))
+};
diff --git a/src/modules/os/os.h b/src/modules/os/os.h
new file mode 100644
index 0000000..a53d285
--- /dev/null
+++ b/src/modules/os/os.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_OS_MODULE_NAME "OS"
+
+bool ffPrintOS(FFOSOptions* options);
+void ffInitOSOptions(FFOSOptions* options);
+void ffDestroyOSOptions(FFOSOptions* options);
+
+extern FFModuleBaseInfo ffOSModuleInfo;
diff --git a/src/modules/packages/option.h b/src/modules/packages/option.h
new file mode 100644
index 0000000..1e89533
--- /dev/null
+++ b/src/modules/packages/option.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFPackagesFlags {
+ FF_PACKAGES_FLAG_NONE = 0,
+ FF_PACKAGES_FLAG_APK_BIT = UINT64_C(1) << 0U,
+ FF_PACKAGES_FLAG_BREW_BIT = UINT64_C(1) << 1U,
+ FF_PACKAGES_FLAG_CHOCO_BIT = UINT64_C(1) << 2U,
+ FF_PACKAGES_FLAG_DPKG_BIT = UINT64_C(1) << 3U,
+ FF_PACKAGES_FLAG_EMERGE_BIT = UINT64_C(1) << 4U,
+ FF_PACKAGES_FLAG_EOPKG_BIT = UINT64_C(1) << 5U,
+ FF_PACKAGES_FLAG_FLATPAK_BIT = UINT64_C(1) << 6U,
+ FF_PACKAGES_FLAG_NIX_BIT = UINT64_C(1) << 7U,
+ FF_PACKAGES_FLAG_OPKG_BIT = UINT64_C(1) << 8U,
+ FF_PACKAGES_FLAG_PACMAN_BIT = UINT64_C(1) << 9U,
+ FF_PACKAGES_FLAG_PALUDIS_BIT = UINT64_C(1) << 10U,
+ FF_PACKAGES_FLAG_PKG_BIT = UINT64_C(1) << 11U,
+ FF_PACKAGES_FLAG_PKGTOOL_BIT = UINT64_C(1) << 12U,
+ FF_PACKAGES_FLAG_MACPORTS_BIT = UINT64_C(1) << 13U,
+ FF_PACKAGES_FLAG_RPM_BIT = UINT64_C(1) << 14U,
+ FF_PACKAGES_FLAG_SCOOP_BIT = UINT64_C(1) << 15U,
+ FF_PACKAGES_FLAG_SNAP_BIT = UINT64_C(1) << 16U,
+ FF_PACKAGES_FLAG_WINGET_BIT = UINT64_C(1) << 17U,
+ FF_PACKAGES_FLAG_XBPS_BIT = UINT64_C(1) << 18U,
+ FF_PACKAGES_FLAG_AM_BIT = UINT64_C(1) << 19U,
+ FF_PACKAGES_FLAG_SORCERY_BIT = UINT64_C(1) << 20U,
+ FF_PACKAGES_FLAG_LPKG_BIT = UINT64_C(1) << 21U,
+ FF_PACKAGES_FLAG_LPKGBUILD_BIT = UINT64_C(1) << 22U,
+ FF_PACKAGES_FLAG_GUIX_BIT = UINT64_C(1) << 23U,
+ FF_PACKAGES_FLAG_LINGLONG_BIT = UINT64_C(1) << 24U,
+ FF_PACKAGES_FLAG_PACSTALL_BIT = UINT64_C(1) << 25U,
+ FF_PACKAGES_FLAG_MPORT_BIT = UINT64_C(1) << 26U,
+ FF_PACKAGES_FLAG_PKGSRC_BIT = UINT64_C(1) << 27U,
+ FF_PACKAGES_FLAG_HPKG_BIT = UINT64_C(1) << 28U,
+ FF_PACKAGES_FLAG_PISI_BIT = UINT64_C(1) << 29U,
+ FF_PACKAGES_FLAG_SOAR_BIT = UINT64_C(1) << 30U,
+ FF_PACKAGES_FLAG_KISS_BIT = UINT64_C(1) << 31U,
+ FF_PACKAGES_FLAG_MOSS_BIT = UINT64_C(1) << 32U,
+ FF_PACKAGES_FLAG_APPIMAGE_BIT = UINT64_C(1) << 33U,
+ FF_PACKAGES_FLAG_CARDS_BIT = UINT64_C(1) << 34U,
+ FF_PACKAGES_FLAG_FORCE_UNSIGNED = UINT64_MAX,
+} FFPackagesFlags;
+static_assert(sizeof(FFPackagesFlags) == sizeof(uint64_t), "");
+
+typedef struct FFPackagesOptions {
+ FFModuleArgs moduleArgs;
+
+#if !FF_PACKAGES_REMOVE_DISABLED
+ FFPackagesFlags disabled;
+#endif
+ bool combined;
+} FFPackagesOptions;
+
+static_assert(sizeof(FFPackagesOptions) <= FF_OPTION_MAX_SIZE, "FFPackagesOptions size exceeds maximum allowed size");
diff --git a/src/modules/packages/packages.c b/src/modules/packages/packages.c
new file mode 100644
index 0000000..4ef1d04
--- /dev/null
+++ b/src/modules/packages/packages.c
@@ -0,0 +1,560 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/packages/packages.h"
+#include "modules/packages/packages.h"
+
+bool ffPrintPackages(FFPackagesOptions* options) {
+ FFPackagesResult counts = {};
+ ffStrbufInit(&counts.pacmanBranch);
+
+ const char* error = ffDetectPackages(&counts, options);
+
+ if (error) {
+ ffPrintError(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (counts.all == 0) {
+ ffPrintError(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No packages from known package managers found");
+ return false;
+ }
+
+ uint32_t nixAll = counts.nixDefault + counts.nixSystem + counts.nixUser;
+ uint32_t flatpakAll = counts.flatpakSystem + counts.flatpakUser;
+ uint32_t brewAll = counts.brew + counts.brewCask;
+ uint32_t guixAll = counts.guixSystem + counts.guixUser + counts.guixHome;
+ uint32_t hpkgAll = counts.hpkgSystem + counts.hpkgUser;
+ uint32_t amAll = counts.amSystem + counts.amUser;
+ uint32_t scoopAll = counts.scoopUser + counts.scoopGlobal;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate();
+
+#define FF_PRINT_PACKAGE_NAME(var, name) \
+ if (counts.var > 0) { \
+ ffStrbufAppendUInt(&output, counts.var); \
+ ffStrbufAppendS(&output, " (" name "), "); \
+ }
+
+#define FF_PRINT_PACKAGE(name) FF_PRINT_PACKAGE_NAME(name, #name)
+
+#define FF_PRINT_PACKAGE_ALL(name) \
+ if (name##All > 0) { \
+ ffStrbufAppendUInt(&output, name##All); \
+ ffStrbufAppendS(&output, " (" #name "), "); \
+ }
+
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(am);
+ } else {
+ FF_PRINT_PACKAGE_NAME(amSystem, "am")
+ FF_PRINT_PACKAGE_NAME(amUser, "appman")
+ }
+ FF_PRINT_PACKAGE(appimage)
+ FF_PRINT_PACKAGE(apk)
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(brew);
+ } else {
+ FF_PRINT_PACKAGE_NAME(brew, "brew")
+ FF_PRINT_PACKAGE_NAME(brewCask, "brew-cask")
+ }
+ FF_PRINT_PACKAGE(cards)
+ FF_PRINT_PACKAGE(choco)
+ FF_PRINT_PACKAGE(dpkg)
+ FF_PRINT_PACKAGE(emerge)
+ FF_PRINT_PACKAGE(eopkg)
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(flatpak);
+ } else if (counts.flatpakUser > 0) {
+ FF_PRINT_PACKAGE_NAME(flatpakSystem, "flatpak-system")
+ FF_PRINT_PACKAGE_NAME(flatpakUser, "flatpak-user")
+ } else {
+ FF_PRINT_PACKAGE_NAME(flatpakSystem, "flatpak")
+ }
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(guix);
+ } else {
+ FF_PRINT_PACKAGE_NAME(guixSystem, "guix-system")
+ FF_PRINT_PACKAGE_NAME(guixUser, "guix-user")
+ FF_PRINT_PACKAGE_NAME(guixHome, "guix-home")
+ }
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(hpkg)
+ } else if (counts.hpkgUser > 0) {
+ FF_PRINT_PACKAGE_NAME(hpkgSystem, "hpkg-system")
+ FF_PRINT_PACKAGE_NAME(hpkgUser, "hpkg-user")
+ } else {
+ FF_PRINT_PACKAGE_NAME(hpkgSystem, "hpkg")
+ }
+ FF_PRINT_PACKAGE(kiss)
+ FF_PRINT_PACKAGE(linglong)
+ FF_PRINT_PACKAGE(lpkg)
+ FF_PRINT_PACKAGE(lpkgbuild)
+ FF_PRINT_PACKAGE(macports)
+ FF_PRINT_PACKAGE(mport)
+ FF_PRINT_PACKAGE(moss)
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(nix);
+ } else {
+ FF_PRINT_PACKAGE_NAME(nixSystem, "nix-system")
+ FF_PRINT_PACKAGE_NAME(nixUser, "nix-user")
+ FF_PRINT_PACKAGE_NAME(nixDefault, "nix-default")
+ }
+ FF_PRINT_PACKAGE(opkg)
+ if (counts.pacman > 0) {
+ ffStrbufAppendF(&output, "%u (pacman)", counts.pacman);
+ if (counts.pacmanBranch.length > 0) {
+ ffStrbufAppendC(&output, '[');
+ ffStrbufAppend(&output, &counts.pacmanBranch);
+ ffStrbufAppendC(&output, ']');
+ }
+ ffStrbufAppendS(&output, ", ");
+ };
+ FF_PRINT_PACKAGE(pacstall)
+ FF_PRINT_PACKAGE(paludis)
+ FF_PRINT_PACKAGE(pisi)
+ FF_PRINT_PACKAGE(pkg)
+ FF_PRINT_PACKAGE(pkgsrc)
+ FF_PRINT_PACKAGE(pkgtool)
+ FF_PRINT_PACKAGE(rpm)
+ if (options->combined) {
+ FF_PRINT_PACKAGE_ALL(scoop);
+ } else if (counts.scoopGlobal > 0) {
+ FF_PRINT_PACKAGE_NAME(scoopUser, "scoop-user")
+ FF_PRINT_PACKAGE_NAME(scoopGlobal, "scoop-global")
+ } else {
+ FF_PRINT_PACKAGE_NAME(scoopUser, "scoop")
+ }
+ FF_PRINT_PACKAGE(snap)
+ FF_PRINT_PACKAGE(soar)
+ FF_PRINT_PACKAGE(sorcery)
+ FF_PRINT_PACKAGE(winget)
+ FF_PRINT_PACKAGE(xbps)
+
+ assert(output.length >= 2); // counts.all > 0 guarantees that at least one package count was printed, which guarantees that ", " was appended at least once
+ ffStrbufSubstrBefore(&output, output.length - 1);
+ output.chars[output.length - 1] = '\n';
+ ffStrbufWriteTo(&output, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_PACKAGES_MODULE_NAME,
+ 0,
+ &options->moduleArgs,
+ FF_PRINT_TYPE_DEFAULT,
+ ((FFformatarg[]) {
+ FF_ARG(counts.all, "all"),
+ FF_ARG(counts.amSystem, "am-system"),
+ FF_ARG(counts.amUser, "am-user"),
+ FF_ARG(counts.appimage, "appimage"),
+ FF_ARG(counts.apk, "apk"),
+ FF_ARG(counts.brew, "brew"),
+ FF_ARG(brewAll, "brew-all"),
+ FF_ARG(counts.brewCask, "brew-cask"),
+ FF_ARG(counts.cards, "cards"),
+ FF_ARG(counts.choco, "choco"),
+ FF_ARG(counts.dpkg, "dpkg"),
+ FF_ARG(counts.emerge, "emerge"),
+ FF_ARG(counts.eopkg, "eopkg"),
+ FF_ARG(flatpakAll, "flatpak-all"),
+ FF_ARG(counts.flatpakSystem, "flatpak-system"),
+ FF_ARG(counts.flatpakUser, "flatpak-user"),
+ FF_ARG(guixAll, "guix-all"),
+ FF_ARG(counts.guixHome, "guix-home"),
+ FF_ARG(counts.guixSystem, "guix-system"),
+ FF_ARG(counts.guixUser, "guix-user"),
+ FF_ARG(hpkgAll, "hpkg-all"),
+ FF_ARG(counts.hpkgSystem, "hpkg-system"),
+ FF_ARG(counts.hpkgUser, "hpkg-user"),
+ FF_ARG(counts.kiss, "kiss"),
+ FF_ARG(counts.linglong, "linglong"),
+ FF_ARG(counts.lpkg, "lpkg"),
+ FF_ARG(counts.lpkgbuild, "lpkgbuild"),
+ FF_ARG(counts.macports, "macports"),
+ FF_ARG(counts.mport, "mport"),
+ FF_ARG(counts.moss, "moss"),
+ FF_ARG(nixAll, "nix-all"),
+ FF_ARG(counts.nixDefault, "nix-default"),
+ FF_ARG(counts.nixSystem, "nix-system"),
+ FF_ARG(counts.nixUser, "nix-user"),
+ FF_ARG(counts.opkg, "opkg"),
+ FF_ARG(counts.pacman, "pacman"),
+ FF_ARG(counts.pacmanBranch, "pacman-branch"),
+ FF_ARG(counts.pacstall, "pacstall"),
+ FF_ARG(counts.paludis, "paludis"),
+ FF_ARG(counts.pisi, "pisi"),
+ FF_ARG(counts.pkg, "pkg"),
+ FF_ARG(counts.pkgsrc, "pkgsrc"),
+ FF_ARG(counts.pkgtool, "pkgtool"),
+ FF_ARG(counts.rpm, "rpm"),
+ FF_ARG(counts.scoopGlobal, "scoop-global"),
+ FF_ARG(counts.scoopUser, "scoop-user"),
+ FF_ARG(counts.snap, "snap"),
+ FF_ARG(counts.soar, "soar"),
+ FF_ARG(counts.sorcery, "sorcery"),
+ FF_ARG(counts.winget, "winget"),
+ FF_ARG(counts.xbps, "xbps"),
+ }));
+ }
+
+ ffStrbufDestroy(&counts.pacmanBranch);
+
+ return true;
+}
+
+void ffParsePackagesJsonObject(FFPackagesOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+#if !FF_PACKAGES_REMOVE_DISABLED
+ if (unsafe_yyjson_equals_str(key, "disabled")) {
+ if (!yyjson_is_null(val) && !yyjson_is_arr(val)) {
+ ffPrintError(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid JSON value for %s", unsafe_yyjson_get_str(key));
+ continue;
+ }
+
+ options->disabled = FF_PACKAGES_FLAG_NONE;
+
+ if (yyjson_is_arr(val)) {
+ yyjson_val* flagObj;
+ size_t flagIdx, flagMax;
+ yyjson_arr_foreach (val, flagIdx, flagMax, flagObj) {
+ if (!yyjson_is_str(flagObj)) {
+ ffPrintError(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid JSON value for %s", unsafe_yyjson_get_str(key));
+ continue;
+ }
+ const char* flag = unsafe_yyjson_get_str(flagObj);
+
+ #define FF_TEST_PACKAGE_NAME(name) \
+ else if (ffStrEqualsIgnCase(flag, #name)) { \
+ options->disabled |= FF_PACKAGES_FLAG_##name##_BIT; \
+ }
+ switch (toupper(flag[0])) {
+ case 'A':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(AM)
+ FF_TEST_PACKAGE_NAME(APK)
+ FF_TEST_PACKAGE_NAME(APPIMAGE)
+ break;
+ case 'B':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(BREW)
+ break;
+ case 'C':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(CARDS)
+ FF_TEST_PACKAGE_NAME(CHOCO)
+ break;
+ case 'D':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(DPKG)
+ break;
+ case 'E':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(EMERGE)
+ FF_TEST_PACKAGE_NAME(EOPKG)
+ break;
+ case 'F':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(FLATPAK)
+ break;
+ case 'G':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(GUIX)
+ break;
+ case 'H':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(HPKG)
+ break;
+ case 'K':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(KISS)
+ break;
+ case 'L':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(LINGLONG)
+ FF_TEST_PACKAGE_NAME(LPKG)
+ FF_TEST_PACKAGE_NAME(LPKGBUILD)
+ break;
+ case 'M':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(MACPORTS)
+ FF_TEST_PACKAGE_NAME(MOSS)
+ FF_TEST_PACKAGE_NAME(MPORT)
+ break;
+ case 'N':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(NIX)
+ break;
+ case 'O':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(OPKG)
+ break;
+ case 'P':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(PACMAN)
+ FF_TEST_PACKAGE_NAME(PACSTALL)
+ FF_TEST_PACKAGE_NAME(PALUDIS)
+ FF_TEST_PACKAGE_NAME(PISI)
+ FF_TEST_PACKAGE_NAME(PKG)
+ FF_TEST_PACKAGE_NAME(PKGSRC)
+ FF_TEST_PACKAGE_NAME(PKGTOOL)
+ break;
+ case 'R':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(RPM)
+ break;
+ case 'S':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(SCOOP)
+ FF_TEST_PACKAGE_NAME(SNAP)
+ FF_TEST_PACKAGE_NAME(SOAR)
+ FF_TEST_PACKAGE_NAME(SORCERY)
+ break;
+ case 'W':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(WINGET)
+ break;
+ case 'X':
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(XBPS)
+ break;
+ }
+ #undef FF_TEST_PACKAGE_NAME
+ }
+ continue;
+ }
+ }
+#endif
+
+ if (unsafe_yyjson_equals_str(key, "combined")) {
+ options->combined = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_PACKAGES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGeneratePackagesJsonConfig(FFPackagesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+#if !FF_PACKAGES_REMOVE_DISABLED
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "disabled");
+ #define FF_TEST_PACKAGE_NAME(name) \
+ else if ((options->disabled & FF_PACKAGES_FLAG_##name##_BIT)) { \
+ ffStrbufSetS(&buf, #name); \
+ ffStrbufLowerCase(&buf); \
+ yyjson_mut_arr_add_strbuf(doc, arr, &buf); \
+ }
+ if (false)
+ ;
+ FF_TEST_PACKAGE_NAME(AM)
+ FF_TEST_PACKAGE_NAME(APK)
+ FF_TEST_PACKAGE_NAME(APPIMAGE)
+ FF_TEST_PACKAGE_NAME(BREW)
+ FF_TEST_PACKAGE_NAME(CARDS)
+ FF_TEST_PACKAGE_NAME(CHOCO)
+ FF_TEST_PACKAGE_NAME(DPKG)
+ FF_TEST_PACKAGE_NAME(EMERGE)
+ FF_TEST_PACKAGE_NAME(EOPKG)
+ FF_TEST_PACKAGE_NAME(FLATPAK)
+ FF_TEST_PACKAGE_NAME(GUIX)
+ FF_TEST_PACKAGE_NAME(HPKG)
+ FF_TEST_PACKAGE_NAME(KISS)
+ FF_TEST_PACKAGE_NAME(LINGLONG)
+ FF_TEST_PACKAGE_NAME(LPKG)
+ FF_TEST_PACKAGE_NAME(LPKGBUILD)
+ FF_TEST_PACKAGE_NAME(MACPORTS)
+ FF_TEST_PACKAGE_NAME(MOSS)
+ FF_TEST_PACKAGE_NAME(MPORT)
+ FF_TEST_PACKAGE_NAME(NIX)
+ FF_TEST_PACKAGE_NAME(OPKG)
+ FF_TEST_PACKAGE_NAME(PACMAN)
+ FF_TEST_PACKAGE_NAME(PACSTALL)
+ FF_TEST_PACKAGE_NAME(PALUDIS)
+ FF_TEST_PACKAGE_NAME(PISI)
+ FF_TEST_PACKAGE_NAME(PKG)
+ FF_TEST_PACKAGE_NAME(PKGSRC)
+ FF_TEST_PACKAGE_NAME(PKGTOOL)
+ FF_TEST_PACKAGE_NAME(RPM)
+ FF_TEST_PACKAGE_NAME(SCOOP)
+ FF_TEST_PACKAGE_NAME(SNAP)
+ FF_TEST_PACKAGE_NAME(SOAR)
+ FF_TEST_PACKAGE_NAME(SORCERY)
+ FF_TEST_PACKAGE_NAME(WINGET)
+ FF_TEST_PACKAGE_NAME(XBPS)
+ #undef FF_TEST_PACKAGE_NAME
+#endif
+
+ yyjson_mut_obj_add_bool(doc, module, "combined", options->combined);
+}
+
+bool ffGeneratePackagesJsonResult(FFPackagesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFPackagesResult counts = {};
+ ffStrbufInit(&counts.pacmanBranch);
+
+ const char* error = ffDetectPackages(&counts, options);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ yyjson_mut_obj_add_uint(doc, obj, "all", counts.all);
+
+#define FF_APPEND_PACKAGE_COUNT(name) \
+ if (counts.name > 0) yyjson_mut_obj_add_uint(doc, obj, #name, counts.name);
+ FF_APPEND_PACKAGE_COUNT(amSystem)
+ FF_APPEND_PACKAGE_COUNT(amUser)
+ FF_APPEND_PACKAGE_COUNT(appimage)
+ FF_APPEND_PACKAGE_COUNT(apk)
+ FF_APPEND_PACKAGE_COUNT(brew)
+ FF_APPEND_PACKAGE_COUNT(brewCask)
+ FF_APPEND_PACKAGE_COUNT(cards)
+ FF_APPEND_PACKAGE_COUNT(choco)
+ FF_APPEND_PACKAGE_COUNT(dpkg)
+ FF_APPEND_PACKAGE_COUNT(emerge)
+ FF_APPEND_PACKAGE_COUNT(eopkg)
+ FF_APPEND_PACKAGE_COUNT(flatpakSystem)
+ FF_APPEND_PACKAGE_COUNT(flatpakUser)
+ FF_APPEND_PACKAGE_COUNT(guixHome)
+ FF_APPEND_PACKAGE_COUNT(guixSystem)
+ FF_APPEND_PACKAGE_COUNT(guixUser)
+ FF_APPEND_PACKAGE_COUNT(hpkgSystem)
+ FF_APPEND_PACKAGE_COUNT(hpkgUser)
+ FF_APPEND_PACKAGE_COUNT(kiss)
+ FF_APPEND_PACKAGE_COUNT(linglong)
+ FF_APPEND_PACKAGE_COUNT(lpkg)
+ FF_APPEND_PACKAGE_COUNT(lpkgbuild)
+ FF_APPEND_PACKAGE_COUNT(macports)
+ FF_APPEND_PACKAGE_COUNT(mport)
+ FF_APPEND_PACKAGE_COUNT(moss)
+ FF_APPEND_PACKAGE_COUNT(nixDefault)
+ FF_APPEND_PACKAGE_COUNT(nixSystem)
+ FF_APPEND_PACKAGE_COUNT(nixUser)
+ FF_APPEND_PACKAGE_COUNT(opkg)
+ FF_APPEND_PACKAGE_COUNT(pacman)
+ FF_APPEND_PACKAGE_COUNT(pacstall)
+ FF_APPEND_PACKAGE_COUNT(paludis)
+ FF_APPEND_PACKAGE_COUNT(pisi)
+ FF_APPEND_PACKAGE_COUNT(pkg)
+ FF_APPEND_PACKAGE_COUNT(pkgsrc)
+ FF_APPEND_PACKAGE_COUNT(pkgtool)
+ FF_APPEND_PACKAGE_COUNT(rpm)
+ FF_APPEND_PACKAGE_COUNT(scoopGlobal)
+ FF_APPEND_PACKAGE_COUNT(scoopUser)
+ FF_APPEND_PACKAGE_COUNT(snap)
+ FF_APPEND_PACKAGE_COUNT(soar)
+ FF_APPEND_PACKAGE_COUNT(sorcery)
+ FF_APPEND_PACKAGE_COUNT(winget)
+ FF_APPEND_PACKAGE_COUNT(xbps)
+ if (counts.pacmanBranch.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, obj, "pacmanBranch", &counts.pacmanBranch);
+ }
+
+ return true;
+}
+
+void ffInitPackagesOptions(FFPackagesOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰏖");
+
+ #if !FF_PACKAGES_REMOVE_DISABLED
+ options->disabled = FF_PACKAGES_DISABLE_LIST;
+ #endif
+ options->combined = false;
+}
+
+void ffDestroyPackagesOptions(FFPackagesOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffPackagesModuleInfo = {
+ .name = FF_PACKAGES_MODULE_NAME,
+ .description = "List installed package managers and count of installed packages",
+ .initOptions = (void*) ffInitPackagesOptions,
+ .destroyOptions = (void*) ffDestroyPackagesOptions,
+ .parseJsonObject = (void*) ffParsePackagesJsonObject,
+ .printModule = (void*) ffPrintPackages,
+ .generateJsonResult = (void*) ffGeneratePackagesJsonResult,
+ .generateJsonConfig = (void*) ffGeneratePackagesJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Number of all packages", "all" },
+ { "Number of am-system packages", "am-system" },
+ { "Number of am-user (aka appman) packages", "am-user" },
+ { "Number of appimage packages", "appimage" },
+ { "Number of apk packages", "apk" },
+ { "Number of brew packages", "brew" },
+ { "Total number of all brew packages", "brew-all" },
+ { "Number of brew-cask packages", "brew-cask" },
+ { "Number of cards packages", "cards" },
+ { "Number of choco packages", "choco" },
+ { "Number of dpkg packages", "dpkg" },
+ { "Number of emerge packages", "emerge" },
+ { "Number of eopkg packages", "eopkg" },
+ { "Total number of all flatpak app packages", "flatpak-all" },
+ { "Number of flatpak-system app packages", "flatpak-system" },
+ { "Number of flatpak-user app packages", "flatpak-user" },
+ { "Total number of all guix packages", "guix-all" },
+ { "Number of guix-home packages", "guix-home" },
+ { "Number of guix-system packages", "guix-system" },
+ { "Number of guix-user packages", "guix-user" },
+ { "Total number of all hpkg packages", "hpkg-all" },
+ { "Number of hpkg-system packages", "hpkg-system" },
+ { "Number of hpkg-user packages", "hpkg-user" },
+ { "Number of kiss packages", "kiss" },
+ { "Number of linglong packages", "linglong" },
+ { "Number of lpkg packages", "lpkg" },
+ { "Number of lpkgbuild packages", "lpkgbuild" },
+ { "Number of macports packages", "macports" },
+ { "Number of mport packages", "mport" },
+ { "Number of moss packages", "moss" },
+ { "Total number of all nix packages", "nix-all" },
+ { "Number of nix-default packages", "nix-default" },
+ { "Number of nix-system packages", "nix-system" },
+ { "Number of nix-user packages", "nix-user" },
+ { "Number of opkg packages", "opkg" },
+ { "Number of pacman packages", "pacman" },
+ { "Pacman branch on manjaro", "pacman-branch" },
+ { "Number of pacstall packages", "pacstall" },
+ { "Number of paludis packages", "paludis" },
+ { "Number of pisi packages", "pisi" },
+ { "Number of pkg packages", "pkg" },
+ { "Number of pkgsrc packages", "pkgsrc" },
+ { "Number of pkgtool packages", "pkgtool" },
+ { "Number of rpm packages", "rpm" },
+ { "Number of scoop-global packages", "scoop-global" },
+ { "Number of scoop-user packages", "scoop-user" },
+ { "Number of snap packages", "snap" },
+ { "Number of soar packages", "soar" },
+ { "Number of sorcery packages", "sorcery" },
+ { "Number of winget packages", "winget" },
+ { "Number of xbps packages", "xbps" },
+ }))
+};
diff --git a/src/modules/packages/packages.h b/src/modules/packages/packages.h
new file mode 100644
index 0000000..4e58768
--- /dev/null
+++ b/src/modules/packages/packages.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_PACKAGES_MODULE_NAME "Packages"
+
+bool ffPrintPackages(FFPackagesOptions* options);
+void ffInitPackagesOptions(FFPackagesOptions* options);
+void ffDestroyPackagesOptions(FFPackagesOptions* options);
+
+extern FFModuleBaseInfo ffPackagesModuleInfo;
diff --git a/src/modules/physicaldisk/option.h b/src/modules/physicaldisk/option.h
new file mode 100644
index 0000000..00ba013
--- /dev/null
+++ b/src/modules/physicaldisk/option.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFPhysicalDiskType {
+ FF_PHYSICALDISK_TYPE_NONE = 0,
+
+ // If none is set, it's unknown
+ FF_PHYSICALDISK_TYPE_HDD = 1 << 0,
+ FF_PHYSICALDISK_TYPE_SSD = 1 << 1,
+ FF_PHYSICALDISK_TYPE_VIRTUAL = 1 << 2,
+
+ FF_PHYSICALDISK_TYPE_FIXED = 1 << 3,
+ FF_PHYSICALDISK_TYPE_REMOVABLE = 1 << 4,
+
+ FF_PHYSICALDISK_TYPE_READWRITE = 1 << 5,
+ FF_PHYSICALDISK_TYPE_READONLY = 1 << 6,
+
+ FF_PHYSICALDISK_TYPE_UNUSED = 1 << 7,
+
+ FF_PHYSICALDISK_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFPhysicalDiskType;
+static_assert(sizeof(FFPhysicalDiskType) == sizeof(uint8_t), "");
+
+typedef struct FFPhysicalDiskOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf namePrefix;
+ FFPhysicalDiskType hideType;
+ bool temp;
+ FFColorRangeConfig tempConfig;
+} FFPhysicalDiskOptions;
+
+static_assert(sizeof(FFPhysicalDiskOptions) <= FF_OPTION_MAX_SIZE, "FFPhysicalDiskOptions size exceeds maximum allowed size");
diff --git a/src/modules/physicaldisk/physicaldisk.c b/src/modules/physicaldisk/physicaldisk.c
new file mode 100644
index 0000000..b0b7c58
--- /dev/null
+++ b/src/modules/physicaldisk/physicaldisk.c
@@ -0,0 +1,288 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/temps.h"
+#include "common/size.h"
+#include "detection/physicaldisk/physicaldisk.h"
+#include "modules/physicaldisk/physicaldisk.h"
+
+#define FF_PHYSICALDISK_DISPLAY_NAME "Physical Disk"
+
+static int sortDevices(const FFPhysicalDiskResult* left, const FFPhysicalDiskResult* right) {
+ return ffStrbufComp(&left->name, &right->name);
+}
+
+static void formatKey(const FFPhysicalDiskOptions* options, FFPhysicalDiskResult* dev, uint32_t index, FFstrbuf* key) {
+ if (options->moduleArgs.key.length == 0) {
+ ffStrbufSetF(key, FF_PHYSICALDISK_DISPLAY_NAME " (%s)", dev->name.length ? dev->name.chars : dev->devPath.chars);
+ } else {
+ ffStrbufClear(key);
+ FF_PARSE_FORMAT_STRING_CHECKED(key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(dev->name, "name"),
+ FF_ARG(dev->devPath, "dev-path"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+}
+
+bool ffPrintPhysicalDisk(FFPhysicalDiskOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectPhysicalDisk(&result, options);
+
+ if (error) {
+ ffPrintError(FF_PHYSICALDISK_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ ffListSort(&result, sizeof(FFPhysicalDiskResult), (const void*) sortDevices);
+
+ uint32_t index = 0;
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+
+ FF_LIST_FOR_EACH (FFPhysicalDiskResult, dev, result) {
+ formatKey(options, dev, result.length == 1 ? 0 : index + 1, &key);
+ ffStrbufClear(&buffer);
+ ffSizeAppendNum(dev->size, &buffer);
+
+ const char* physicalType = dev->type & FF_PHYSICALDISK_TYPE_VIRTUAL
+ ? "Virtual"
+ : dev->type & FF_PHYSICALDISK_TYPE_HDD
+ ? "HDD"
+ : dev->type & FF_PHYSICALDISK_TYPE_SSD
+ ? "SSD"
+ : "";
+ const char* removableType = dev->type & FF_PHYSICALDISK_TYPE_REMOVABLE
+ ? "Removable"
+ : dev->type & FF_PHYSICALDISK_TYPE_FIXED
+ ? "Fixed"
+ : "";
+ const char* readOnlyType = dev->type & FF_PHYSICALDISK_TYPE_READONLY
+ ? "Read-only"
+ : "";
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ if (physicalType[0] || removableType[0] || readOnlyType[0]) {
+ ffStrbufAppendS(&buffer, " [");
+ if (physicalType[0]) {
+ ffStrbufAppendS(&buffer, physicalType);
+ }
+ if (removableType[0]) {
+ if (buffer.chars[buffer.length - 1] != '[') {
+ ffStrbufAppendS(&buffer, ", ");
+ }
+ ffStrbufAppendS(&buffer, removableType);
+ }
+ if (readOnlyType[0]) {
+ if (buffer.chars[buffer.length - 1] != '[') {
+ ffStrbufAppendS(&buffer, ", ");
+ }
+ ffStrbufAppendS(&buffer, readOnlyType);
+ }
+ ffStrbufAppendC(&buffer, ']');
+ }
+
+ if (dev->temperature != FF_PHYSICALDISK_TEMP_UNSET) {
+ if (buffer.length > 0) {
+ ffStrbufAppendS(&buffer, " - ");
+ }
+
+ ffTempsAppendNum(dev->temperature, &buffer, options->tempConfig, &options->moduleArgs);
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY tempStr = ffStrbufCreate();
+ ffTempsAppendNum(dev->temperature, &tempStr, options->tempConfig, &options->moduleArgs);
+ if (dev->type & FF_PHYSICALDISK_TYPE_READWRITE) {
+ readOnlyType = "Read-write";
+ }
+ FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(buffer, "size"),
+ FF_ARG(dev->name, "name"),
+ FF_ARG(dev->interconnect, "interconnect"),
+ FF_ARG(dev->devPath, "dev-path"),
+ FF_ARG(dev->serial, "serial"),
+ FF_ARG(physicalType, "physical-type"),
+ FF_ARG(removableType, "removable-type"),
+ FF_ARG(readOnlyType, "readonly-type"),
+ FF_ARG(dev->revision, "revision"),
+ FF_ARG(tempStr, "temperature"),
+ }));
+ }
+ ++index;
+ }
+
+ FF_LIST_FOR_EACH (FFPhysicalDiskResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->interconnect);
+ ffStrbufDestroy(&dev->devPath);
+ ffStrbufDestroy(&dev->serial);
+ ffStrbufDestroy(&dev->revision);
+ }
+
+ return true;
+}
+
+void ffParsePhysicalDiskJsonObject(FFPhysicalDiskOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "namePrefix")) {
+ ffStrbufSetJsonVal(&options->namePrefix, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "hideVirtual")) {
+ if (!yyjson_is_bool(val)) {
+ ffPrintError(FF_PHYSICALDISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "hideVirtual must be a boolean");
+ } else {
+ if (unsafe_yyjson_is_true(val)) {
+ options->hideType |= FF_PHYSICALDISK_TYPE_VIRTUAL;
+ } else {
+ options->hideType &= ~FF_PHYSICALDISK_TYPE_VIRTUAL;
+ }
+ }
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "hideUnused")) {
+ if (!yyjson_is_bool(val)) {
+ ffPrintError(FF_PHYSICALDISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "hideUnused must be a boolean");
+ } else {
+ if (unsafe_yyjson_is_true(val)) {
+ options->hideType |= FF_PHYSICALDISK_TYPE_UNUSED;
+ } else {
+ options->hideType &= ~FF_PHYSICALDISK_TYPE_UNUSED;
+ }
+ }
+ continue;
+ }
+
+ if (ffTempsParseJsonObject(key, val, &options->temp, &options->tempConfig)) {
+ continue;
+ }
+
+ ffPrintError(FF_PHYSICALDISK_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGeneratePhysicalDiskJsonConfig(FFPhysicalDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "namePrefix", &options->namePrefix);
+
+ ffTempsGenerateJsonConfig(doc, module, options->temp, options->tempConfig);
+
+ yyjson_mut_obj_add_bool(doc, module, "hideVirtual", !!(options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL));
+ yyjson_mut_obj_add_bool(doc, module, "hideUnused", !!(options->hideType & FF_PHYSICALDISK_TYPE_UNUSED));
+}
+
+bool ffGeneratePhysicalDiskJsonResult(FFPhysicalDiskOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectPhysicalDisk(&result, options);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFPhysicalDiskResult, dev, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &dev->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "devPath", &dev->devPath);
+ yyjson_mut_obj_add_strbuf(doc, obj, "interconnect", &dev->interconnect);
+
+ if (dev->type & FF_PHYSICALDISK_TYPE_VIRTUAL) {
+ yyjson_mut_obj_add_str(doc, obj, "kind", "Virtual");
+ } else if (dev->type & FF_PHYSICALDISK_TYPE_HDD) {
+ yyjson_mut_obj_add_str(doc, obj, "kind", "HDD");
+ } else if (dev->type & FF_PHYSICALDISK_TYPE_SSD) {
+ yyjson_mut_obj_add_str(doc, obj, "kind", "SSD");
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "kind");
+ }
+
+ yyjson_mut_obj_add_uint(doc, obj, "size", dev->size);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &dev->serial);
+
+ if (dev->type & FF_PHYSICALDISK_TYPE_REMOVABLE) {
+ yyjson_mut_obj_add_bool(doc, obj, "removable", true);
+ } else if (dev->type & FF_PHYSICALDISK_TYPE_FIXED) {
+ yyjson_mut_obj_add_bool(doc, obj, "removable", false);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "removable");
+ }
+
+ if (dev->type & FF_PHYSICALDISK_TYPE_READONLY) {
+ yyjson_mut_obj_add_bool(doc, obj, "readOnly", true);
+ } else if (dev->type & FF_PHYSICALDISK_TYPE_READWRITE) {
+ yyjson_mut_obj_add_bool(doc, obj, "readOnly", false);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "readOnly");
+ }
+
+ yyjson_mut_obj_add_bool(doc, obj, "unknown", !!(dev->type & FF_PHYSICALDISK_TYPE_UNUSED));
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "revision", &dev->revision);
+
+ if (dev->temperature != FF_PHYSICALDISK_TEMP_UNSET) {
+ yyjson_mut_obj_add_real(doc, obj, "temperature", dev->temperature);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "temperature");
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFPhysicalDiskResult, dev, result) {
+ ffStrbufDestroy(&dev->name);
+ ffStrbufDestroy(&dev->interconnect);
+ ffStrbufDestroy(&dev->devPath);
+ ffStrbufDestroy(&dev->serial);
+ ffStrbufDestroy(&dev->revision);
+ }
+
+ return true;
+}
+
+void ffInitPhysicalDiskOptions(FFPhysicalDiskOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰋊");
+
+ ffStrbufInit(&options->namePrefix);
+ options->temp = false;
+ options->tempConfig = (FFColorRangeConfig) { 50, 70 };
+ options->hideType = FF_PHYSICALDISK_TYPE_UNUSED;
+}
+
+void ffDestroyPhysicalDiskOptions(FFPhysicalDiskOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->namePrefix);
+}
+
+FFModuleBaseInfo ffPhysicalDiskModuleInfo = {
+ .name = FF_PHYSICALDISK_MODULE_NAME,
+ .description = "Print physical disk information",
+ .initOptions = (void*) ffInitPhysicalDiskOptions,
+ .destroyOptions = (void*) ffDestroyPhysicalDiskOptions,
+ .parseJsonObject = (void*) ffParsePhysicalDiskJsonObject,
+ .printModule = (void*) ffPrintPhysicalDisk,
+ .generateJsonResult = (void*) ffGeneratePhysicalDiskJsonResult,
+ .generateJsonConfig = (void*) ffGeneratePhysicalDiskJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Device size (formatted)", "size" },
+ { "Device name", "name" },
+ { "Device interconnect type", "interconnect" },
+ { "Device raw file path", "dev-path" },
+ { "Serial number", "serial" },
+ { "Device kind (SSD or HDD)", "physical-type" },
+ { "Device kind (Removable or Fixed)", "removable-type" },
+ { "Device kind (Read-only or Read-write)", "readonly-type" },
+ { "Product revision", "revision" },
+ { "Device temperature (formatted)", "temperature" },
+ }))
+};
diff --git a/src/modules/physicaldisk/physicaldisk.h b/src/modules/physicaldisk/physicaldisk.h
new file mode 100644
index 0000000..aaad888
--- /dev/null
+++ b/src/modules/physicaldisk/physicaldisk.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_PHYSICALDISK_MODULE_NAME "PhysicalDisk"
+
+bool ffPrintPhysicalDisk(FFPhysicalDiskOptions* options);
+void ffInitPhysicalDiskOptions(FFPhysicalDiskOptions* options);
+void ffDestroyPhysicalDiskOptions(FFPhysicalDiskOptions* options);
+
+extern FFModuleBaseInfo ffPhysicalDiskModuleInfo;
diff --git a/src/modules/physicalmemory/option.h b/src/modules/physicalmemory/option.h
new file mode 100644
index 0000000..faaff95
--- /dev/null
+++ b/src/modules/physicalmemory/option.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFPhysicalMemoryOptions {
+ FFModuleArgs moduleArgs;
+ bool showEmptySlots;
+} FFPhysicalMemoryOptions;
+
+static_assert(sizeof(FFPhysicalMemoryOptions) <= FF_OPTION_MAX_SIZE, "FFPhysicalMemoryOptions size exceeds maximum allowed size");
diff --git a/src/modules/physicalmemory/physicalmemory.c b/src/modules/physicalmemory/physicalmemory.c
new file mode 100644
index 0000000..d4bee7d
--- /dev/null
+++ b/src/modules/physicalmemory/physicalmemory.c
@@ -0,0 +1,184 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/size.h"
+#include "detection/physicalmemory/physicalmemory.h"
+#include "modules/physicalmemory/physicalmemory.h"
+
+#define FF_PHYSICALMEMORY_DISPLAY_NAME "Physical Memory"
+
+bool ffPrintPhysicalMemory(FFPhysicalMemoryOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectPhysicalMemory(options, &result);
+
+ if (error) {
+ ffPrintError(FF_PHYSICALMEMORY_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_PHYSICALMEMORY_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No physical memory devices detected");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY prettySize = ffStrbufCreate();
+
+ uint32_t i = 0;
+ FF_LIST_FOR_EACH (FFPhysicalMemoryResult, device, result) {
+ ffStrbufClear(&prettySize);
+ if (device->installed) {
+ ffSizeAppendNum(device->size, &prettySize);
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_PHYSICALMEMORY_DISPLAY_NAME, result.length == 1 ? 0 : (uint8_t) (i + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ if (device->installed) {
+ fputs(prettySize.chars, stdout);
+ fputs(" - ", stdout);
+ ffStrbufWriteTo(&device->type, stdout);
+ if (device->maxSpeed > 0) {
+ printf("-%u", device->maxSpeed);
+ }
+ if (device->runningSpeed > 0 && device->runningSpeed != device->maxSpeed) {
+ printf(" @ %u MT/s", device->runningSpeed);
+ }
+ if (device->vendor.length > 0) {
+ printf(" (%s)", device->vendor.chars);
+ }
+ if (device->ecc) {
+ fputs(" - ECC", stdout);
+ }
+ } else {
+ fputs("Empty", stdout);
+ if (device->formFactor.length > 0) {
+ printf(" - %s", device->formFactor.chars);
+ }
+ if (device->locator.length > 0) {
+ printf(" (%s)", device->locator.chars);
+ }
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_PHYSICALMEMORY_DISPLAY_NAME, (uint8_t) (i + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(device->size, "bytes"),
+ FF_ARG(prettySize, "size"),
+ FF_ARG(device->maxSpeed, "max-speed"),
+ FF_ARG(device->runningSpeed, "running-speed"),
+ FF_ARG(device->type, "type"),
+ FF_ARG(device->formFactor, "form-factor"),
+ FF_ARG(device->locator, "locator"),
+ FF_ARG(device->vendor, "vendor"),
+ FF_ARG(device->serial, "serial"),
+ FF_ARG(device->partNumber, "part-number"),
+ FF_ARG(device->ecc, "is-ecc-enabled"),
+ FF_ARG(device->installed, "is-installed"),
+ }));
+ }
+
+ i++;
+ }
+
+ FF_LIST_FOR_EACH (FFPhysicalMemoryResult, device, result) {
+ ffStrbufDestroy(&device->type);
+ ffStrbufDestroy(&device->locator);
+ ffStrbufDestroy(&device->formFactor);
+ ffStrbufDestroy(&device->vendor);
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->partNumber);
+ }
+
+ return true;
+}
+
+void ffParsePhysicalMemoryJsonObject(FFPhysicalMemoryOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "showEmptySlots")) {
+ options->showEmptySlots = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_PHYSICALMEMORY_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGeneratePhysicalMemoryJsonConfig(FFPhysicalMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+ yyjson_mut_obj_add_bool(doc, module, "showEmptySlots", options->showEmptySlots);
+}
+
+bool ffGeneratePhysicalMemoryJsonResult(FFPhysicalMemoryOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectPhysicalMemory(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFPhysicalMemoryResult, device, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_uint(doc, obj, "size", device->size);
+ yyjson_mut_obj_add_bool(doc, obj, "installed", device->installed);
+ yyjson_mut_obj_add_uint(doc, obj, "maxSpeed", device->maxSpeed);
+ yyjson_mut_obj_add_uint(doc, obj, "runningSpeed", device->runningSpeed);
+ yyjson_mut_obj_add_strbuf(doc, obj, "type", &device->type);
+ yyjson_mut_obj_add_strbuf(doc, obj, "locator", &device->locator);
+ yyjson_mut_obj_add_strbuf(doc, obj, "formFactor", &device->formFactor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "vendor", &device->vendor);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &device->serial);
+ yyjson_mut_obj_add_strbuf(doc, obj, "partNumber", &device->partNumber);
+ yyjson_mut_obj_add_bool(doc, obj, "ecc", device->ecc);
+ }
+
+ FF_LIST_FOR_EACH (FFPhysicalMemoryResult, device, result) {
+ ffStrbufDestroy(&device->type);
+ ffStrbufDestroy(&device->locator);
+ ffStrbufDestroy(&device->formFactor);
+ ffStrbufDestroy(&device->vendor);
+ ffStrbufDestroy(&device->serial);
+ ffStrbufDestroy(&device->partNumber);
+ }
+
+ return true;
+}
+
+void ffInitPhysicalMemoryOptions(FFPhysicalMemoryOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰑭");
+ options->showEmptySlots = false;
+}
+
+void ffDestroyPhysicalMemoryOptions(FFPhysicalMemoryOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffPhysicalMemoryModuleInfo = {
+ .name = FF_PHYSICALMEMORY_MODULE_NAME,
+ .description = "Print system physical memory devices",
+ .initOptions = (void*) ffInitPhysicalMemoryOptions,
+ .destroyOptions = (void*) ffDestroyPhysicalMemoryOptions,
+ .parseJsonObject = (void*) ffParsePhysicalMemoryJsonObject,
+ .printModule = (void*) ffPrintPhysicalMemory,
+ .generateJsonConfig = (void*) ffGeneratePhysicalMemoryJsonConfig,
+ .generateJsonResult = (void*) ffGeneratePhysicalMemoryJsonResult,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Size (in bytes)", "bytes" },
+ { "Size formatted", "size" },
+ { "Max speed (in MT/s)", "max-speed" },
+ { "Running speed (in MT/s)", "running-speed" },
+ { "Type (DDR4, DDR5, etc.)", "type" },
+ { "Form factor (SODIMM, DIMM, etc.)", "form-factor" },
+ { "Bank/Device Locator (BANK0/SIMM0, BANK0/SIMM1, etc.)", "locator" },
+ { "Vendor", "vendor" },
+ { "Serial number", "serial" },
+ { "Part number", "part-number" },
+ { "True if ECC enabled", "is-ecc-enabled" },
+ { "True if a memory module is installed in the slot", "is-installed" },
+ }))
+};
diff --git a/src/modules/physicalmemory/physicalmemory.h b/src/modules/physicalmemory/physicalmemory.h
new file mode 100644
index 0000000..feb3d1d
--- /dev/null
+++ b/src/modules/physicalmemory/physicalmemory.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_PHYSICALMEMORY_MODULE_NAME "PhysicalMemory"
+
+bool ffPrintPhysicalMemory(FFPhysicalMemoryOptions* options);
+void ffInitPhysicalMemoryOptions(FFPhysicalMemoryOptions* options);
+void ffDestroyPhysicalMemoryOptions(FFPhysicalMemoryOptions* options);
+
+extern FFModuleBaseInfo ffPhysicalMemoryModuleInfo;
diff --git a/src/modules/player/option.h b/src/modules/player/option.h
new file mode 100644
index 0000000..17aa9ba
--- /dev/null
+++ b/src/modules/player/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFPlayerOptions {
+ FFModuleArgs moduleArgs;
+} FFPlayerOptions;
+
+static_assert(sizeof(FFPlayerOptions) <= FF_OPTION_MAX_SIZE, "FFPlayerOptions size exceeds maximum allowed size");
diff --git a/src/modules/player/player.c b/src/modules/player/player.c
new file mode 100644
index 0000000..58d1b3b
--- /dev/null
+++ b/src/modules/player/player.c
@@ -0,0 +1,126 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/media/media.h"
+#include "modules/player/player.h"
+
+#include <ctype.h>
+
+#define FF_PLAYER_DISPLAY_NAME "Media Player"
+
+bool ffPrintPlayer(FFPlayerOptions* options) {
+ const FFMediaResult* media = ffDetectMedia(false);
+
+ if (media->error.length > 0) {
+ ffPrintError(FF_PLAYER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", media->error.chars);
+ return false;
+ }
+
+ if (media->player.length == 0) {
+ ffPrintError(FF_PLAYER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No media player detected");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY playerPretty = ffStrbufCreate();
+
+ if (media->url.length > 0) {
+ // If we are on a website, prepend the website name
+ if (
+ ffStrbufIgnCaseEqualS(&media->playerId, "spotify") ||
+ ffStrbufIgnCaseEqualS(&media->playerId, "vlc")) {
+ } // do noting, surely not a website, even if the url is set
+ else if (ffStrbufStartsWithS(&media->url, "https://www.")) {
+ ffStrbufAppendS(&playerPretty, media->url.chars + 12);
+ } else if (ffStrbufStartsWithS(&media->url, "http://www.")) {
+ ffStrbufAppendS(&playerPretty, media->url.chars + 11);
+ } else if (ffStrbufStartsWithS(&media->url, "https://")) {
+ ffStrbufAppendS(&playerPretty, media->url.chars + 8);
+ } else if (ffStrbufStartsWithS(&media->url, "http://")) {
+ ffStrbufAppendS(&playerPretty, media->url.chars + 7);
+ }
+ }
+
+ // If we found a website name, make it more pretty
+ if (playerPretty.length > 0) {
+ ffStrbufSubstrBeforeFirstC(&playerPretty, '/'); // Remove the path
+ ffStrbufSubstrBeforeLastC(&playerPretty, '.'); // Remove the TLD
+ }
+
+ // Check again for length, as we may have removed everything.
+ bool playerPrettyIsCustom = playerPretty.length > 0;
+
+ // If we don't have subdomains, it is usually more pretty to capitalize the first letter.
+ if (playerPrettyIsCustom && ffStrbufFirstIndexC(&playerPretty, '.') == playerPretty.length) {
+ playerPretty.chars[0] = (char) toupper(playerPretty.chars[0]);
+ }
+
+ if (playerPrettyIsCustom) {
+ ffStrbufAppendS(&playerPretty, " (");
+ }
+
+ ffStrbufAppend(&playerPretty, &media->player);
+
+ if (playerPrettyIsCustom) {
+ ffStrbufAppendC(&playerPretty, ')');
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_PLAYER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&playerPretty, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_PLAYER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(playerPretty, "player"),
+ FF_ARG(media->player, "name"),
+ FF_ARG(media->playerId, "id"),
+ FF_ARG(media->url, "url"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParsePlayerJsonObject(FFPlayerOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_PLAYER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGeneratePlayerJsonConfig(FFPlayerOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGeneratePlayerJsonResult(FF_A_UNUSED FFMediaOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Player module is an alias of Media module");
+ return false;
+}
+
+void ffInitPlayerOptions(FFPlayerOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰥠");
+}
+
+void ffDestroyPlayerOptions(FFPlayerOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffPlayerModuleInfo = {
+ .name = FF_PLAYER_MODULE_NAME,
+ .description = "Print the music player name that is currently active",
+ .initOptions = (void*) ffInitPlayerOptions,
+ .destroyOptions = (void*) ffDestroyPlayerOptions,
+ .parseJsonObject = (void*) ffParsePlayerJsonObject,
+ .printModule = (void*) ffPrintPlayer,
+ .generateJsonResult = (void*) ffGeneratePlayerJsonResult,
+ .generateJsonConfig = (void*) ffGeneratePlayerJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Pretty player name", "player" },
+ { "Player name", "name" },
+ { "Player Identifier", "id" },
+ { "URL name", "url" },
+ }))
+};
diff --git a/src/modules/player/player.h b/src/modules/player/player.h
new file mode 100644
index 0000000..8b18d29
--- /dev/null
+++ b/src/modules/player/player.h
@@ -0,0 +1,12 @@
+
+#pragma once
+
+#include "option.h"
+
+#define FF_PLAYER_MODULE_NAME "Player"
+
+bool ffPrintPlayer(FFPlayerOptions* options);
+void ffInitPlayerOptions(FFPlayerOptions* options);
+void ffDestroyPlayerOptions(FFPlayerOptions* options);
+
+extern FFModuleBaseInfo ffPlayerModuleInfo;
diff --git a/src/modules/poweradapter/option.h b/src/modules/poweradapter/option.h
new file mode 100644
index 0000000..6f28754
--- /dev/null
+++ b/src/modules/poweradapter/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFPowerAdapterOptions {
+ FFModuleArgs moduleArgs;
+} FFPowerAdapterOptions;
+
+static_assert(sizeof(FFPowerAdapterOptions) <= FF_OPTION_MAX_SIZE, "FFPowerAdapterOptions size exceeds maximum allowed size");
diff --git a/src/modules/poweradapter/poweradapter.c b/src/modules/poweradapter/poweradapter.c
new file mode 100644
index 0000000..2e70c00
--- /dev/null
+++ b/src/modules/poweradapter/poweradapter.c
@@ -0,0 +1,129 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/poweradapter/poweradapter.h"
+#include "modules/poweradapter/poweradapter.h"
+
+#define FF_POWERADAPTER_DISPLAY_NAME "Power Adapter"
+
+bool ffPrintPowerAdapter(FFPowerAdapterOptions* options) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectPowerAdapter(&results);
+
+ if (error) {
+ ffPrintError(FF_POWERADAPTER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (results.length == 0) {
+ ffPrintError(FF_POWERADAPTER_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No power adapters found");
+ return false;
+ }
+
+ for (uint8_t i = 0; i < (uint8_t) results.length; i++) {
+ FFPowerAdapterResult* result = FF_LIST_GET(FFPowerAdapterResult, results, i);
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_POWERADAPTER_DISPLAY_NAME, i, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ if (result->name.length > 0) {
+ puts(result->name.chars);
+ } else {
+ printf("%dW\n", result->watts);
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_POWERADAPTER_DISPLAY_NAME, i, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->watts, "watts"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(result->manufacturer, "manufacturer"),
+ FF_ARG(result->modelName, "model-name"),
+ FF_ARG(result->description, "description"),
+ FF_ARG(result->serial, "serial"),
+ }));
+ }
+
+ ffStrbufDestroy(&result->manufacturer);
+ ffStrbufDestroy(&result->description);
+ ffStrbufDestroy(&result->modelName);
+ ffStrbufDestroy(&result->name);
+ ffStrbufDestroy(&result->serial);
+ }
+
+ return true;
+}
+
+void ffGeneratePowerAdapterJsonConfig(FFPowerAdapterOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+void ffParsePowerAdapterJsonObject(FFPowerAdapterOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_POWERADAPTER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+bool ffGeneratePowerAdapterJsonResult(FF_A_UNUSED FFPowerAdapterOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectPowerAdapter(&results);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFPowerAdapterResult, item, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "description", &item->description);
+ yyjson_mut_obj_add_strbuf(doc, obj, "manufacturer", &item->manufacturer);
+ yyjson_mut_obj_add_strbuf(doc, obj, "modelName", &item->modelName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "serial", &item->serial);
+ yyjson_mut_obj_add_int(doc, obj, "watts", item->watts);
+ }
+
+ FF_LIST_FOR_EACH (FFPowerAdapterResult, item, results) {
+ ffStrbufDestroy(&item->manufacturer);
+ ffStrbufDestroy(&item->description);
+ ffStrbufDestroy(&item->modelName);
+ ffStrbufDestroy(&item->name);
+ ffStrbufDestroy(&item->serial);
+ }
+
+ return true;
+}
+
+void ffInitPowerAdapterOptions(FFPowerAdapterOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰚥");
+}
+
+void ffDestroyPowerAdapterOptions(FFPowerAdapterOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffPowerAdapterModuleInfo = {
+ .name = FF_POWERADAPTER_MODULE_NAME,
+ .description = "Print power adapter name and charging watts",
+ .initOptions = (void*) ffInitPowerAdapterOptions,
+ .destroyOptions = (void*) ffDestroyPowerAdapterOptions,
+ .parseJsonObject = (void*) ffParsePowerAdapterJsonObject,
+ .printModule = (void*) ffPrintPowerAdapter,
+ .generateJsonResult = (void*) ffGeneratePowerAdapterJsonResult,
+ .generateJsonConfig = (void*) ffGeneratePowerAdapterJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Power adapter watts", "watts" },
+ { "Power adapter name", "name" },
+ { "Power adapter manufacturer", "manufacturer" },
+ { "Power adapter model", "model" },
+ { "Power adapter description", "description" },
+ { "Power adapter serial number", "serial" },
+ }))
+};
diff --git a/src/modules/poweradapter/poweradapter.h b/src/modules/poweradapter/poweradapter.h
new file mode 100644
index 0000000..3e61025
--- /dev/null
+++ b/src/modules/poweradapter/poweradapter.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_POWERADAPTER_MODULE_NAME "PowerAdapter"
+
+bool ffPrintPowerAdapter(FFPowerAdapterOptions* options);
+void ffInitPowerAdapterOptions(FFPowerAdapterOptions* options);
+void ffDestroyPowerAdapterOptions(FFPowerAdapterOptions* options);
+
+extern FFModuleBaseInfo ffPowerAdapterModuleInfo;
diff --git a/src/modules/processes/option.h b/src/modules/processes/option.h
new file mode 100644
index 0000000..2ee2563
--- /dev/null
+++ b/src/modules/processes/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFProcessesOptions {
+ FFModuleArgs moduleArgs;
+} FFProcessesOptions;
+
+static_assert(sizeof(FFProcessesOptions) <= FF_OPTION_MAX_SIZE, "FFProcessesOptions size exceeds maximum allowed size");
diff --git a/src/modules/processes/processes.c b/src/modules/processes/processes.c
new file mode 100644
index 0000000..44ab59d
--- /dev/null
+++ b/src/modules/processes/processes.c
@@ -0,0 +1,76 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/processes/processes.h"
+#include "modules/processes/processes.h"
+
+bool ffPrintProcesses(FFProcessesOptions* options) {
+ uint32_t numProcesses = 0;
+ const char* error = ffDetectProcesses(&numProcesses);
+
+ if (error) {
+ ffPrintError(FF_PROCESSES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_PROCESSES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ printf("%u\n", numProcesses);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_PROCESSES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(numProcesses, "result") }));
+ }
+
+ return true;
+}
+
+void ffParseProcessesJsonObject(FFProcessesOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_PROCESSES_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateProcessesJsonConfig(FFProcessesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateProcessesJsonResult(FF_A_UNUSED FFProcessesOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ uint32_t result;
+ const char* error = ffDetectProcesses(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_obj_add_uint(doc, module, "result", result);
+
+ return true;
+}
+
+void ffInitProcessesOptions(FFProcessesOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyProcessesOptions(FFProcessesOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffProcessesModuleInfo = {
+ .name = FF_PROCESSES_MODULE_NAME,
+ .description = "Print number of running processes",
+ .initOptions = (void*) ffInitProcessesOptions,
+ .destroyOptions = (void*) ffDestroyProcessesOptions,
+ .parseJsonObject = (void*) ffParseProcessesJsonObject,
+ .printModule = (void*) ffPrintProcesses,
+ .generateJsonResult = (void*) ffGenerateProcessesJsonResult,
+ .generateJsonConfig = (void*) ffGenerateProcessesJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Process count", "result" } }))
+};
diff --git a/src/modules/processes/processes.h b/src/modules/processes/processes.h
new file mode 100644
index 0000000..0220c17
--- /dev/null
+++ b/src/modules/processes/processes.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_PROCESSES_MODULE_NAME "Processes"
+
+bool ffPrintProcesses(FFProcessesOptions* options);
+void ffInitProcessesOptions(FFProcessesOptions* options);
+void ffDestroyProcessesOptions(FFProcessesOptions* options);
+
+extern FFModuleBaseInfo ffProcessesModuleInfo;
diff --git a/src/modules/publicip/option.h b/src/modules/publicip/option.h
new file mode 100644
index 0000000..aca7236
--- /dev/null
+++ b/src/modules/publicip/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFPublicIPOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf url;
+ uint32_t timeout;
+ bool ipv6;
+} FFPublicIPOptions;
+
+static_assert(sizeof(FFPublicIPOptions) <= FF_OPTION_MAX_SIZE, "FFPublicIPOptions size exceeds maximum allowed size");
diff --git a/src/modules/publicip/publicip.c b/src/modules/publicip/publicip.c
new file mode 100644
index 0000000..5521733
--- /dev/null
+++ b/src/modules/publicip/publicip.c
@@ -0,0 +1,125 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "modules/publicip/publicip.h"
+#include "detection/publicip/publicip.h"
+
+#define FF_PUBLICIP_DISPLAY_NAME "Public IP"
+
+bool ffPrintPublicIp(FFPublicIPOptions* options) {
+ FFPublicIpResult result;
+ ffStrbufInit(&result.ip);
+ ffStrbufInit(&result.location);
+ const char* error = ffDetectPublicIp(options, &result);
+
+ if (error) {
+ ffPrintError(FF_PUBLICIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_PUBLICIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (result.location.length) {
+ printf("%s (%s)\n", result.ip.chars, result.location.chars);
+ } else {
+ ffStrbufPutTo(&result.ip, stdout);
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_PUBLICIP_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.ip, "ip"),
+ FF_ARG(result.location, "location"),
+ }));
+ }
+
+ ffStrbufDestroy(&result.ip);
+ ffStrbufDestroy(&result.location);
+
+ return true;
+}
+
+void ffParsePublicIpJsonObject(FFPublicIPOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "url")) {
+ ffStrbufSetJsonVal(&options->url, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "timeout")) {
+ options->timeout = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "ipv6")) {
+ options->ipv6 = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_PUBLICIP_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGeneratePublicIpJsonConfig(FFPublicIPOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "url", &options->url);
+
+ yyjson_mut_obj_add_uint(doc, module, "timeout", options->timeout);
+
+ yyjson_mut_obj_add_bool(doc, module, "ipv6", options->ipv6);
+}
+
+bool ffGeneratePublicIpJsonResult(FFPublicIPOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFPublicIpResult result;
+ ffStrbufInit(&result.ip);
+ ffStrbufInit(&result.location);
+ const char* error = ffDetectPublicIp(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "ip", &result.ip);
+ yyjson_mut_obj_add_strbuf(doc, obj, "location", &result.location);
+
+ ffStrbufDestroy(&result.ip);
+ ffStrbufDestroy(&result.location);
+
+ return true;
+}
+
+void ffInitPublicIpOptions(FFPublicIPOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰩠");
+
+ ffStrbufInit(&options->url);
+ options->timeout = 0;
+ options->ipv6 = false;
+}
+
+void ffDestroyPublicIpOptions(FFPublicIPOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+
+ ffStrbufDestroy(&options->url);
+}
+
+FFModuleBaseInfo ffPublicIPModuleInfo = {
+ .name = FF_PUBLICIP_MODULE_NAME,
+ .description = "Print your public IP address and related information",
+ .initOptions = (void*) ffInitPublicIpOptions,
+ .destroyOptions = (void*) ffDestroyPublicIpOptions,
+ .parseJsonObject = (void*) ffParsePublicIpJsonObject,
+ .printModule = (void*) ffPrintPublicIp,
+ .generateJsonResult = (void*) ffGeneratePublicIpJsonResult,
+ .generateJsonConfig = (void*) ffGeneratePublicIpJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Public IP address", "ip" },
+ { "Location", "location" },
+ }))
+};
diff --git a/src/modules/publicip/publicip.h b/src/modules/publicip/publicip.h
new file mode 100644
index 0000000..92768cb
--- /dev/null
+++ b/src/modules/publicip/publicip.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_PUBLICIP_MODULE_NAME "PublicIp"
+
+void ffPreparePublicIp(FFPublicIPOptions* options);
+
+bool ffPrintPublicIp(FFPublicIPOptions* options);
+void ffInitPublicIpOptions(FFPublicIPOptions* options);
+void ffDestroyPublicIpOptions(FFPublicIPOptions* options);
+
+extern FFModuleBaseInfo ffPublicIPModuleInfo;
diff --git a/src/modules/separator/option.h b/src/modules/separator/option.h
new file mode 100644
index 0000000..510132c
--- /dev/null
+++ b/src/modules/separator/option.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFSeparatorOptions {
+ FFstrbuf string;
+ FFstrbuf outputColor;
+ uint32_t times;
+} FFSeparatorOptions;
+
+static_assert(sizeof(FFSeparatorOptions) <= FF_OPTION_MAX_SIZE, "FFSeparatorOptions size exceeds maximum allowed size");
diff --git a/src/modules/separator/separator.c b/src/modules/separator/separator.c
new file mode 100644
index 0000000..4c5b4bc
--- /dev/null
+++ b/src/modules/separator/separator.c
@@ -0,0 +1,130 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "common/textModifier.h"
+#include "logo/logo.h"
+#include "modules/separator/separator.h"
+
+bool ffPrintSeparator(FFSeparatorOptions* options) {
+ ffLogoPrintLine();
+
+ if (options->outputColor.length && !instance.config.display.pipe) {
+ ffPrintColor(&options->outputColor);
+ }
+
+ if (options->times > 0) {
+ if (__builtin_expect(options->string.length == 1, 1)) {
+ ffPrintCharTimes(options->string.chars[0], options->times);
+ } else {
+ for (uint32_t i = 0; i < options->times; i++) {
+ fputs(options->string.chars, stdout);
+ }
+ }
+ } else {
+ const FFPlatform* platform = &instance.state.platform;
+
+ uint32_t titleLength = 1 // @
+ + ffUtf8StrWidth(platform->userName.chars, platform->userName.length) // user name
+ + (instance.state.titleFqdn ? platform->hostName.length : ffStrbufFirstIndexC(&platform->hostName, '.')); // host name
+
+ if (__builtin_expect(options->string.length == 1, 1)) {
+ ffPrintCharTimes(options->string.chars[0], titleLength);
+ } else {
+ uint32_t wcsLength = ffUtf8StrWidth(options->string.chars, options->string.length);
+
+ int remaining = (int) titleLength;
+ // Write the whole separator as often as it fits fully into titleLength
+ for (; remaining >= (int) wcsLength; remaining -= (int) wcsLength) {
+ ffStrbufWriteTo(&options->string, stdout);
+ }
+
+ if (remaining > 0) {
+ // Write as much of the separator as needed to fill titleLength
+ if (wcsLength != options->string.length) {
+ // Unicode chars
+ const char* ptr = options->string.chars;
+ uint32_t remainBytes = options->string.length;
+ while (remaining > 0 && remainBytes > 0 && *ptr != '\0') {
+ uint8_t charWidth = 0;
+ uint8_t bytes = ffUtf8CharLenWidth(ptr, remainBytes, &charWidth);
+ if (__builtin_expect(bytes == 0, false)) {
+ break;
+ }
+
+ remaining -= (int) charWidth;
+ ptr += bytes;
+ remainBytes -= bytes;
+ }
+ fwrite(options->string.chars, (size_t) (ptr - options->string.chars), 1, stdout);
+ } else {
+ fwrite(options->string.chars, (size_t) remaining, 1, stdout);
+ }
+ }
+ }
+ }
+
+ if (options->outputColor.length && !instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ }
+ putchar('\n');
+
+ return true;
+}
+
+void ffParseSeparatorJsonObject(FFSeparatorOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "string")) {
+ ffStrbufSetJsonVal(&options->string, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "outputColor")) {
+ ffOptionParseColor(yyjson_get_str(val), &options->outputColor);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "times")) {
+ options->times = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "length")) {
+ ffPrintError(FF_SEPARATOR_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "The option length has been renamed to times.");
+ continue;
+ }
+
+ ffPrintError(FF_SEPARATOR_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateSeparatorJsonConfig(FFSeparatorOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ yyjson_mut_obj_add_strbuf(doc, module, "string", &options->string);
+ yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &options->outputColor);
+ yyjson_mut_obj_add_uint(doc, module, "times", options->times);
+}
+
+void ffInitSeparatorOptions(FFSeparatorOptions* options) {
+ ffStrbufInitStatic(&options->string, "-");
+ ffStrbufInit(&options->outputColor);
+ options->times = 0;
+}
+
+void ffDestroySeparatorOptions(FFSeparatorOptions* options) {
+ ffStrbufDestroy(&options->string);
+}
+
+FFModuleBaseInfo ffSeparatorModuleInfo = {
+ .name = FF_SEPARATOR_MODULE_NAME,
+ .description = "Print a separator line",
+ .initOptions = (void*) ffInitSeparatorOptions,
+ .destroyOptions = (void*) ffDestroySeparatorOptions,
+ .parseJsonObject = (void*) ffParseSeparatorJsonObject,
+ .printModule = (void*) ffPrintSeparator,
+ .generateJsonConfig = (void*) ffGenerateSeparatorJsonConfig,
+};
diff --git a/src/modules/separator/separator.h b/src/modules/separator/separator.h
new file mode 100644
index 0000000..eebf39a
--- /dev/null
+++ b/src/modules/separator/separator.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_SEPARATOR_MODULE_NAME "Separator"
+
+bool ffPrintSeparator(FFSeparatorOptions* options);
+void ffInitSeparatorOptions(FFSeparatorOptions* options);
+void ffDestroySeparatorOptions(FFSeparatorOptions* options);
+
+extern FFModuleBaseInfo ffSeparatorModuleInfo;
diff --git a/src/modules/shell/option.h b/src/modules/shell/option.h
new file mode 100644
index 0000000..bd8ed24
--- /dev/null
+++ b/src/modules/shell/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFShellOptions {
+ FFModuleArgs moduleArgs;
+} FFShellOptions;
+
+static_assert(sizeof(FFShellOptions) <= FF_OPTION_MAX_SIZE, "FFShellOptions size exceeds maximum allowed size");
diff --git a/src/modules/shell/shell.c b/src/modules/shell/shell.c
new file mode 100644
index 0000000..f18c1bf
--- /dev/null
+++ b/src/modules/shell/shell.c
@@ -0,0 +1,110 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/terminalshell/terminalshell.h"
+#include "modules/shell/shell.h"
+
+bool ffPrintShell(FFShellOptions* options) {
+ const FFShellResult* result = ffDetectShell();
+
+ if (result->processName.length == 0) {
+ ffPrintError(FF_SHELL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Couldn't detect shell");
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_SHELL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&result->prettyName, stdout);
+
+ if (result->version.length > 0) {
+ putchar(' ');
+ ffStrbufWriteTo(&result->version, stdout);
+ }
+
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_SHELL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->processName, "process-name"),
+ FF_ARG(result->exe, "exe"),
+ FF_ARG(result->exeName, "exe-name"),
+ FF_ARG(result->version, "version"),
+ FF_ARG(result->pid, "pid"),
+ FF_ARG(result->prettyName, "pretty-name"),
+ FF_ARG(result->exePath, "exe-path"),
+ FF_ARG(result->tty, "tty"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseShellJsonObject(FFShellOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_SHELL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateShellJsonConfig(FFShellOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateShellJsonResult(FF_A_UNUSED FFShellOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFShellResult* result = ffDetectShell();
+
+ if (result->processName.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Couldn't detect shell");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "exe", &result->exe);
+ yyjson_mut_obj_add_strcpy(doc, obj, "exeName", result->exeName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exePath", &result->exePath);
+ yyjson_mut_obj_add_uint(doc, obj, "pid", result->pid);
+ yyjson_mut_obj_add_uint(doc, obj, "ppid", result->ppid);
+ yyjson_mut_obj_add_strbuf(doc, obj, "processName", &result->processName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "prettyName", &result->prettyName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result->version);
+ if (result->tty >= 0) {
+ yyjson_mut_obj_add_int(doc, obj, "tty", result->tty);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "tty");
+ }
+
+ return true;
+}
+
+void ffInitShellOptions(FFShellOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyShellOptions(FFShellOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffShellModuleInfo = {
+ .name = FF_SHELL_MODULE_NAME,
+ .description = "Print the current shell name and version",
+ .initOptions = (void*) ffInitShellOptions,
+ .destroyOptions = (void*) ffDestroyShellOptions,
+ .parseJsonObject = (void*) ffParseShellJsonObject,
+ .printModule = (void*) ffPrintShell,
+ .generateJsonResult = (void*) ffGenerateShellJsonResult,
+ .generateJsonConfig = (void*) ffGenerateShellJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Shell process name", "process-name" },
+ { "The first argument of the command line when running the shell", "exe" },
+ { "Shell base name of arg0", "exe-name" },
+ { "Shell version", "version" },
+ { "Shell pid", "pid" },
+ { "Shell pretty name", "pretty-name" },
+ { "Shell full exe path", "exe-path" },
+ { "Shell tty used", "tty" },
+ }))
+};
diff --git a/src/modules/shell/shell.h b/src/modules/shell/shell.h
new file mode 100644
index 0000000..50a094c
--- /dev/null
+++ b/src/modules/shell/shell.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_SHELL_MODULE_NAME "Shell"
+
+bool ffPrintShell(FFShellOptions* options);
+void ffInitShellOptions(FFShellOptions* options);
+void ffDestroyShellOptions(FFShellOptions* options);
+
+extern FFModuleBaseInfo ffShellModuleInfo;
diff --git a/src/modules/sound/option.h b/src/modules/sound/option.h
new file mode 100644
index 0000000..9892569
--- /dev/null
+++ b/src/modules/sound/option.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef enum FF_A_PACKED FFSoundType {
+ FF_SOUND_TYPE_NONE = 0,
+ FF_SOUND_TYPE_MAIN = 1 << 0,
+ FF_SOUND_TYPE_ACTIVE = 1 << 1,
+} FFSoundType;
+
+typedef struct FFSoundOptions {
+ FFModuleArgs moduleArgs;
+
+ // Reports matched device only, otherwise reports all devices
+ // NOTE: for FF_SOUND_TYPE_NONE, reports all devices
+ FFSoundType soundType;
+ FFPercentageModuleConfig percent;
+} FFSoundOptions;
+
+static_assert(sizeof(FFSoundOptions) <= FF_OPTION_MAX_SIZE, "FFSoundOptions size exceeds maximum allowed size");
diff --git a/src/modules/sound/sound.c b/src/modules/sound/sound.c
new file mode 100644
index 0000000..bf37c1f
--- /dev/null
+++ b/src/modules/sound/sound.c
@@ -0,0 +1,219 @@
+#include "common/percent.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/sound/sound.h"
+#include "modules/sound/sound.h"
+
+static void printDevice(FFSoundOptions* options, const FFSoundDevice* device, uint8_t index) {
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_SOUND_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppend(&str, &device->name);
+ }
+
+ if (device->volume != FF_SOUND_VOLUME_UNKNOWN) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ if (str.length) {
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ ffPercentAppendBar(&str, device->volume, options->percent, &options->moduleArgs);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ if (str.length) {
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ ffPercentAppendNum(&str, device->volume, options->percent, str.length > 0, &options->moduleArgs);
+ }
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ if ((device->type & FF_SOUND_TYPE_MAIN) && index > 0) {
+ ffStrbufAppendS(&str, " (*)");
+ }
+ }
+
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (device->volume != FF_SOUND_VOLUME_UNKNOWN) {
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentageNum, device->volume, options->percent, false, &options->moduleArgs);
+ }
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentageBar, device->volume, options->percent, &options->moduleArgs);
+ }
+ }
+
+ bool isMain = !!(device->type & FF_SOUND_TYPE_MAIN);
+ bool isActive = !!(device->type & FF_SOUND_TYPE_ACTIVE);
+ FF_PRINT_FORMAT_CHECKED(FF_SOUND_MODULE_NAME, index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(isMain, "is-main"),
+ FF_ARG(isActive, "is-active"),
+ FF_ARG(device->name, "name"),
+ FF_ARG(percentageNum, "volume-percentage"),
+ FF_ARG(device->identifier, "identifier"),
+ FF_ARG(percentageBar, "volume-percentage-bar"),
+ FF_ARG(device->platformApi, "platform-api"),
+ }));
+ }
+}
+
+bool ffPrintSound(FFSoundOptions* options) {
+ bool success = false;
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectSound(options, &result);
+
+ if (error) {
+ ffPrintError(FF_SOUND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (result.length == 0) {
+ ffPrintError(FF_SOUND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No matched sound devices found");
+ return false;
+ }
+
+ uint8_t index = 1;
+ FF_LIST_FOR_EACH (FFSoundDevice, device, result) {
+ printDevice(options, device, result.length == 1 ? 0 : index++);
+ }
+
+ FF_LIST_FOR_EACH (FFSoundDevice, device, result) {
+ ffStrbufDestroy(&device->identifier);
+ ffStrbufDestroy(&device->name);
+ ffStrbufDestroy(&device->platformApi);
+ }
+
+ return success;
+}
+
+void ffParseSoundJsonObject(FFSoundOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "soundType")) {
+ int value;
+ const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
+ { "main", FF_SOUND_TYPE_MAIN },
+ { "active", FF_SOUND_TYPE_ACTIVE },
+ { "all", FF_SOUND_TYPE_NONE }, // Don't filter devices
+ {},
+ });
+ if (error) {
+ ffPrintError(FF_SOUND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
+ } else {
+ options->soundType = (FFSoundType) value;
+ }
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_SOUND_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateSoundJsonConfig(FFSoundOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ switch (options->soundType) {
+ case FF_SOUND_TYPE_MAIN:
+ yyjson_mut_obj_add_str(doc, module, "soundType", "main");
+ break;
+ case FF_SOUND_TYPE_ACTIVE:
+ yyjson_mut_obj_add_str(doc, module, "soundType", "active");
+ break;
+ case FF_SOUND_TYPE_NONE:
+ yyjson_mut_obj_add_str(doc, module, "soundType", "all");
+ break;
+ }
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateSoundJsonResult(FFSoundOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectSound(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFSoundDevice, item, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "identifier", &item->identifier);
+ yyjson_mut_obj_add_strbuf(doc, obj, "platformApi", &item->platformApi);
+
+ yyjson_mut_val* type = yyjson_mut_obj_add_arr(doc, obj, "type");
+ if (item->type & FF_SOUND_TYPE_MAIN) {
+ yyjson_mut_arr_add_str(doc, type, "main");
+ }
+ if (item->type & FF_SOUND_TYPE_ACTIVE) {
+ yyjson_mut_arr_add_str(doc, type, "active");
+ }
+
+ if (item->volume != FF_SOUND_VOLUME_UNKNOWN) {
+ yyjson_mut_obj_add_uint(doc, obj, "volume", item->volume);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "volume");
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFSoundDevice, device, result) {
+ ffStrbufDestroy(&device->identifier);
+ ffStrbufDestroy(&device->name);
+ ffStrbufDestroy(&device->platformApi);
+ }
+
+ return true;
+}
+
+void ffInitSoundOptions(FFSoundOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->soundType = FF_SOUND_TYPE_MAIN;
+ options->percent = (FFPercentageModuleConfig) { 80, 90, 0 };
+}
+
+void ffDestroySoundOptions(FFSoundOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffSoundModuleInfo = {
+ .name = FF_SOUND_MODULE_NAME,
+ .description = "Print sound devices, volume levels, etc",
+ .initOptions = (void*) ffInitSoundOptions,
+ .destroyOptions = (void*) ffDestroySoundOptions,
+ .parseJsonObject = (void*) ffParseSoundJsonObject,
+ .printModule = (void*) ffPrintSound,
+ .generateJsonResult = (void*) ffGenerateSoundJsonResult,
+ .generateJsonConfig = (void*) ffGenerateSoundJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Is main sound device", "is-main" },
+ { "Is active sound device", "is-active" },
+ { "Device name", "name" },
+ { "Volume (in percentage num)", "volume-percentage" },
+ { "Identifier", "identifier" },
+ { "Volume (in percentage bar)", "volume-percentage-bar" },
+ { "Platform API used", "platform-api" },
+ }))
+};
diff --git a/src/modules/sound/sound.h b/src/modules/sound/sound.h
new file mode 100644
index 0000000..a23f8d5
--- /dev/null
+++ b/src/modules/sound/sound.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_SOUND_MODULE_NAME "Sound"
+
+bool ffPrintSound(FFSoundOptions* options);
+void ffInitSoundOptions(FFSoundOptions* options);
+void ffDestroySoundOptions(FFSoundOptions* options);
+
+extern FFModuleBaseInfo ffSoundModuleInfo;
diff --git a/src/modules/swap/option.h b/src/modules/swap/option.h
new file mode 100644
index 0000000..860da9c
--- /dev/null
+++ b/src/modules/swap/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFSwapOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+ bool separate;
+} FFSwapOptions;
+
+static_assert(sizeof(FFSwapOptions) <= FF_OPTION_MAX_SIZE, "FFSwapOptions size exceeds maximum allowed size");
diff --git a/src/modules/swap/swap.c b/src/modules/swap/swap.c
new file mode 100644
index 0000000..78af475
--- /dev/null
+++ b/src/modules/swap/swap.c
@@ -0,0 +1,200 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/size.h"
+#include "common/strutil.h"
+#include "detection/swap/swap.h"
+#include "modules/swap/swap.h"
+
+void printSwap(FFSwapOptions* options, uint8_t index, uint32_t totalCount, FFSwapResult* storage) {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+
+ if (options->moduleArgs.key.length == 0) {
+ if (storage->name.length > 0) {
+ ffStrbufSetF(&key, "%s (%s)", FF_SWAP_MODULE_NAME, storage->name.chars);
+ } else {
+ ffStrbufSetS(&key, FF_SWAP_MODULE_NAME);
+ }
+ } else {
+ ffStrbufClear(&key);
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(storage->name, "name"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
+ ffSizeAppendNum(storage->bytesUsed, &usedPretty);
+
+ FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
+ ffSizeAppendNum(storage->bytesTotal, &totalPretty);
+
+ double percentage = storage->bytesTotal == 0
+ ? 0
+ : (double) storage->bytesUsed / (double) storage->bytesTotal * 100.0;
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+ FF_STRBUF_AUTO_DESTROY str = ffStrbufCreate();
+
+ if (storage->bytesTotal == 0) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, 0, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&str, ' ');
+ }
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppendS(&str, totalCount ? "Unused" : "Disabled");
+ } else {
+ ffPercentAppendNum(&str, 0, options->percent, str.length > 0, &options->moduleArgs);
+ }
+ } else {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&str, percentage, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&str, ' ');
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppendF(&str, "%s / %s ", usedPretty.chars, totalPretty.chars);
+ }
+
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&str, percentage, options->percent, str.length > 0, &options->moduleArgs);
+ }
+ }
+
+ ffStrbufTrimRight(&str, ' ');
+ ffStrbufPutTo(&str, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentageNum, percentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY percentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentageBar, percentage, options->percent, &options->moduleArgs);
+ }
+ FF_PRINT_FORMAT_CHECKED(key.chars, index, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(usedPretty, "used"),
+ FF_ARG(totalPretty, "total"),
+ FF_ARG(percentageNum, "percentage"),
+ FF_ARG(percentageBar, "percentage-bar"),
+ FF_ARG(storage->name, "name"),
+ }));
+ }
+}
+
+bool ffPrintSwap(FFSwapOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectSwap(&result);
+
+ if (error) {
+ ffPrintError(FF_SWAP_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->separate && result.length > 0) {
+ uint8_t index = 0;
+ FF_LIST_FOR_EACH (FFSwapResult, storage, result) {
+ ++index;
+ printSwap(options, index, result.length, storage);
+ }
+ } else {
+ FFSwapResult total = {
+ .name = ffStrbufCreate(),
+ };
+ FF_LIST_FOR_EACH (FFSwapResult, storage, result) {
+ total.bytesUsed += storage->bytesUsed;
+ total.bytesTotal += storage->bytesTotal;
+ }
+ printSwap(options, 0, result.length, &total);
+ ffStrbufDestroy(&total.name);
+ }
+
+ FF_LIST_FOR_EACH (FFSwapResult, storage, result) {
+ ffStrbufDestroy(&storage->name);
+ }
+
+ return true;
+}
+
+void ffParseSwapJsonObject(FFSwapOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "separate")) {
+ options->separate = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_SWAP_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateSwapJsonConfig(FFSwapOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+ yyjson_mut_obj_add_bool(doc, module, "separate", options->separate);
+}
+
+bool ffGenerateSwapJsonResult(FF_A_UNUSED FFSwapOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectSwap(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFSwapResult, storage, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &storage->name);
+ yyjson_mut_obj_add_uint(doc, obj, "used", storage->bytesUsed);
+ yyjson_mut_obj_add_uint(doc, obj, "total", storage->bytesTotal);
+ }
+
+ FF_LIST_FOR_EACH (FFSwapResult, storage, result) {
+ ffStrbufDestroy(&storage->name);
+ }
+
+ return true;
+}
+
+void ffInitSwapOptions(FFSwapOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰓡");
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+ options->separate = false;
+}
+
+void ffDestroySwapOptions(FFSwapOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffSwapModuleInfo = {
+ .name = FF_SWAP_MODULE_NAME,
+ .description = "Print swap (paging file) space usage",
+ .initOptions = (void*) ffInitSwapOptions,
+ .destroyOptions = (void*) ffDestroySwapOptions,
+ .parseJsonObject = (void*) ffParseSwapJsonObject,
+ .printModule = (void*) ffPrintSwap,
+ .generateJsonResult = (void*) ffGenerateSwapJsonResult,
+ .generateJsonConfig = (void*) ffGenerateSwapJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Used size", "used" },
+ { "Total size", "total" },
+ { "Percentage used (num)", "percentage" },
+ { "Percentage used (bar)", "percentage-bar" },
+ { "Name", "name" },
+ }))
+};
diff --git a/src/modules/swap/swap.h b/src/modules/swap/swap.h
new file mode 100644
index 0000000..e6b9a8b
--- /dev/null
+++ b/src/modules/swap/swap.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_SWAP_MODULE_NAME "Swap"
+
+bool ffPrintSwap(FFSwapOptions* options);
+void ffInitSwapOptions(FFSwapOptions* options);
+void ffDestroySwapOptions(FFSwapOptions* options);
+
+extern FFModuleBaseInfo ffSwapModuleInfo;
diff --git a/src/modules/terminal/option.h b/src/modules/terminal/option.h
new file mode 100644
index 0000000..79f6970
--- /dev/null
+++ b/src/modules/terminal/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTerminalOptions {
+ FFModuleArgs moduleArgs;
+} FFTerminalOptions;
+
+static_assert(sizeof(FFTerminalOptions) <= FF_OPTION_MAX_SIZE, "FFTerminalOptions size exceeds maximum allowed size");
diff --git a/src/modules/terminal/terminal.c b/src/modules/terminal/terminal.c
new file mode 100644
index 0000000..8dc6f4c
--- /dev/null
+++ b/src/modules/terminal/terminal.c
@@ -0,0 +1,104 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/terminalshell/terminalshell.h"
+#include "modules/terminal/terminal.h"
+
+bool ffPrintTerminal(FFTerminalOptions* options) {
+ const FFTerminalResult* result = ffDetectTerminal();
+
+ if (result->processName.length == 0) {
+ ffPrintError(FF_TERMINAL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Couldn't detect terminal");
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TERMINAL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ if (result->version.length) {
+ printf("%s %s\n", result->prettyName.chars, result->version.chars);
+ } else {
+ ffStrbufPutTo(&result->prettyName, stdout);
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_TERMINAL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->processName, "process-name"),
+ FF_ARG(result->exe, "exe"),
+ FF_ARG(result->exeName, "exe-name"),
+ FF_ARG(result->pid, "pid"),
+ FF_ARG(result->prettyName, "pretty-name"),
+ FF_ARG(result->version, "version"),
+ FF_ARG(result->exePath, "exe-path"),
+ FF_ARG(result->tty, "tty"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseTerminalJsonObject(FFTerminalOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_TERMINAL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTerminalJsonConfig(FFTerminalOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateTerminalJsonResult(FF_A_UNUSED FFTerminalOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFTerminalResult* result = ffDetectTerminal();
+
+ if (result->processName.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Couldn't detect terminal");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "processName", &result->processName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exe", &result->exe);
+ yyjson_mut_obj_add_strcpy(doc, obj, "exeName", result->exeName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exePath", &result->exePath);
+ yyjson_mut_obj_add_uint(doc, obj, "pid", result->pid);
+ yyjson_mut_obj_add_uint(doc, obj, "ppid", result->ppid);
+ yyjson_mut_obj_add_strbuf(doc, obj, "prettyName", &result->prettyName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result->version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "tty", &result->tty);
+
+ return true;
+}
+
+void ffInitTerminalOptions(FFTerminalOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyTerminalOptions(FFTerminalOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffTerminalModuleInfo = {
+ .name = FF_TERMINAL_MODULE_NAME,
+ .description = "Print the current terminal name and version",
+ .initOptions = (void*) ffInitTerminalOptions,
+ .destroyOptions = (void*) ffDestroyTerminalOptions,
+ .parseJsonObject = (void*) ffParseTerminalJsonObject,
+ .printModule = (void*) ffPrintTerminal,
+ .generateJsonResult = (void*) ffGenerateTerminalJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTerminalJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Terminal process name", "process-name" },
+ { "The first argument of the command line when running the terminal", "exe" },
+ { "Terminal base name of arg0", "exe-name" },
+ { "Terminal pid", "pid" },
+ { "Terminal pretty name", "pretty-name" },
+ { "Terminal version", "version" },
+ { "Terminal full exe path", "exe-path" },
+ { "Terminal tty / pts used", "tty" },
+ }))
+};
diff --git a/src/modules/terminal/terminal.h b/src/modules/terminal/terminal.h
new file mode 100644
index 0000000..bb7c9a1
--- /dev/null
+++ b/src/modules/terminal/terminal.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TERMINAL_MODULE_NAME "Terminal"
+
+bool ffPrintTerminal(FFTerminalOptions* options);
+void ffInitTerminalOptions(FFTerminalOptions* options);
+void ffDestroyTerminalOptions(FFTerminalOptions* options);
+
+extern FFModuleBaseInfo ffTerminalModuleInfo;
diff --git a/src/modules/terminalfont/option.h b/src/modules/terminalfont/option.h
new file mode 100644
index 0000000..6da3d1e
--- /dev/null
+++ b/src/modules/terminalfont/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTerminalFontOptions {
+ FFModuleArgs moduleArgs;
+} FFTerminalFontOptions;
+
+static_assert(sizeof(FFTerminalFontOptions) <= FF_OPTION_MAX_SIZE, "FFTerminalFontOptions size exceeds maximum allowed size");
diff --git a/src/modules/terminalfont/terminalfont.c b/src/modules/terminalfont/terminalfont.c
new file mode 100644
index 0000000..91e45c7
--- /dev/null
+++ b/src/modules/terminalfont/terminalfont.c
@@ -0,0 +1,122 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/terminalfont/terminalfont.h"
+#include "modules/terminalfont/terminalfont.h"
+
+#define FF_TERMINALFONT_DISPLAY_NAME "Terminal Font"
+
+bool ffPrintTerminalFont(FFTerminalFontOptions* options) {
+ bool success = false;
+ FFTerminalFontResult terminalFont;
+ ffFontInit(&terminalFont.font);
+ ffFontInit(&terminalFont.fallback);
+ ffStrbufInit(&terminalFont.error);
+
+ if (!ffDetectTerminalFont(&terminalFont)) {
+ ffPrintError(FF_TERMINALFONT_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", terminalFont.error.chars);
+ } else {
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TERMINALFONT_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufWriteTo(&terminalFont.font.pretty, stdout);
+ if (terminalFont.fallback.pretty.length) {
+ fputs(" / ", stdout);
+ ffStrbufWriteTo(&terminalFont.fallback.pretty, stdout);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_TERMINALFONT_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(terminalFont.font.pretty, "combined"),
+ FF_ARG(terminalFont.font.name, "name"),
+ FF_ARG(terminalFont.font.size, "size"),
+ FF_ARG(terminalFont.font.styles, "styles"),
+ }));
+ }
+ success = true;
+ }
+
+ ffStrbufDestroy(&terminalFont.error);
+ ffFontDestroy(&terminalFont.font);
+ ffFontDestroy(&terminalFont.fallback);
+
+ return success;
+}
+
+void ffParseTerminalFontJsonObject(FFTerminalFontOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_TERMINALFONT_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTerminalFontJsonConfig(FFTerminalFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateTerminalFontJsonResult(FF_A_UNUSED FFTerminalFontOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ bool success = false;
+ FFTerminalFontResult result;
+ ffFontInit(&result.font);
+ ffFontInit(&result.fallback);
+ ffStrbufInit(&result.error);
+
+ if (!ffDetectTerminalFont(&result)) {
+ yyjson_mut_obj_add_strbuf(doc, module, "error", &result.error);
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ yyjson_mut_val* font = yyjson_mut_obj_add_obj(doc, obj, "font");
+ yyjson_mut_obj_add_strbuf(doc, font, "name", &result.font.name);
+ yyjson_mut_obj_add_strbuf(doc, font, "size", &result.font.size);
+ yyjson_mut_val* fontStyles = yyjson_mut_obj_add_arr(doc, font, "styles");
+ FF_LIST_FOR_EACH (FFstrbuf, style, result.font.styles) {
+ yyjson_mut_arr_add_strbuf(doc, fontStyles, style);
+ }
+ yyjson_mut_obj_add_strbuf(doc, font, "pretty", &result.font.pretty);
+
+ yyjson_mut_val* fallback = yyjson_mut_obj_add_obj(doc, obj, "fallback");
+ yyjson_mut_obj_add_strbuf(doc, fallback, "name", &result.fallback.name);
+ yyjson_mut_obj_add_strbuf(doc, fallback, "size", &result.fallback.size);
+ yyjson_mut_val* fallbackStyles = yyjson_mut_obj_add_arr(doc, fallback, "styles");
+ FF_LIST_FOR_EACH (FFstrbuf, style, result.fallback.styles) {
+ yyjson_mut_arr_add_strbuf(doc, fallbackStyles, style);
+ }
+ yyjson_mut_obj_add_strbuf(doc, fallback, "pretty", &result.fallback.pretty);
+ success = true;
+ }
+
+ ffStrbufDestroy(&result.error);
+ ffFontDestroy(&result.font);
+ ffFontDestroy(&result.fallback);
+ return success;
+}
+
+void ffInitTerminalFontOptions(FFTerminalFontOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyTerminalFontOptions(FFTerminalFontOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffTerminalFontModuleInfo = {
+ .name = FF_TERMINALFONT_MODULE_NAME,
+ .description = "Print the font name and size used by the current terminal",
+ .initOptions = (void*) ffInitTerminalFontOptions,
+ .destroyOptions = (void*) ffDestroyTerminalFontOptions,
+ .parseJsonObject = (void*) ffParseTerminalFontJsonObject,
+ .printModule = (void*) ffPrintTerminalFont,
+ .generateJsonResult = (void*) ffGenerateTerminalFontJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTerminalFontJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Terminal font combined", "combined" },
+ { "Terminal font name", "name" },
+ { "Terminal font size", "size" },
+ { "Terminal font styles", "styles" },
+ })),
+};
diff --git a/src/modules/terminalfont/terminalfont.h b/src/modules/terminalfont/terminalfont.h
new file mode 100644
index 0000000..04987a6
--- /dev/null
+++ b/src/modules/terminalfont/terminalfont.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TERMINALFONT_MODULE_NAME "TerminalFont"
+
+bool ffPrintTerminalFont(FFTerminalFontOptions* options);
+void ffInitTerminalFontOptions(FFTerminalFontOptions* options);
+void ffDestroyTerminalFontOptions(FFTerminalFontOptions* options);
+
+extern FFModuleBaseInfo ffTerminalFontModuleInfo;
diff --git a/src/modules/terminalsize/option.h b/src/modules/terminalsize/option.h
new file mode 100644
index 0000000..482f754
--- /dev/null
+++ b/src/modules/terminalsize/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTerminalSizeOptions {
+ FFModuleArgs moduleArgs;
+} FFTerminalSizeOptions;
+
+static_assert(sizeof(FFTerminalSizeOptions) <= FF_OPTION_MAX_SIZE, "FFTerminalSizeOptions size exceeds maximum allowed size");
diff --git a/src/modules/terminalsize/terminalsize.c b/src/modules/terminalsize/terminalsize.c
new file mode 100644
index 0000000..4a7c555
--- /dev/null
+++ b/src/modules/terminalsize/terminalsize.c
@@ -0,0 +1,93 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/terminalsize/terminalsize.h"
+#include "modules/terminalsize/terminalsize.h"
+
+#define FF_TERMINALSIZE_DISPLAY_NAME "Terminal Size"
+
+bool ffPrintTerminalSize(FFTerminalSizeOptions* options) {
+ FFTerminalSizeResult result = {};
+
+ if (!ffDetectTerminalSize(&result)) {
+ ffPrintError(FF_TERMINALSIZE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Failed to detect terminal size");
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TERMINALSIZE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ printf("%u columns x %u rows", result.columns, result.rows);
+
+ if (result.width != 0 && result.height != 0) {
+ printf(" (%upx x %upx)", result.width, result.height);
+ }
+
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_TERMINALSIZE_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.rows, "rows"),
+ FF_ARG(result.columns, "columns"),
+ FF_ARG(result.width, "width"),
+ FF_ARG(result.height, "height"),
+ }));
+ }
+ return true;
+}
+
+void ffParseTerminalSizeJsonObject(FFTerminalSizeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_TERMINALSIZE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTerminalSizeJsonConfig(FFTerminalSizeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateTerminalSizeJsonResult(FF_A_UNUSED FFTerminalSizeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFTerminalSizeResult result;
+
+ if (!ffDetectTerminalSize(&result)) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Failed to detect terminal size");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_uint(doc, obj, "columns", result.columns);
+ yyjson_mut_obj_add_uint(doc, obj, "rows", result.rows);
+ yyjson_mut_obj_add_uint(doc, obj, "width", result.width);
+ yyjson_mut_obj_add_uint(doc, obj, "height", result.height);
+
+ return true;
+}
+
+void ffInitTerminalSizeOptions(FFTerminalSizeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰲎");
+}
+
+void ffDestroyTerminalSizeOptions(FFTerminalSizeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffTerminalSizeModuleInfo = {
+ .name = FF_TERMINALSIZE_MODULE_NAME,
+ .description = "Print the current terminal size",
+ .initOptions = (void*) ffInitTerminalSizeOptions,
+ .destroyOptions = (void*) ffDestroyTerminalSizeOptions,
+ .parseJsonObject = (void*) ffParseTerminalSizeJsonObject,
+ .printModule = (void*) ffPrintTerminalSize,
+ .generateJsonResult = (void*) ffGenerateTerminalSizeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTerminalSizeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Terminal rows", "rows" },
+ { "Terminal columns", "columns" },
+ { "Terminal width (in pixels)", "width" },
+ { "Terminal height (in pixels)", "height" },
+ })),
+};
diff --git a/src/modules/terminalsize/terminalsize.h b/src/modules/terminalsize/terminalsize.h
new file mode 100644
index 0000000..3cd05d5
--- /dev/null
+++ b/src/modules/terminalsize/terminalsize.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TERMINALSIZE_MODULE_NAME "TerminalSize"
+
+bool ffPrintTerminalSize(FFTerminalSizeOptions* options);
+void ffInitTerminalSizeOptions(FFTerminalSizeOptions* options);
+void ffDestroyTerminalSizeOptions(FFTerminalSizeOptions* options);
+
+extern FFModuleBaseInfo ffTerminalSizeModuleInfo;
diff --git a/src/modules/terminaltheme/option.h b/src/modules/terminaltheme/option.h
new file mode 100644
index 0000000..25a0cc3
--- /dev/null
+++ b/src/modules/terminaltheme/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTerminalThemeOptions {
+ FFModuleArgs moduleArgs;
+} FFTerminalThemeOptions;
+
+static_assert(sizeof(FFTerminalThemeOptions) <= FF_OPTION_MAX_SIZE, "FFTerminalThemeOptions size exceeds maximum allowed size");
diff --git a/src/modules/terminaltheme/terminaltheme.c b/src/modules/terminaltheme/terminaltheme.c
new file mode 100644
index 0000000..80bcd29
--- /dev/null
+++ b/src/modules/terminaltheme/terminaltheme.c
@@ -0,0 +1,110 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/terminaltheme/terminaltheme.h"
+#include "modules/terminaltheme/terminaltheme.h"
+
+#include <inttypes.h>
+
+#define FF_TERMINALTHEME_DISPLAY_NAME "Terminal Theme"
+
+bool ffPrintTerminalTheme(FFTerminalThemeOptions* options) {
+ FFTerminalThemeResult result = {};
+
+ if (!ffDetectTerminalTheme(&result, false)) {
+ ffPrintError(FF_TERMINALTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Failed to detect terminal theme");
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TERMINALTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ printf("#%02" PRIX16 "%02" PRIX16 "%02" PRIX16 " (FG) - #%02" PRIX16 "%02" PRIX16 "%02" PRIX16 " (BG) [%s]\n",
+ result.fg.r,
+ result.fg.g,
+ result.fg.b,
+ result.bg.r,
+ result.bg.g,
+ result.bg.b,
+ result.bg.dark ? "Dark" : "Light");
+ } else {
+ char fg[32], bg[32];
+ const char* fgType = result.fg.dark ? "Dark" : "Light";
+ const char* bgType = result.bg.dark ? "Dark" : "Light";
+ snprintf(fg, ARRAY_SIZE(fg), "#%02" PRIX16 "%02" PRIX16 "%02" PRIX16, result.fg.r, result.fg.g, result.fg.b);
+ snprintf(bg, ARRAY_SIZE(bg), "#%02" PRIX16 "%02" PRIX16 "%02" PRIX16, result.bg.r, result.bg.g, result.bg.b);
+ FF_PRINT_FORMAT_CHECKED(FF_TERMINALTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(fg, "fg-color"),
+ FF_ARG(fgType, "fg-type"),
+ FF_ARG(bg, "bg-color"),
+ FF_ARG(bgType, "bg-type"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseTerminalThemeJsonObject(FFTerminalThemeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_TERMINALTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTerminalThemeJsonConfig(FFTerminalThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateTerminalThemeJsonResult(FF_A_UNUSED FFTerminalThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFTerminalThemeResult result = {};
+
+ if (!ffDetectTerminalTheme(&result, false)) {
+ yyjson_mut_obj_add_str(doc, module, "error", "Failed to detect terminal theme");
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+
+ yyjson_mut_val* fg = yyjson_mut_obj_add_obj(doc, obj, "fg");
+ yyjson_mut_obj_add_uint(doc, fg, "r", result.fg.r);
+ yyjson_mut_obj_add_uint(doc, fg, "g", result.fg.g);
+ yyjson_mut_obj_add_uint(doc, fg, "b", result.fg.b);
+ yyjson_mut_obj_add_bool(doc, fg, "dark", result.fg.dark);
+
+ yyjson_mut_val* bg = yyjson_mut_obj_add_obj(doc, obj, "bg");
+ yyjson_mut_obj_add_uint(doc, bg, "r", result.bg.r);
+ yyjson_mut_obj_add_uint(doc, bg, "g", result.bg.g);
+ yyjson_mut_obj_add_uint(doc, bg, "b", result.bg.b);
+ yyjson_mut_obj_add_bool(doc, bg, "dark", result.bg.dark);
+
+ return true;
+}
+
+void ffInitTerminalThemeOptions(FFTerminalThemeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰔎");
+}
+
+void ffDestroyTerminalThemeOptions(FFTerminalThemeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffTerminalThemeModuleInfo = {
+ .name = FF_TERMINALTHEME_MODULE_NAME,
+ .description = "Print the current terminal theme (foreground and background colors)",
+ .initOptions = (void*) ffInitTerminalThemeOptions,
+ .destroyOptions = (void*) ffDestroyTerminalThemeOptions,
+ .parseJsonObject = (void*) ffParseTerminalThemeJsonObject,
+ .printModule = (void*) ffPrintTerminalTheme,
+ .generateJsonResult = (void*) ffGenerateTerminalThemeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTerminalThemeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Terminal foreground color", "fg-color" },
+ { "Terminal foreground type (Dark / Light)", "fg-type" },
+ { "Terminal background color", "bg-color" },
+ { "Terminal background type (Dark / Light)", "bg-type" },
+ }))
+};
diff --git a/src/modules/terminaltheme/terminaltheme.h b/src/modules/terminaltheme/terminaltheme.h
new file mode 100644
index 0000000..db120f7
--- /dev/null
+++ b/src/modules/terminaltheme/terminaltheme.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TERMINALTHEME_MODULE_NAME "TerminalTheme"
+
+bool ffPrintTerminalTheme(FFTerminalThemeOptions* options);
+void ffInitTerminalThemeOptions(FFTerminalThemeOptions* options);
+void ffDestroyTerminalThemeOptions(FFTerminalThemeOptions* options);
+
+extern FFModuleBaseInfo ffTerminalThemeModuleInfo;
diff --git a/src/modules/theme/option.h b/src/modules/theme/option.h
new file mode 100644
index 0000000..66a68ba
--- /dev/null
+++ b/src/modules/theme/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFThemeOptions {
+ FFModuleArgs moduleArgs;
+} FFThemeOptions;
+
+static_assert(sizeof(FFThemeOptions) <= FF_OPTION_MAX_SIZE, "FFThemeOptions size exceeds maximum allowed size");
diff --git a/src/modules/theme/theme.c b/src/modules/theme/theme.c
new file mode 100644
index 0000000..70cb778
--- /dev/null
+++ b/src/modules/theme/theme.c
@@ -0,0 +1,102 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/theme/theme.h"
+#include "modules/theme/theme.h"
+
+bool ffPrintTheme(FFThemeOptions* options) {
+ FFThemeResult result = {
+ .theme1 = ffStrbufCreate(),
+ .theme2 = ffStrbufCreate()
+ };
+ const char* error = ffDetectTheme(&result);
+
+ if (error) {
+ ffPrintError(FF_THEME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_THEME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (result.theme1.length) {
+ ffStrbufWriteTo(&result.theme1, stdout);
+ }
+ if (result.theme2.length) {
+ if (result.theme1.length) {
+ fputs(", ", stdout);
+ }
+ ffStrbufWriteTo(&result.theme2, stdout);
+ }
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_THEME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.theme1, "theme1"),
+ FF_ARG(result.theme2, "theme2"),
+ }));
+ }
+
+ ffStrbufDestroy(&result.theme1);
+ ffStrbufDestroy(&result.theme2);
+ return true;
+}
+
+void ffParseThemeJsonObject(FFThemeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_THEME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateThemeJsonConfig(FFThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateThemeJsonResult(FF_A_UNUSED FFThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFThemeResult result = {
+ .theme1 = ffStrbufCreate(),
+ .theme2 = ffStrbufCreate()
+ };
+ const char* error = ffDetectTheme(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* theme = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, theme, "theme1", &result.theme1);
+ yyjson_mut_obj_add_strbuf(doc, theme, "theme2", &result.theme2);
+
+ ffStrbufDestroy(&result.theme1);
+ ffStrbufDestroy(&result.theme2);
+
+ return true;
+}
+
+void ffInitThemeOptions(FFThemeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰉼");
+}
+
+void ffDestroyThemeOptions(FFThemeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffThemeModuleInfo = {
+ .name = FF_THEME_MODULE_NAME,
+ .description = "Print the current desktop environment theme",
+ .initOptions = (void*) ffInitThemeOptions,
+ .destroyOptions = (void*) ffDestroyThemeOptions,
+ .parseJsonObject = (void*) ffParseThemeJsonObject,
+ .printModule = (void*) ffPrintTheme,
+ .generateJsonResult = (void*) ffGenerateThemeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateThemeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Theme part 1", "theme1" },
+ { "Theme part 2", "theme2" },
+ }))
+};
diff --git a/src/modules/theme/theme.h b/src/modules/theme/theme.h
new file mode 100644
index 0000000..56ba49c
--- /dev/null
+++ b/src/modules/theme/theme.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_THEME_MODULE_NAME "Theme"
+
+bool ffPrintTheme(FFThemeOptions* options);
+void ffInitThemeOptions(FFThemeOptions* options);
+void ffDestroyThemeOptions(FFThemeOptions* options);
+
+extern FFModuleBaseInfo ffThemeModuleInfo;
diff --git a/src/modules/title/option.h b/src/modules/title/option.h
new file mode 100644
index 0000000..e66fdba
--- /dev/null
+++ b/src/modules/title/option.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTitleOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf colorUser;
+ FFstrbuf colorAt;
+ FFstrbuf colorHost;
+ bool fqdn;
+} FFTitleOptions;
+
+static_assert(sizeof(FFTitleOptions) <= FF_OPTION_MAX_SIZE, "FFTitleOptions size exceeds maximum allowed size");
diff --git a/src/modules/title/title.c b/src/modules/title/title.c
new file mode 100644
index 0000000..97e5843
--- /dev/null
+++ b/src/modules/title/title.c
@@ -0,0 +1,201 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/textModifier.h"
+#include "modules/title/title.h"
+
+static void appendText(FFstrbuf* output, const FFstrbuf* text, const FFstrbuf* color) {
+ if (!instance.config.display.pipe) {
+ if (instance.config.display.brightColor) {
+ ffStrbufAppendS(output, FASTFETCH_TEXT_MODIFIER_BOLT);
+ }
+ if (color->length > 0) {
+ ffStrbufAppendF(output, "\e[%sm", color->chars);
+ } else if (instance.config.display.colorTitle.length > 0) {
+ ffStrbufAppendF(output, "\e[%sm", instance.config.display.colorTitle.chars);
+ }
+ }
+
+ ffStrbufAppend(output, text);
+
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendS(output, FASTFETCH_TEXT_MODIFIER_RESET);
+ }
+}
+
+bool ffPrintTitle(FFTitleOptions* options) {
+ FF_STRBUF_AUTO_DESTROY userNameColored = ffStrbufCreate();
+ appendText(&userNameColored, &instance.state.platform.userName, &options->colorUser);
+
+ FF_STRBUF_AUTO_DESTROY hostName = ffStrbufCreateCopy(&instance.state.platform.hostName);
+ if (!options->fqdn) {
+ ffStrbufSubstrBeforeFirstC(&hostName, '.');
+ }
+ instance.state.titleFqdn = options->fqdn;
+
+ FF_STRBUF_AUTO_DESTROY hostNameColored = ffStrbufCreate();
+ appendText(&hostNameColored, &hostName, &options->colorHost);
+
+ FF_STRBUF_AUTO_DESTROY atColored = ffStrbufCreate();
+ if (!instance.config.display.pipe && options->colorAt.length > 0) {
+ ffStrbufAppendF(&atColored, "\e[%sm", options->colorAt.chars);
+ ffStrbufAppendC(&atColored, '@');
+ ffStrbufAppendS(&atColored, FASTFETCH_TEXT_MODIFIER_RESET);
+ } else {
+ ffStrbufAppendC(&atColored, '@');
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TITLE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ ffStrbufWriteTo(&userNameColored, stdout);
+ ffStrbufWriteTo(&atColored, stdout);
+ ffStrbufPutTo(&hostNameColored, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY cwdTilde = ffStrbufCreate();
+ if (
+#if _WIN32
+ ffStrbufStartsWithIgnCase
+#else
+ ffStrbufStartsWith
+#endif
+ (&instance.state.platform.cwd, &instance.state.platform.homeDir)) {
+ ffStrbufAppendS(&cwdTilde, "~/");
+ ffStrbufAppendNS(&cwdTilde, instance.state.platform.cwd.length - instance.state.platform.homeDir.length, &instance.state.platform.cwd.chars[instance.state.platform.homeDir.length]);
+ } else {
+ ffStrbufSet(&cwdTilde, &instance.state.platform.cwd);
+ }
+ if (cwdTilde.length > 1) {
+ ffStrbufTrimRight(&cwdTilde, '/');
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_TITLE_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(instance.state.platform.userName, "user-name"),
+ FF_ARG(hostName, "host-name"),
+ FF_ARG(instance.state.platform.homeDir, "home-dir"),
+ FF_ARG(instance.state.platform.exePath, "exe-path"),
+ FF_ARG(instance.state.platform.userShell, "user-shell"),
+ FF_ARG(userNameColored, "user-name-colored"),
+ FF_ARG(atColored, "at-symbol-colored"),
+ FF_ARG(hostNameColored, "host-name-colored"),
+ FF_ARG(instance.state.platform.fullUserName, "full-user-name"),
+#ifndef _WIN32
+ FF_ARG(instance.state.platform.uid, "user-id"),
+#else
+ FF_ARG(instance.state.platform.sid, "user-id"),
+#endif
+ FF_ARG(instance.state.platform.pid, "pid"),
+ FF_ARG(cwdTilde, "cwd"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseTitleJsonObject(FFTitleOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "fqdn")) {
+ options->fqdn = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "color")) {
+ if (!yyjson_is_obj(val)) {
+ continue;
+ }
+
+ yyjson_val* color = yyjson_obj_get(val, "user");
+ if (color) {
+ ffOptionParseColor(yyjson_get_str(color), &options->colorUser);
+ }
+ color = yyjson_obj_get(val, "at");
+ if (color) {
+ ffOptionParseColor(yyjson_get_str(color), &options->colorAt);
+ }
+ color = yyjson_obj_get(val, "host");
+ if (color) {
+ ffOptionParseColor(yyjson_get_str(color), &options->colorHost);
+ }
+ continue;
+ }
+
+ ffPrintError(FF_TITLE_MODULE_NAME, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTitleJsonConfig(FFTitleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "fqdn", options->fqdn);
+
+ yyjson_mut_val* color = yyjson_mut_obj_add_obj(doc, module, "color");
+ yyjson_mut_obj_add_strbuf(doc, color, "user", &options->colorUser);
+ yyjson_mut_obj_add_strbuf(doc, color, "at", &options->colorAt);
+ yyjson_mut_obj_add_strbuf(doc, color, "host", &options->colorHost);
+}
+
+bool ffGenerateTitleJsonResult(FF_A_UNUSED FFTitleOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+#ifdef _WIN32
+ yyjson_mut_obj_add_strbuf(doc, obj, "userId", &instance.state.platform.sid);
+#else
+ yyjson_mut_obj_add_uint(doc, obj, "userId", instance.state.platform.uid);
+#endif
+ yyjson_mut_obj_add_strbuf(doc, obj, "userName", &instance.state.platform.userName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "fullUserName", &instance.state.platform.fullUserName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "hostName", &instance.state.platform.hostName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "homeDir", &instance.state.platform.homeDir);
+ yyjson_mut_obj_add_strbuf(doc, obj, "exePath", &instance.state.platform.exePath);
+ yyjson_mut_obj_add_strbuf(doc, obj, "userShell", &instance.state.platform.userShell);
+ yyjson_mut_obj_add_uint(doc, obj, "pid", instance.state.platform.pid);
+ yyjson_mut_obj_add_strbuf(doc, obj, "cwd", &instance.state.platform.cwd);
+
+ return true;
+}
+
+void ffInitTitleOptions(FFTitleOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ ffStrbufSetStatic(&options->moduleArgs.key, " ");
+
+ options->fqdn = false;
+ ffStrbufInit(&options->colorUser);
+ ffStrbufInit(&options->colorAt);
+ ffStrbufInit(&options->colorHost);
+}
+
+void ffDestroyTitleOptions(FFTitleOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+ ffStrbufDestroy(&options->colorUser);
+ ffStrbufDestroy(&options->colorAt);
+ ffStrbufDestroy(&options->colorHost);
+}
+
+FFModuleBaseInfo ffTitleModuleInfo = {
+ .name = FF_TITLE_MODULE_NAME,
+ .description = "Print the title, including your username and hostname",
+ .initOptions = (void*) ffInitTitleOptions,
+ .destroyOptions = (void*) ffDestroyTitleOptions,
+ .parseJsonObject = (void*) ffParseTitleJsonObject,
+ .printModule = (void*) ffPrintTitle,
+ .generateJsonResult = (void*) ffGenerateTitleJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTitleJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "User name", "user-name" },
+ { "Host name", "host-name" },
+ { "Home directory", "home-dir" },
+ { "Executable path of current process", "exe-path" },
+ { "User's default shell", "user-shell" },
+ { "User name (colored)", "user-name-colored" },
+ { "@ symbol (colored)", "at-symbol-colored" },
+ { "Host name (colored)", "host-name-colored" },
+ { "Full user name", "full-user-name" },
+ { "UID (*nix) / SID (Windows)", "user-id" },
+ { "PID of current process", "pid" },
+ { "CWD with home dir replaced by `~`", "cwd" },
+ }))
+};
diff --git a/src/modules/title/title.h b/src/modules/title/title.h
new file mode 100644
index 0000000..83c9496
--- /dev/null
+++ b/src/modules/title/title.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TITLE_MODULE_NAME "Title"
+
+bool ffPrintTitle(FFTitleOptions* options);
+void ffInitTitleOptions(FFTitleOptions* options);
+void ffDestroyTitleOptions(FFTitleOptions* options);
+
+extern FFModuleBaseInfo ffTitleModuleInfo;
diff --git a/src/modules/tpm/option.h b/src/modules/tpm/option.h
new file mode 100644
index 0000000..d40806f
--- /dev/null
+++ b/src/modules/tpm/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFTPMOptions {
+ FFModuleArgs moduleArgs;
+} FFTPMOptions;
+
+static_assert(sizeof(FFTPMOptions) <= FF_OPTION_MAX_SIZE, "FFTPMOptions size exceeds maximum allowed size");
diff --git a/src/modules/tpm/tpm.c b/src/modules/tpm/tpm.c
new file mode 100644
index 0000000..ecdd386
--- /dev/null
+++ b/src/modules/tpm/tpm.c
@@ -0,0 +1,98 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/tpm/tpm.h"
+#include "modules/tpm/tpm.h"
+
+bool ffPrintTPM(FFTPMOptions* options) {
+ FFTPMResult result = {
+ .version = ffStrbufCreate(),
+ .description = ffStrbufCreate()
+ };
+ const char* error = ffDetectTPM(&result);
+
+ if (error) {
+ ffPrintError(FF_TPM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_TPM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ if (result.description.length > 0) {
+ ffStrbufPutTo(&result.description, stdout);
+ } else {
+ ffStrbufPutTo(&result.version, stdout);
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_TPM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result.version, "version"),
+ FF_ARG(result.description, "description"),
+ }));
+ }
+
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.description);
+
+ return true;
+}
+
+void ffParseTPMJsonObject(FFTPMOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_TPM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateTPMJsonConfig(FFTPMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateTPMJsonResult(FF_A_UNUSED FFTPMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFTPMResult result = {
+ .version = ffStrbufCreate(),
+ .description = ffStrbufCreate()
+ };
+ const char* error = ffDetectTPM(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
+ yyjson_mut_obj_add_strbuf(doc, obj, "description", &result.description);
+
+ ffStrbufDestroy(&result.version);
+ ffStrbufDestroy(&result.description);
+
+ return true;
+}
+
+void ffInitTPMOptions(FFTPMOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyTPMOptions(FFTPMOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffTPMModuleInfo = {
+ .name = FF_TPM_MODULE_NAME,
+ .description = "Print information about the Trusted Platform Module (TPM) security device",
+ .initOptions = (void*) ffInitTPMOptions,
+ .destroyOptions = (void*) ffDestroyTPMOptions,
+ .parseJsonObject = (void*) ffParseTPMJsonObject,
+ .printModule = (void*) ffPrintTPM,
+ .generateJsonResult = (void*) ffGenerateTPMJsonResult,
+ .generateJsonConfig = (void*) ffGenerateTPMJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "TPM device version", "version" },
+ { "TPM general description", "description" },
+ }))
+};
diff --git a/src/modules/tpm/tpm.h b/src/modules/tpm/tpm.h
new file mode 100644
index 0000000..215f8c9
--- /dev/null
+++ b/src/modules/tpm/tpm.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_TPM_MODULE_NAME "TPM"
+
+bool ffPrintTPM(FFTPMOptions* options);
+void ffInitTPMOptions(FFTPMOptions* options);
+void ffDestroyTPMOptions(FFTPMOptions* options);
+
+extern FFModuleBaseInfo ffTPMModuleInfo;
diff --git a/src/modules/uptime/option.h b/src/modules/uptime/option.h
new file mode 100644
index 0000000..34be61a
--- /dev/null
+++ b/src/modules/uptime/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFUptimeOptions {
+ FFModuleArgs moduleArgs;
+} FFUptimeOptions;
+
+static_assert(sizeof(FFUptimeOptions) <= FF_OPTION_MAX_SIZE, "FFUptimeOptions size exceeds maximum allowed size");
diff --git a/src/modules/uptime/uptime.c b/src/modules/uptime/uptime.c
new file mode 100644
index 0000000..e096bfc
--- /dev/null
+++ b/src/modules/uptime/uptime.c
@@ -0,0 +1,106 @@
+#include "common/duration.h"
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/time.h"
+#include "common/strutil.h"
+#include "detection/uptime/uptime.h"
+#include "modules/uptime/uptime.h"
+
+bool ffPrintUptime(FFUptimeOptions* options) {
+ FFUptimeResult result = {};
+
+ const char* error = ffDetectUptime(&result);
+
+ if (error) {
+ ffPrintError(FF_UPTIME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ uint64_t uptime = result.uptime;
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ ffDurationAppendNum((uptime + 500) / 1000, &buffer);
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_UPTIME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ uint32_t milliseconds = (uint32_t) (uptime % 1000);
+ uptime /= 1000;
+ uint32_t seconds = (uint32_t) (uptime % 60);
+ uptime /= 60;
+ uint32_t minutes = (uint32_t) (uptime % 60);
+ uptime /= 60;
+ uint32_t hours = (uint32_t) (uptime % 24);
+ uptime /= 24;
+ uint32_t days = (uint32_t) uptime;
+
+ FFTimeGetAgeResult age = ffTimeGetAge(result.bootTime, ffTimeGetNow());
+
+ FF_PRINT_FORMAT_CHECKED(FF_UPTIME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(days, "days"), FF_ARG(hours, "hours"), FF_ARG(minutes, "minutes"), FF_ARG(seconds, "seconds"), FF_ARG(milliseconds, "milliseconds"), { FF_ARG_TYPE_STRING, ffTimeToShortStr(result.bootTime), "boot-time" }, FF_ARG(age.years, "years"), FF_ARG(age.daysOfYear, "days-of-year"), FF_ARG(age.yearsFraction, "years-fraction"), FF_ARG(buffer, "formatted") }));
+ }
+
+ return true;
+}
+
+void ffParseUptimeJsonObject(FFUptimeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_UPTIME_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateUptimeJsonConfig(FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateUptimeJsonResult(FF_A_UNUSED FFUptimeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFUptimeResult result;
+ const char* error = ffDetectUptime(&result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_uint(doc, obj, "uptime", result.uptime);
+ yyjson_mut_obj_add_strcpy(doc, obj, "bootTime", ffTimeToFullStr(result.bootTime));
+
+ return true;
+}
+
+void ffInitUptimeOptions(FFUptimeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyUptimeOptions(FFUptimeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffUptimeModuleInfo = {
+ .name = FF_UPTIME_MODULE_NAME,
+ .description = "Print how long the system has been running",
+ .initOptions = (void*) ffInitUptimeOptions,
+ .destroyOptions = (void*) ffDestroyUptimeOptions,
+ .parseJsonObject = (void*) ffParseUptimeJsonObject,
+ .printModule = (void*) ffPrintUptime,
+ .generateJsonResult = (void*) ffGenerateUptimeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateUptimeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Days after boot", "days" },
+ { "Hours after boot", "hours" },
+ { "Minutes after boot", "minutes" },
+ { "Seconds after boot", "seconds" },
+ { "Milliseconds after boot", "milliseconds" },
+ { "Boot time in local timezone", "boot-time" },
+ { "Years integer after boot", "years" },
+ { "Days of year after boot", "days-of-year" },
+ { "Years fraction after boot", "years-fraction" },
+ { "Formatted uptime", "formatted" },
+ }))
+};
diff --git a/src/modules/uptime/uptime.h b/src/modules/uptime/uptime.h
new file mode 100644
index 0000000..dec5f95
--- /dev/null
+++ b/src/modules/uptime/uptime.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_UPTIME_MODULE_NAME "Uptime"
+
+bool ffPrintUptime(FFUptimeOptions* options);
+void ffInitUptimeOptions(FFUptimeOptions* options);
+void ffDestroyUptimeOptions(FFUptimeOptions* options);
+
+extern FFModuleBaseInfo ffUptimeModuleInfo;
diff --git a/src/modules/users/option.h b/src/modules/users/option.h
new file mode 100644
index 0000000..6863d49
--- /dev/null
+++ b/src/modules/users/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFUsersOptions {
+ FFModuleArgs moduleArgs;
+
+ bool compact;
+ bool myselfOnly;
+} FFUsersOptions;
+
+static_assert(sizeof(FFUsersOptions) <= FF_OPTION_MAX_SIZE, "FFUsersOptions size exceeds maximum allowed size");
diff --git a/src/modules/users/users.c b/src/modules/users/users.c
new file mode 100644
index 0000000..5575bbe
--- /dev/null
+++ b/src/modules/users/users.c
@@ -0,0 +1,206 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/time.h"
+#include "common/strutil.h"
+#include "detection/users/users.h"
+#include "modules/users/users.h"
+
+#pragma GCC diagnostic ignored "-Wformat" // warning: unknown conversion type character 'F' in format
+
+bool ffPrintUsers(FFUsersOptions* options) {
+ FF_LIST_AUTO_DESTROY users = ffListCreate();
+
+ const char* error = ffDetectUsers(options, &users);
+
+ if (error) {
+ ffPrintError(FF_USERS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (users.length == 0) {
+ ffPrintError(FF_USERS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", "Unable to detect any users");
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ if (options->compact) {
+ ffPrintLogoAndKey(FF_USERS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+ for (uint32_t i = 0; i < users.length; ++i) {
+ if (i > 0) {
+ ffStrbufAppendS(&result, ", ");
+ }
+ FFUserResult* user = FF_LIST_GET(FFUserResult, users, i);
+ ffStrbufAppend(&result, &user->name);
+ }
+ ffStrbufPutTo(&result, stdout);
+ } else {
+ for (uint32_t i = 0; i < users.length; ++i) {
+ FFUserResult* user = FF_LIST_GET(FFUserResult, users, i);
+
+ ffPrintLogoAndKey(FF_USERS_MODULE_NAME, users.length == 1 ? 0 : (uint8_t) (i + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreateCopy(&user->name);
+ if (user->hostName.length) {
+ ffStrbufAppendF(&result, "@%s", user->hostName.chars);
+ }
+
+ if (user->loginTime) {
+ ffStrbufAppendF(&result, " - login time %s", ffTimeToShortStr(user->loginTime));
+ }
+
+ ffStrbufPutTo(&result, stdout);
+ }
+ }
+ } else {
+ uint64_t now = ffTimeGetNow();
+ for (uint32_t i = 0; i < users.length; ++i) {
+ FFUserResult* user = FF_LIST_GET(FFUserResult, users, i);
+
+ uint64_t duration = now - user->loginTime;
+ uint32_t milliseconds = (uint32_t) (duration % 1000);
+ duration /= 1000;
+ uint32_t seconds = (uint32_t) (duration % 60);
+ duration /= 60;
+ uint32_t minutes = (uint32_t) (duration % 60);
+ duration /= 60;
+ uint32_t hours = (uint32_t) (duration % 24);
+ duration /= 24;
+ uint32_t days = (uint32_t) duration;
+
+ FFTimeGetAgeResult age = ffTimeGetAge(user->loginTime, ffTimeGetNow());
+
+ FF_PRINT_FORMAT_CHECKED(FF_USERS_MODULE_NAME, users.length == 1 ? 0 : (uint8_t) (i + 1), &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(user->name, "name"),
+ FF_ARG(user->hostName, "host-name"),
+ FF_ARG(user->sessionName, "session-name"),
+ FF_ARG(user->clientIp, "client-ip"),
+ { FF_ARG_TYPE_STRING, ffTimeToShortStr(user->loginTime), "login-time" },
+ FF_ARG(days, "days"),
+ FF_ARG(hours, "hours"),
+ FF_ARG(minutes, "minutes"),
+ FF_ARG(seconds, "seconds"),
+ FF_ARG(milliseconds, "milliseconds"),
+ FF_ARG(age.years, "years"),
+ FF_ARG(age.daysOfYear, "days-of-year"),
+ FF_ARG(age.yearsFraction, "years-fraction"),
+ }));
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFUserResult, user, users) {
+ ffStrbufDestroy(&user->clientIp);
+ ffStrbufDestroy(&user->hostName);
+ ffStrbufDestroy(&user->sessionName);
+ ffStrbufDestroy(&user->name);
+ }
+
+ return true;
+}
+
+void ffParseUsersJsonObject(FFUsersOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
+ continue;
+ }
+
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "compact")) {
+ options->compact = yyjson_get_bool(val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "myselfOnly")) {
+ options->myselfOnly = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_USERS_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateUsersJsonConfig(FFUsersOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "compact", options->compact);
+
+ yyjson_mut_obj_add_bool(doc, module, "myselfOnly", options->myselfOnly);
+}
+
+bool ffGenerateUsersJsonResult(FFUsersOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectUsers(options, &results);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFUserResult, user, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &user->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "hostName", &user->hostName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "sessionName", &user->sessionName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "clientIp", &user->clientIp);
+ const char* pstr = ffTimeToFullStr(user->loginTime);
+ if (*pstr) {
+ yyjson_mut_obj_add_strcpy(doc, obj, "loginTime", pstr);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "loginTime");
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFUserResult, user, results) {
+ ffStrbufDestroy(&user->clientIp);
+ ffStrbufDestroy(&user->hostName);
+ ffStrbufDestroy(&user->sessionName);
+ ffStrbufDestroy(&user->name);
+ }
+
+ return true;
+}
+
+void ffInitUsersOptions(FFUsersOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->compact = false;
+ options->myselfOnly = false;
+}
+
+void ffDestroyUsersOptions(FFUsersOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffUsersModuleInfo = {
+ .name = FF_USERS_MODULE_NAME,
+ .description = "Print users who are currently logged in",
+ .initOptions = (void*) ffInitUsersOptions,
+ .destroyOptions = (void*) ffDestroyUsersOptions,
+ .parseJsonObject = (void*) ffParseUsersJsonObject,
+ .printModule = (void*) ffPrintUsers,
+ .generateJsonResult = (void*) ffGenerateUsersJsonResult,
+ .generateJsonConfig = (void*) ffGenerateUsersJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "User name", "name" },
+ { "Host name", "host-name" },
+ { "Session name", "session-name" },
+ { "Client IP", "client-ip" },
+ { "Login Time in local timezone", "login-time" },
+ { "Days after login", "days" },
+ { "Hours after login", "hours" },
+ { "Minutes after login", "minutes" },
+ { "Seconds after login", "seconds" },
+ { "Milliseconds after login", "milliseconds" },
+ { "Years integer after login", "years" },
+ { "Days of year after login", "days-of-year" },
+ { "Years fraction after login", "years-fraction" },
+ }))
+};
diff --git a/src/modules/users/users.h b/src/modules/users/users.h
new file mode 100644
index 0000000..44c8b01
--- /dev/null
+++ b/src/modules/users/users.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_USERS_MODULE_NAME "Users"
+
+bool ffPrintUsers(FFUsersOptions* options);
+void ffInitUsersOptions(FFUsersOptions* options);
+void ffDestroyUsersOptions(FFUsersOptions* options);
+
+extern FFModuleBaseInfo ffUsersModuleInfo;
diff --git a/src/modules/version/option.h b/src/modules/version/option.h
new file mode 100644
index 0000000..8647420
--- /dev/null
+++ b/src/modules/version/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFVersionOptions {
+ FFModuleArgs moduleArgs;
+} FFVersionOptions;
+
+static_assert(sizeof(FFVersionOptions) <= FF_OPTION_MAX_SIZE, "FFVersionOptions size exceeds maximum allowed size");
diff --git a/src/modules/version/version.c b/src/modules/version/version.c
new file mode 100644
index 0000000..dcc4bbe
--- /dev/null
+++ b/src/modules/version/version.c
@@ -0,0 +1,117 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/libc/libc.h"
+#include "detection/version/version.h"
+#include "modules/version/version.h"
+
+bool ffPrintVersion(FFVersionOptions* options) {
+ FFVersionResult* result = &ffVersionResult;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_VERSION_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ printf("%s %s%s%s (%s)\n", result->projectName, result->version, result->versionTweak, result->debugMode ? "-debug" : "", result->architecture);
+ } else {
+ FFLibcResult libcResult;
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ if (!ffDetectLibc(&libcResult)) {
+ ffStrbufSetS(&buf, libcResult.name);
+ if (libcResult.version) {
+ ffStrbufAppendC(&buf, ' ');
+ ffStrbufAppendS(&buf, libcResult.version);
+ }
+ }
+
+ const char* buildType = result->debugMode ? "debug" : "release";
+ FF_PRINT_FORMAT_CHECKED(FF_VERSION_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->projectName, "project-name"),
+ FF_ARG(result->version, "version"),
+ FF_ARG(result->versionTweak, "version-tweak"),
+ FF_ARG(buildType, "build-type"),
+ FF_ARG(result->sysName, "sysname"),
+ FF_ARG(result->architecture, "arch"),
+ FF_ARG(result->cmakeBuiltType, "cmake-built-type"),
+ FF_ARG(result->compileTime, "compile-time"),
+ FF_ARG(result->compiler, "compiler"),
+ FF_ARG(buf, "libc"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseVersionJsonObject(FFVersionOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_VERSION_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateVersionJsonConfig(FFVersionOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateVersionJsonResult(FF_A_UNUSED FFVersionOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FFVersionResult* result = &ffVersionResult;
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_str(doc, obj, "projectName", result->projectName);
+ yyjson_mut_obj_add_str(doc, obj, "sysName", result->sysName);
+ yyjson_mut_obj_add_str(doc, obj, "architecture", result->architecture);
+ yyjson_mut_obj_add_str(doc, obj, "version", result->version);
+ yyjson_mut_obj_add_str(doc, obj, "versionGit", result->versionGit);
+ yyjson_mut_obj_add_str(doc, obj, "cmakeBuiltType", result->cmakeBuiltType);
+ yyjson_mut_obj_add_str(doc, obj, "compileTime", result->compileTime);
+ yyjson_mut_obj_add_str(doc, obj, "compiler", result->compiler);
+ yyjson_mut_obj_add_bool(doc, obj, "debugMode", result->debugMode);
+
+ FFLibcResult libcResult;
+ if (ffDetectLibc(&libcResult)) {
+ yyjson_mut_obj_add_null(doc, obj, "libc");
+ } else {
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreateS(libcResult.name);
+ if (libcResult.version) {
+ ffStrbufAppendC(&buf, ' ');
+ ffStrbufAppendS(&buf, libcResult.version);
+ }
+ yyjson_mut_obj_add_strbuf(doc, obj, "libc", &buf);
+ }
+
+ return true;
+}
+
+void ffInitVersionOptions(FFVersionOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyVersionOptions(FFVersionOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffVersionModuleInfo = {
+ .name = FF_VERSION_MODULE_NAME,
+ .description = "Print the Fastfetch version and build information",
+ .initOptions = (void*) ffInitVersionOptions,
+ .destroyOptions = (void*) ffDestroyVersionOptions,
+ .parseJsonObject = (void*) ffParseVersionJsonObject,
+ .printModule = (void*) ffPrintVersion,
+ .generateJsonResult = (void*) ffGenerateVersionJsonResult,
+ .generateJsonConfig = (void*) ffGenerateVersionJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Project name", "project-name" },
+ { "Version", "version" },
+ { "Version tweak", "version-tweak" },
+ { "Build type (debug or release)", "build-type" },
+ { "System name", "sysname" },
+ { "Architecture", "arch" },
+ { "CMake build type when compiling (Debug, Release, RelWithDebInfo, MinSizeRel)", "cmake-built-type" },
+ { "Date time when compiling", "compile-time" },
+ { "Compiler used when compiling", "compiler" },
+ { "Libc used when compiling", "libc" },
+ }))
+};
diff --git a/src/modules/version/version.h b/src/modules/version/version.h
new file mode 100644
index 0000000..add5e11
--- /dev/null
+++ b/src/modules/version/version.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_VERSION_MODULE_NAME "Version"
+
+bool ffPrintVersion(FFVersionOptions* options);
+void ffInitVersionOptions(FFVersionOptions* options);
+void ffDestroyVersionOptions(FFVersionOptions* options);
+
+extern FFModuleBaseInfo ffVersionModuleInfo;
diff --git a/src/modules/vulkan/option.h b/src/modules/vulkan/option.h
new file mode 100644
index 0000000..2ab42c5
--- /dev/null
+++ b/src/modules/vulkan/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFVulkanOptions {
+ FFModuleArgs moduleArgs;
+} FFVulkanOptions;
+
+static_assert(sizeof(FFVulkanOptions) <= FF_OPTION_MAX_SIZE, "FFVulkanOptions size exceeds maximum allowed size");
diff --git a/src/modules/vulkan/vulkan.c b/src/modules/vulkan/vulkan.c
new file mode 100644
index 0000000..9ecbd32
--- /dev/null
+++ b/src/modules/vulkan/vulkan.c
@@ -0,0 +1,149 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/gpu/gpu.h"
+#include "detection/vulkan/vulkan.h"
+#include "modules/vulkan/vulkan.h"
+
+bool ffPrintVulkan(FFVulkanOptions* options) {
+ const FFVulkanResult* vulkan = ffDetectVulkan();
+
+ if (vulkan->error) {
+ ffPrintError(FF_VULKAN_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", vulkan->error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_VULKAN_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ if (vulkan->apiVersion.length == 0 && vulkan->driver.length == 0) {
+ ffStrbufWriteTo(&vulkan->instanceVersion, stdout);
+ puts(" [Software only]");
+ } else {
+ if (vulkan->apiVersion.length > 0) {
+ ffStrbufWriteTo(&vulkan->apiVersion, stdout);
+
+ if (vulkan->driver.length > 0) {
+ fputs(" - ", stdout);
+ }
+ }
+
+ if (vulkan->driver.length > 0) {
+ ffStrbufWriteTo(&vulkan->driver, stdout);
+ }
+
+ putchar('\n');
+ }
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_VULKAN_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(vulkan->driver, "driver"),
+ FF_ARG(vulkan->apiVersion, "api-version"),
+ FF_ARG(vulkan->conformanceVersion, "conformance-version"),
+ FF_ARG(vulkan->instanceVersion, "instance-version"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseVulkanJsonObject(FFVulkanOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_VULKAN_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateVulkanJsonConfig(FFVulkanOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateVulkanJsonResult(FF_A_UNUSED FFVulkanOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFVulkanResult* result = ffDetectVulkan();
+
+ if (result->error) {
+ yyjson_mut_obj_add_str(doc, module, "error", result->error);
+ return false;
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "apiVersion", &result->apiVersion);
+ yyjson_mut_obj_add_strbuf(doc, obj, "conformanceVersion", &result->conformanceVersion);
+ yyjson_mut_obj_add_strbuf(doc, obj, "driver", &result->driver);
+ yyjson_mut_val* gpus = yyjson_mut_obj_add_arr(doc, obj, "gpus");
+ FF_LIST_FOR_EACH (FFGPUResult, vulkanGpu, result->gpus) {
+ yyjson_mut_val* gpuObj = yyjson_mut_arr_add_obj(doc, gpus);
+ yyjson_mut_obj_add_str(doc, gpuObj, "type", vulkanGpu->type == FF_GPU_TYPE_UNKNOWN ? "Unknown" : vulkanGpu->type == FF_GPU_TYPE_INTEGRATED ? "Integrated"
+ : "Discrete");
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "vendor", &vulkanGpu->vendor);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "name", &vulkanGpu->name);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "driver", &vulkanGpu->driver);
+ yyjson_mut_obj_add_strbuf(doc, gpuObj, "platformApi", &vulkanGpu->platformApi);
+ yyjson_mut_obj_add_uint(doc, gpuObj, "deviceId", vulkanGpu->deviceId);
+
+ yyjson_mut_val* memoryObj = yyjson_mut_obj_add_obj(doc, gpuObj, "memory");
+
+ {
+ yyjson_mut_val* dedicatedMemory = yyjson_mut_obj_add_obj(doc, memoryObj, "dedicated");
+ if (vulkanGpu->dedicated.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedMemory, "total", vulkanGpu->dedicated.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedMemory, "total");
+ }
+
+ if (vulkanGpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", vulkanGpu->dedicated.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, dedicatedMemory, "used");
+ }
+ }
+
+ {
+ yyjson_mut_val* sharedMemory = yyjson_mut_obj_add_obj(doc, memoryObj, "shared");
+ if (vulkanGpu->shared.total != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedMemory, "total", vulkanGpu->shared.total);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedMemory, "total");
+ }
+
+ if (vulkanGpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) {
+ yyjson_mut_obj_add_uint(doc, sharedMemory, "used", vulkanGpu->shared.used);
+ } else {
+ yyjson_mut_obj_add_null(doc, sharedMemory, "used");
+ }
+ }
+
+ yyjson_mut_obj_add_uint(doc, gpuObj, "deviceId", vulkanGpu->deviceId);
+ }
+
+ return true;
+}
+
+void ffInitVulkanOptions(FFVulkanOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+}
+
+void ffDestroyVulkanOptions(FFVulkanOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffVulkanModuleInfo = {
+ .name = FF_VULKAN_MODULE_NAME,
+ .description = "Print the highest Vulkan version supported by the GPU",
+ .initOptions = (void*) ffInitVulkanOptions,
+ .destroyOptions = (void*) ffDestroyVulkanOptions,
+ .parseJsonObject = (void*) ffParseVulkanJsonObject,
+ .printModule = (void*) ffPrintVulkan,
+ .generateJsonResult = (void*) ffGenerateVulkanJsonResult,
+ .generateJsonConfig = (void*) ffGenerateVulkanJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Driver name", "driver" },
+ { "API version", "api-version" },
+ { "Conformance version", "conformance-version" },
+ { "Instance version", "instance-version" },
+ }))
+};
diff --git a/src/modules/vulkan/vulkan.h b/src/modules/vulkan/vulkan.h
new file mode 100644
index 0000000..c838028
--- /dev/null
+++ b/src/modules/vulkan/vulkan.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_VULKAN_MODULE_NAME "Vulkan"
+
+bool ffPrintVulkan(FFVulkanOptions* options);
+void ffInitVulkanOptions(FFVulkanOptions* options);
+void ffDestroyVulkanOptions(FFVulkanOptions* options);
+
+extern FFModuleBaseInfo ffVulkanModuleInfo;
diff --git a/src/modules/wallpaper/option.h b/src/modules/wallpaper/option.h
new file mode 100644
index 0000000..8f7008d
--- /dev/null
+++ b/src/modules/wallpaper/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFWallpaperOptions {
+ FFModuleArgs moduleArgs;
+} FFWallpaperOptions;
+
+static_assert(sizeof(FFWallpaperOptions) <= FF_OPTION_MAX_SIZE, "FFWallpaperOptions size exceeds maximum allowed size");
diff --git a/src/modules/wallpaper/wallpaper.c b/src/modules/wallpaper/wallpaper.c
new file mode 100644
index 0000000..a4d17c0
--- /dev/null
+++ b/src/modules/wallpaper/wallpaper.c
@@ -0,0 +1,90 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/wallpaper/wallpaper.h"
+#include "modules/wallpaper/wallpaper.h"
+
+bool ffPrintWallpaper(FFWallpaperOptions* options) {
+ FF_STRBUF_AUTO_DESTROY fullpath = ffStrbufCreate();
+ const char* error = ffDetectWallpaper(&fullpath);
+
+ const uint32_t index = ffStrbufLastIndexC(&fullpath,
+#ifndef _WIN32
+ '/'
+#else
+ '\\'
+#endif
+ ) +
+ 1;
+ const char* filename = index >= fullpath.length
+ ? fullpath.chars
+ : fullpath.chars + index;
+
+ if (error) {
+ ffPrintError(FF_WALLPAPER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_WALLPAPER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ puts(filename);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_WALLPAPER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(filename, "file-name"),
+ FF_ARG(fullpath, "full-path"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseWallpaperJsonObject(FFWallpaperOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_WALLPAPER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateWallpaperJsonConfig(FFWallpaperOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateWallpaperJsonResult(FF_A_UNUSED FFWallpaperOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_STRBUF_AUTO_DESTROY fullpath = ffStrbufCreate();
+ const char* error = ffDetectWallpaper(&fullpath);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+ yyjson_mut_obj_add_strbuf(doc, module, "result", &fullpath);
+
+ return true;
+}
+
+void ffInitWallpaperOptions(FFWallpaperOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰸉");
+}
+
+void ffDestroyWallpaperOptions(FFWallpaperOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffWallpaperModuleInfo = {
+ .name = FF_WALLPAPER_MODULE_NAME,
+ .description = "Print the file path of the current wallpaper",
+ .initOptions = (void*) ffInitWallpaperOptions,
+ .destroyOptions = (void*) ffDestroyWallpaperOptions,
+ .parseJsonObject = (void*) ffParseWallpaperJsonObject,
+ .printModule = (void*) ffPrintWallpaper,
+ .generateJsonResult = (void*) ffGenerateWallpaperJsonResult,
+ .generateJsonConfig = (void*) ffGenerateWallpaperJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "File name", "file-name" },
+ { "Full path", "full-path" },
+ }))
+};
diff --git a/src/modules/wallpaper/wallpaper.h b/src/modules/wallpaper/wallpaper.h
new file mode 100644
index 0000000..c3e7291
--- /dev/null
+++ b/src/modules/wallpaper/wallpaper.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_WALLPAPER_MODULE_NAME "Wallpaper"
+
+bool ffPrintWallpaper(FFWallpaperOptions* options);
+void ffInitWallpaperOptions(FFWallpaperOptions* options);
+void ffDestroyWallpaperOptions(FFWallpaperOptions* options);
+
+extern FFModuleBaseInfo ffWallpaperModuleInfo;
diff --git a/src/modules/weather/option.h b/src/modules/weather/option.h
new file mode 100644
index 0000000..136277f
--- /dev/null
+++ b/src/modules/weather/option.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFWeatherOptions {
+ FFModuleArgs moduleArgs;
+
+ FFstrbuf location;
+ FFstrbuf outputFormat;
+ uint32_t timeout;
+} FFWeatherOptions;
+
+static_assert(sizeof(FFWeatherOptions) <= FF_OPTION_MAX_SIZE, "FFWeatherOptions size exceeds maximum allowed size");
diff --git a/src/modules/weather/weather.c b/src/modules/weather/weather.c
new file mode 100644
index 0000000..6c15692
--- /dev/null
+++ b/src/modules/weather/weather.c
@@ -0,0 +1,105 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/weather/weather.h"
+#include "modules/weather/weather.h"
+
+bool ffPrintWeather(FFWeatherOptions* options) {
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+ const char* error = ffDetectWeather(options, &result);
+
+ if (error) {
+ ffPrintError(FF_WEATHER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_WEATHER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ ffStrbufPutTo(&result, stdout);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_WEATHER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result, "result"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseWeatherJsonObject(FFWeatherOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "location")) {
+ ffStrbufSetJsonVal(&options->location, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "outputFormat")) {
+ ffStrbufSetJsonVal(&options->outputFormat, val);
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "timeout")) {
+ options->timeout = (uint32_t) yyjson_get_uint(val);
+ continue;
+ }
+
+ ffPrintError(FF_WEATHER_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateWeatherJsonConfig(FFWeatherOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "location", &options->location);
+
+ yyjson_mut_obj_add_strbuf(doc, module, "outputFormat", &options->outputFormat);
+
+ yyjson_mut_obj_add_uint(doc, module, "timeout", options->timeout);
+}
+
+bool ffGenerateWeatherJsonResult(FFWeatherOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
+ const char* error = ffDetectWeather(options, &result);
+
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
+
+ return true;
+}
+
+void ffInitWeatherOptions(FFWeatherOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰖙");
+
+ ffStrbufInit(&options->location);
+ ffStrbufInitStatic(&options->outputFormat, "%t+-+%C+(%l)");
+ options->timeout = 0;
+}
+
+void ffDestroyWeatherOptions(FFWeatherOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+
+ ffStrbufDestroy(&options->outputFormat);
+}
+
+FFModuleBaseInfo ffWeatherModuleInfo = {
+ .name = FF_WEATHER_MODULE_NAME,
+ .description = "Print weather information",
+ .initOptions = (void*) ffInitWeatherOptions,
+ .destroyOptions = (void*) ffDestroyWeatherOptions,
+ .parseJsonObject = (void*) ffParseWeatherJsonObject,
+ .printModule = (void*) ffPrintWeather,
+ .generateJsonResult = (void*) ffGenerateWeatherJsonResult,
+ .generateJsonConfig = (void*) ffGenerateWeatherJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Weather result", "result" },
+ }))
+};
diff --git a/src/modules/weather/weather.h b/src/modules/weather/weather.h
new file mode 100644
index 0000000..f28a4c2
--- /dev/null
+++ b/src/modules/weather/weather.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_WEATHER_MODULE_NAME "Weather"
+
+void ffPrepareWeather(FFWeatherOptions* options);
+
+bool ffPrintWeather(FFWeatherOptions* options);
+void ffInitWeatherOptions(FFWeatherOptions* options);
+void ffDestroyWeatherOptions(FFWeatherOptions* options);
+
+extern FFModuleBaseInfo ffWeatherModuleInfo;
diff --git a/src/modules/wifi/option.h b/src/modules/wifi/option.h
new file mode 100644
index 0000000..45ff966
--- /dev/null
+++ b/src/modules/wifi/option.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFWifiOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+} FFWifiOptions;
+
+static_assert(sizeof(FFWifiOptions) <= FF_OPTION_MAX_SIZE, "FFWifiOptions size exceeds maximum allowed size");
diff --git a/src/modules/wifi/wifi.c b/src/modules/wifi/wifi.c
new file mode 100644
index 0000000..42485dc
--- /dev/null
+++ b/src/modules/wifi/wifi.c
@@ -0,0 +1,239 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/wifi/wifi.h"
+#include "modules/wifi/wifi.h"
+
+bool ffPrintWifi(FFWifiOptions* options) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+
+ const char* error = ffDetectWifi(&result);
+ if (error) {
+ ffPrintError(FF_WIFI_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+ if (!result.length) {
+ ffPrintError(FF_WIFI_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No Wifi interfaces found");
+ return false;
+ }
+
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ for (uint32_t index = 0; index < result.length; ++index) {
+ FFWifiResult* item = FF_LIST_GET(FFWifiResult, result, index);
+ uint8_t moduleIndex = result.length == 1 ? 0 : (uint8_t) (index + 1);
+
+ // https://en.wikipedia.org/wiki/List_of_WLAN_channels
+ char bandStr[8];
+ if (item->conn.frequency > 58000) {
+ strcpy(bandStr, "60");
+ } else if (item->conn.frequency > 40000) {
+ strcpy(bandStr, "45");
+ } else if (item->conn.frequency > 5900) {
+ strcpy(bandStr, "6");
+ } else if (item->conn.frequency > 5100) {
+ strcpy(bandStr, "5");
+ } else if (item->conn.frequency > 4900) {
+ strcpy(bandStr, "4.9");
+ } else if (item->conn.frequency > 3600) {
+ strcpy(bandStr, "3.65");
+ } else if (item->conn.frequency > 2000) {
+ strcpy(bandStr, "2.4");
+ } else if (item->conn.frequency > 800) {
+ strcpy(bandStr, "0.9");
+ } else {
+ bandStr[0] = '\0';
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_WIFI_MODULE_NAME, moduleIndex, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ if (item->conn.ssid.length) {
+ if (item->conn.signalQuality != -DBL_MAX) {
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&buffer, item->conn.signalQuality, options->percent, &options->moduleArgs);
+ ffStrbufAppendC(&buffer, ' ');
+ }
+ }
+
+ if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
+ ffStrbufAppend(&buffer, &item->conn.ssid);
+
+ if (item->conn.protocol.length) {
+ ffStrbufAppendS(&buffer, " - ");
+ ffStrbufAppend(&buffer, &item->conn.protocol);
+ }
+ if (bandStr[0]) {
+ ffStrbufAppendF(&buffer, " - %s%sGHz", bandStr, instance.config.display.freqSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_NEVER ? "" : " ");
+ }
+ if (item->conn.security.length) {
+ ffStrbufAppendS(&buffer, " - ");
+ ffStrbufAppend(&buffer, &item->conn.security);
+ }
+ ffStrbufAppendC(&buffer, ' ');
+ }
+
+ if (item->conn.signalQuality != -DBL_MAX) {
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&buffer, item->conn.signalQuality, options->percent, buffer.length > 0, &options->moduleArgs);
+ }
+ }
+
+ ffStrbufTrimRight(&buffer, ' ');
+ } else {
+ ffStrbufAppend(&buffer, &item->inf.status);
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY percentNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&percentNum, item->conn.signalQuality, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY percentBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&percentBar, item->conn.signalQuality, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(FF_WIFI_MODULE_NAME, moduleIndex, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(item->inf.description, "inf-desc"),
+ FF_ARG(item->inf.status, "inf-status"),
+ FF_ARG(item->conn.status, "status"),
+ FF_ARG(item->conn.ssid, "ssid"),
+ FF_ARG(item->conn.bssid, "bssid"),
+ FF_ARG(item->conn.protocol, "protocol"),
+ FF_ARG(percentNum, "signal-quality"),
+ FF_ARG(item->conn.rxRate, "rx-rate"),
+ FF_ARG(item->conn.txRate, "tx-rate"),
+ FF_ARG(item->conn.security, "security"),
+ FF_ARG(percentBar, "signal-quality-bar"),
+ FF_ARG(item->conn.channel, "channel"),
+ FF_ARG(bandStr, "band"),
+ }));
+ }
+ }
+
+ FF_LIST_FOR_EACH (FFWifiResult, item, result) {
+ ffStrbufDestroy(&item->inf.description);
+ ffStrbufDestroy(&item->inf.status);
+ ffStrbufDestroy(&item->conn.status);
+ ffStrbufDestroy(&item->conn.ssid);
+ ffStrbufDestroy(&item->conn.bssid);
+ ffStrbufDestroy(&item->conn.protocol);
+ ffStrbufDestroy(&item->conn.security);
+ }
+
+ return true;
+}
+
+void ffParseWifiJsonObject(FFWifiOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_WIFI_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateWifiJsonConfig(FFWifiOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateWifiJsonResult(FF_A_UNUSED FFWifiOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY result = ffListCreate();
+ const char* error = ffDetectWifi(&result);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+ FF_LIST_FOR_EACH (FFWifiResult, wifi, result) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+
+ yyjson_mut_val* inf = yyjson_mut_obj_add_obj(doc, obj, "inf");
+ yyjson_mut_obj_add_strbuf(doc, inf, "description", &wifi->inf.description);
+ yyjson_mut_obj_add_strbuf(doc, inf, "status", &wifi->inf.status);
+
+ yyjson_mut_val* conn = yyjson_mut_obj_add_obj(doc, obj, "conn");
+ yyjson_mut_obj_add_strbuf(doc, conn, "status", &wifi->conn.status);
+ yyjson_mut_obj_add_strbuf(doc, conn, "ssid", &wifi->conn.ssid);
+ yyjson_mut_obj_add_strbuf(doc, conn, "bssid", &wifi->conn.bssid);
+ yyjson_mut_obj_add_strbuf(doc, conn, "protocol", &wifi->conn.protocol);
+ yyjson_mut_obj_add_strbuf(doc, conn, "security", &wifi->conn.security);
+ if (wifi->conn.signalQuality != -DBL_MAX) {
+ yyjson_mut_obj_add_real(doc, conn, "signalQuality", wifi->conn.signalQuality);
+ } else {
+ yyjson_mut_obj_add_null(doc, conn, "signalQuality");
+ }
+ if (wifi->conn.rxRate != -DBL_MAX) {
+ yyjson_mut_obj_add_real(doc, conn, "rxRate", wifi->conn.rxRate);
+ } else {
+ yyjson_mut_obj_add_null(doc, conn, "rxRate");
+ }
+ if (wifi->conn.txRate != -DBL_MAX) {
+ yyjson_mut_obj_add_real(doc, conn, "txRate", wifi->conn.txRate);
+ } else {
+ yyjson_mut_obj_add_null(doc, conn, "txRate");
+ }
+ yyjson_mut_obj_add_uint(doc, conn, "channel", wifi->conn.channel);
+ yyjson_mut_obj_add_uint(doc, conn, "frequency", wifi->conn.frequency);
+ }
+
+ FF_LIST_FOR_EACH (FFWifiResult, item, result) {
+ ffStrbufDestroy(&item->inf.description);
+ ffStrbufDestroy(&item->inf.status);
+ ffStrbufDestroy(&item->conn.status);
+ ffStrbufDestroy(&item->conn.ssid);
+ ffStrbufDestroy(&item->conn.bssid);
+ ffStrbufDestroy(&item->conn.protocol);
+ ffStrbufDestroy(&item->conn.security);
+ }
+
+ return true;
+}
+
+void ffInitWifiOptions(FFWifiOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+
+ options->percent = (FFPercentageModuleConfig) { 75, 50, 0 };
+}
+
+void ffDestroyWifiOptions(FFWifiOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffWifiModuleInfo = {
+ .name = FF_WIFI_MODULE_NAME,
+ .description = "Print connected Wi-Fi info (SSID, connection and security protocol)",
+ .initOptions = (void*) ffInitWifiOptions,
+ .destroyOptions = (void*) ffDestroyWifiOptions,
+ .parseJsonObject = (void*) ffParseWifiJsonObject,
+ .printModule = (void*) ffPrintWifi,
+ .generateJsonResult = (void*) ffGenerateWifiJsonResult,
+ .generateJsonConfig = (void*) ffGenerateWifiJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Interface description", "inf-desc" },
+ { "Interface status", "inf-status" },
+ { "Connection status", "status" },
+ { "Connection SSID", "ssid" },
+ { "Connection BSSID", "bssid" },
+ { "Connection protocol", "protocol" },
+ { "Connection signal quality (percentage num)", "signal-quality" },
+ { "Connection RX rate", "rx-rate" },
+ { "Connection TX rate", "tx-rate" },
+ { "Connection Security algorithm", "security" },
+ { "Connection signal quality (percentage bar)", "signal-quality-bar" },
+ { "Connection channel number", "channel" },
+ { "Connection channel band in GHz", "band" },
+ }))
+};
diff --git a/src/modules/wifi/wifi.h b/src/modules/wifi/wifi.h
new file mode 100644
index 0000000..506e1e2
--- /dev/null
+++ b/src/modules/wifi/wifi.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_WIFI_MODULE_NAME "Wifi"
+
+bool ffPrintWifi(FFWifiOptions* options);
+void ffInitWifiOptions(FFWifiOptions* options);
+void ffDestroyWifiOptions(FFWifiOptions* options);
+
+extern FFModuleBaseInfo ffWifiModuleInfo;
diff --git a/src/modules/wm/option.h b/src/modules/wm/option.h
new file mode 100644
index 0000000..7d343c9
--- /dev/null
+++ b/src/modules/wm/option.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFWMOptions {
+ FFModuleArgs moduleArgs;
+
+ bool detectPlugin;
+} FFWMOptions;
+
+static_assert(sizeof(FFWMOptions) <= FF_OPTION_MAX_SIZE, "FFWMOptions size exceeds maximum allowed size");
diff --git a/src/modules/wm/wm.c b/src/modules/wm/wm.c
new file mode 100644
index 0000000..5ec6646
--- /dev/null
+++ b/src/modules/wm/wm.c
@@ -0,0 +1,138 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/displayserver/displayserver.h"
+#include "detection/wm/wm.h"
+#include "modules/wm/wm.h"
+
+bool ffPrintWM(FFWMOptions* options) {
+ const FFDisplayServerResult* result = ffConnectDisplayServer();
+
+ if (result->wmPrettyName.length == 0) {
+ ffPrintError(FF_WM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No WM found");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY pluginName = ffStrbufCreate();
+ if (options->detectPlugin) {
+ ffDetectWMPlugin(&pluginName);
+ }
+
+ FF_STRBUF_AUTO_DESTROY version = ffStrbufCreate();
+ if (instance.config.general.detectVersion) {
+ ffDetectWMVersion(&result->wmProcessName, &version, options);
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_WM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+
+ ffStrbufWriteTo(&result->wmPrettyName, stdout);
+
+ if (version.length > 0) {
+ putchar(' ');
+ ffStrbufWriteTo(&version, stdout);
+ }
+
+ if (result->wmProtocolName.length > 0) {
+ fputs(" (", stdout);
+ ffStrbufWriteTo(&result->wmProtocolName, stdout);
+ putchar(')');
+ }
+
+ if (pluginName.length > 0) {
+ fputs(" (with ", stdout);
+ ffStrbufWriteTo(&pluginName, stdout);
+ putchar(')');
+ }
+
+ putchar('\n');
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_WM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(result->wmProcessName, "process-name"),
+ FF_ARG(result->wmPrettyName, "pretty-name"),
+ FF_ARG(result->wmProtocolName, "protocol-name"),
+ FF_ARG(pluginName, "plugin-name"),
+ FF_ARG(version, "version"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseWMJsonObject(FFWMOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "detectPlugin")) {
+ options->detectPlugin = yyjson_get_bool(val);
+ continue;
+ }
+
+ ffPrintError(FF_WM_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateWMJsonConfig(FFWMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ yyjson_mut_obj_add_bool(doc, module, "detectPlugin", options->detectPlugin);
+}
+
+bool ffGenerateWMJsonResult(FF_A_UNUSED FFWMOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ const FFDisplayServerResult* result = ffConnectDisplayServer();
+
+ if (result->wmPrettyName.length == 0) {
+ yyjson_mut_obj_add_str(doc, module, "error", "No WM found");
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY pluginName = ffStrbufCreate();
+ if (options->detectPlugin) {
+ ffDetectWMPlugin(&pluginName);
+ }
+
+ FF_STRBUF_AUTO_DESTROY version = ffStrbufCreate();
+ if (instance.config.general.detectVersion) {
+ ffDetectWMVersion(&result->wmProcessName, &version, options);
+ }
+
+ yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
+ yyjson_mut_obj_add_strbuf(doc, obj, "processName", &result->wmProcessName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "prettyName", &result->wmPrettyName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "protocolName", &result->wmProtocolName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "pluginName", &pluginName);
+ yyjson_mut_obj_add_strbuf(doc, obj, "version", &version);
+
+ return true;
+}
+
+void ffInitWMOptions(FFWMOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "");
+ options->detectPlugin = true;
+}
+
+void ffDestroyWMOptions(FFWMOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffWMModuleInfo = {
+ .name = FF_WM_MODULE_NAME,
+ .description = "Print the window manager name and version",
+ .initOptions = (void*) ffInitWMOptions,
+ .destroyOptions = (void*) ffDestroyWMOptions,
+ .parseJsonObject = (void*) ffParseWMJsonObject,
+ .printModule = (void*) ffPrintWM,
+ .generateJsonResult = (void*) ffGenerateWMJsonResult,
+ .generateJsonConfig = (void*) ffGenerateWMJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "WM process name", "process-name" },
+ { "WM pretty name", "pretty-name" },
+ { "WM protocol name", "protocol-name" },
+ { "WM plugin name", "plugin-name" },
+ { "WM version", "version" },
+ }))
+};
diff --git a/src/modules/wm/wm.h b/src/modules/wm/wm.h
new file mode 100644
index 0000000..4819d73
--- /dev/null
+++ b/src/modules/wm/wm.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_WM_MODULE_NAME "WM"
+
+bool ffPrintWM(FFWMOptions* options);
+void ffInitWMOptions(FFWMOptions* options);
+void ffDestroyWMOptions(FFWMOptions* options);
+
+extern FFModuleBaseInfo ffWMModuleInfo;
diff --git a/src/modules/wmtheme/option.h b/src/modules/wmtheme/option.h
new file mode 100644
index 0000000..5b0af07
--- /dev/null
+++ b/src/modules/wmtheme/option.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/option.h"
+
+typedef struct FFWMThemeOptions {
+ FFModuleArgs moduleArgs;
+} FFWMThemeOptions;
+
+static_assert(sizeof(FFWMThemeOptions) <= FF_OPTION_MAX_SIZE, "FFWMThemeOptions size exceeds maximum allowed size");
diff --git a/src/modules/wmtheme/wmtheme.c b/src/modules/wmtheme/wmtheme.c
new file mode 100644
index 0000000..775e88a
--- /dev/null
+++ b/src/modules/wmtheme/wmtheme.c
@@ -0,0 +1,75 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "detection/wmtheme/wmtheme.h"
+#include "modules/wmtheme/wmtheme.h"
+
+#define FF_WMTHEME_DISPLAY_NAME "WM Theme"
+
+bool ffPrintWMTheme(FFWMThemeOptions* options) {
+ FF_STRBUF_AUTO_DESTROY themeOrError = ffStrbufCreate();
+ if (!ffDetectWmTheme(&themeOrError)) {
+ ffPrintError(FF_WMTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", themeOrError.chars);
+ return false;
+ }
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(FF_WMTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
+ puts(themeOrError.chars);
+ } else {
+ FF_PRINT_FORMAT_CHECKED(FF_WMTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
+ FF_ARG(themeOrError, "result"),
+ }));
+ }
+
+ return true;
+}
+
+void ffParseWMThemeJsonObject(FFWMThemeOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ ffPrintError(FF_WMTHEME_DISPLAY_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateWMThemeJsonConfig(FFWMThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+}
+
+bool ffGenerateWMThemeJsonResult(FF_A_UNUSED FFWMThemeOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_STRBUF_AUTO_DESTROY themeOrError = ffStrbufCreate();
+ if (!ffDetectWmTheme(&themeOrError)) {
+ yyjson_mut_obj_add_strbuf(doc, module, "error", &themeOrError);
+ return false;
+ }
+
+ yyjson_mut_obj_add_strbuf(doc, module, "result", &themeOrError);
+ return true;
+}
+
+void ffInitWMThemeOptions(FFWMThemeOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󰓸");
+}
+
+void ffDestroyWMThemeOptions(FFWMThemeOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffWMThemeModuleInfo = {
+ .name = FF_WMTHEME_MODULE_NAME,
+ .description = "Print the current window manager theme",
+ .initOptions = (void*) ffInitWMThemeOptions,
+ .destroyOptions = (void*) ffDestroyWMThemeOptions,
+ .parseJsonObject = (void*) ffParseWMThemeJsonObject,
+ .printModule = (void*) ffPrintWMTheme,
+ .generateJsonResult = (void*) ffGenerateWMThemeJsonResult,
+ .generateJsonConfig = (void*) ffGenerateWMThemeJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "WM theme", "result" },
+ }))
+};
diff --git a/src/modules/wmtheme/wmtheme.h b/src/modules/wmtheme/wmtheme.h
new file mode 100644
index 0000000..5142ca4
--- /dev/null
+++ b/src/modules/wmtheme/wmtheme.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_WMTHEME_MODULE_NAME "WMTheme"
+
+bool ffPrintWMTheme(FFWMThemeOptions* options);
+void ffInitWMThemeOptions(FFWMThemeOptions* options);
+void ffDestroyWMThemeOptions(FFWMThemeOptions* options);
+
+extern FFModuleBaseInfo ffWMThemeModuleInfo;
diff --git a/src/modules/zpool/option.h b/src/modules/zpool/option.h
new file mode 100644
index 0000000..e4efdcb
--- /dev/null
+++ b/src/modules/zpool/option.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "common/option.h"
+#include "common/percent.h"
+
+typedef struct FFZpoolOptions {
+ FFModuleArgs moduleArgs;
+
+ FFPercentageModuleConfig percent;
+} FFZpoolOptions;
+
+static_assert(sizeof(FFZpoolOptions) <= FF_OPTION_MAX_SIZE, "FFZpoolOptions size exceeds maximum allowed size");
diff --git a/src/modules/zpool/zpool.c b/src/modules/zpool/zpool.c
new file mode 100644
index 0000000..44a6de2
--- /dev/null
+++ b/src/modules/zpool/zpool.c
@@ -0,0 +1,218 @@
+#include "common/printing.h"
+#include "common/jsonconfig.h"
+#include "common/percent.h"
+#include "common/size.h"
+#include "common/FFstrbuf.h"
+#include "common/strutil.h"
+#include "detection/zpool/zpool.h"
+#include "modules/zpool/zpool.h"
+
+static void printZpool(FFZpoolOptions* options, FFZpoolResult* result, uint8_t index) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ if (options->moduleArgs.key.length == 0) {
+ if (result->name.length > 0) {
+ ffStrbufSetF(&buffer, "%s (%s)", FF_ZPOOL_MODULE_NAME, result->name.chars);
+ } else {
+ ffStrbufSetS(&buffer, FF_ZPOOL_MODULE_NAME);
+ }
+ } else {
+ ffStrbufClear(&buffer);
+ FF_PARSE_FORMAT_STRING_CHECKED(&buffer, &options->moduleArgs.key, ((FFformatarg[]) {
+ FF_ARG(index, "index"),
+ FF_ARG(result->name, "name"),
+ FF_ARG(result->guid, "guid"),
+ FF_ARG(options->moduleArgs.keyIcon, "icon"),
+ }));
+ }
+
+ FF_STRBUF_AUTO_DESTROY usedPretty = ffStrbufCreate();
+ ffSizeAppendNum(result->used, &usedPretty);
+
+ FF_STRBUF_AUTO_DESTROY allocatedPretty = ffStrbufCreate();
+ ffSizeAppendNum(result->allocated, &allocatedPretty);
+
+ FF_STRBUF_AUTO_DESTROY totalPretty = ffStrbufCreate();
+ ffSizeAppendNum(result->total, &totalPretty);
+
+ double usedPercentage = result->total > 0 ? (double) result->used / (double) result->total * 100.0 : 0;
+ double allocatedPercentage = result->total > 0 ? (double) result->allocated / (double) result->total * 100.0 : 0;
+ FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
+
+ if (options->moduleArgs.outputFormat.length == 0) {
+ ffPrintLogoAndKey(buffer.chars, index, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY);
+
+ ffStrbufClear(&buffer);
+ ffStrbufSetF(&buffer, "%s / %s (", usedPretty.chars, totalPretty.chars);
+ ffPercentAppendNum(&buffer, usedPercentage, options->percent, false, &options->moduleArgs);
+ ffStrbufAppendS(&buffer, ", ");
+ ffPercentAppendNum(&buffer, allocatedPercentage, options->percent, false, &options->moduleArgs);
+ ffStrbufAppendS(&buffer, " allocated, ");
+ ffPercentAppendNum(&buffer, result->fragmentation, options->percent, false, &options->moduleArgs);
+ ffStrbufAppendF(&buffer, " frag) - %s", result->state.chars);
+ if (result->readOnly) {
+ ffStrbufAppendS(&buffer, " [Read-only]");
+ }
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ FF_STRBUF_AUTO_DESTROY usedPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&usedPercentageNum, usedPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY usedPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&usedPercentageBar, usedPercentage, options->percent, &options->moduleArgs);
+ }
+
+ FF_STRBUF_AUTO_DESTROY allocatedPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&allocatedPercentageNum, allocatedPercentage, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY allocatedPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&allocatedPercentageBar, allocatedPercentage, options->percent, &options->moduleArgs);
+ }
+
+ FF_STRBUF_AUTO_DESTROY fragPercentageNum = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
+ ffPercentAppendNum(&fragPercentageNum, result->fragmentation, options->percent, false, &options->moduleArgs);
+ }
+ FF_STRBUF_AUTO_DESTROY fragPercentageBar = ffStrbufCreate();
+ if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
+ ffPercentAppendBar(&fragPercentageBar, result->fragmentation, options->percent, &options->moduleArgs);
+ }
+
+ FF_PRINT_FORMAT_CHECKED(buffer.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
+ FF_ARG(result->name, "name"),
+ FF_ARG(result->guid, "guid"),
+ FF_ARG(result->state, "state"),
+ FF_ARG(usedPretty, "size-used"),
+ FF_ARG(allocatedPretty, "size-allocated"),
+ FF_ARG(totalPretty, "size-total"),
+ FF_ARG(usedPercentageNum, "used-percentage"),
+ FF_ARG(allocatedPercentageNum, "allocated-percentage"),
+ FF_ARG(fragPercentageNum, "frag-percentage"),
+ FF_ARG(usedPercentageBar, "used-percentage-bar"),
+ FF_ARG(allocatedPercentageBar, "allocated-percentage-bar"),
+ FF_ARG(fragPercentageBar, "frag-percentage-bar"),
+ FF_ARG(result->readOnly, "is-readonly"),
+ }));
+ }
+}
+
+bool ffPrintZpool(FFZpoolOptions* options) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectZpool(&results);
+
+ if (error) {
+ ffPrintError(FF_ZPOOL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
+ return false;
+ }
+ if (results.length == 0) {
+ ffPrintError(FF_ZPOOL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", "No zpool found");
+ return false;
+ }
+
+ for (uint32_t i = 0; i < results.length; i++) {
+ FFZpoolResult* result = FF_LIST_GET(FFZpoolResult, results, i);
+ uint8_t index = results.length == 1 ? 0 : (uint8_t) (i + 1);
+ printZpool(options, result, index);
+ }
+
+ FF_LIST_FOR_EACH (FFZpoolResult, result, results) {
+ ffStrbufDestroy(&result->name);
+ ffStrbufDestroy(&result->state);
+ }
+ return true;
+}
+
+void ffParseZpoolJsonObject(FFZpoolOptions* options, yyjson_val* module) {
+ yyjson_val *key, *val;
+ size_t idx, max;
+ yyjson_obj_foreach (module, idx, max, key, val) {
+ if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
+ continue;
+ }
+
+ if (ffPercentParseJsonObject(key, val, &options->percent)) {
+ continue;
+ }
+
+ ffPrintError(FF_ZPOOL_MODULE_NAME, 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
+ }
+}
+
+void ffGenerateZpoolJsonConfig(FFZpoolOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
+
+ ffPercentGenerateJsonConfig(doc, module, options->percent);
+}
+
+bool ffGenerateZpoolJsonResult(FF_A_UNUSED FFZpoolOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
+ FF_LIST_AUTO_DESTROY results = ffListCreate();
+
+ const char* error = ffDetectZpool(&results);
+ if (error) {
+ yyjson_mut_obj_add_str(doc, module, "error", error);
+ return false;
+ }
+
+ yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
+
+ FF_LIST_FOR_EACH (FFZpoolResult, zpool, results) {
+ yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
+ yyjson_mut_obj_add_strbuf(doc, obj, "name", &zpool->name);
+ yyjson_mut_obj_add_strbuf(doc, obj, "state", &zpool->state);
+ yyjson_mut_obj_add_uint(doc, obj, "guid", zpool->guid);
+ yyjson_mut_obj_add_uint(doc, obj, "used", zpool->used);
+ yyjson_mut_obj_add_uint(doc, obj, "allocated", zpool->allocated);
+ yyjson_mut_obj_add_uint(doc, obj, "total", zpool->total);
+ if (zpool->fragmentation != -DBL_MAX) {
+ yyjson_mut_obj_add_real(doc, obj, "fragmentation", zpool->fragmentation);
+ } else {
+ yyjson_mut_obj_add_null(doc, obj, "fragmentation");
+ }
+ yyjson_mut_obj_add_bool(doc, obj, "readOnly", zpool->readOnly);
+ }
+
+ FF_LIST_FOR_EACH (FFZpoolResult, zpool, results) {
+ ffStrbufDestroy(&zpool->name);
+ ffStrbufDestroy(&zpool->state);
+ }
+ return true;
+}
+
+void ffInitZpoolOptions(FFZpoolOptions* options) {
+ ffOptionInitModuleArg(&options->moduleArgs, "󱑛");
+ options->percent = (FFPercentageModuleConfig) { 50, 80, 0 };
+}
+
+void ffDestroyZpoolOptions(FFZpoolOptions* options) {
+ ffOptionDestroyModuleArg(&options->moduleArgs);
+}
+
+FFModuleBaseInfo ffZpoolModuleInfo = {
+ .name = FF_ZPOOL_MODULE_NAME,
+ .description = "Print ZFS storage pools",
+ .initOptions = (void*) ffInitZpoolOptions,
+ .destroyOptions = (void*) ffDestroyZpoolOptions,
+ .parseJsonObject = (void*) ffParseZpoolJsonObject,
+ .printModule = (void*) ffPrintZpool,
+ .generateJsonResult = (void*) ffGenerateZpoolJsonResult,
+ .generateJsonConfig = (void*) ffGenerateZpoolJsonConfig,
+ .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
+ { "Zpool name", "name" },
+ { "Zpool guid", "guid" },
+ { "Zpool state", "state" },
+ { "Size used", "size-used" },
+ { "Size allocated", "size-allocated" },
+ { "Size total", "size-total" },
+ { "Size used percentage num", "used-percentage" },
+ { "Size allocated percentage num", "allocated-percentage" },
+ { "Fragmentation percentage num", "frag-percentage" },
+ { "Size used percentage bar", "used-percentage-bar" },
+ { "Size allocated percentage bar", "allocated-percentage-bar" },
+ { "Fragmentation percentage bar", "frag-percentage-bar" },
+ { "Is read-only", "is-readonly" },
+ }))
+};
diff --git a/src/modules/zpool/zpool.h b/src/modules/zpool/zpool.h
new file mode 100644
index 0000000..35706bb
--- /dev/null
+++ b/src/modules/zpool/zpool.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "option.h"
+
+#define FF_ZPOOL_MODULE_NAME "Zpool"
+
+bool ffPrintZpool(FFZpoolOptions* options);
+void ffInitZpoolOptions(FFZpoolOptions* options);
+void ffDestroyZpoolOptions(FFZpoolOptions* options);
+
+extern FFModuleBaseInfo ffZpoolModuleInfo;