master ohmu/fastfetch-openbsd / src / modules / bluetooth / bluetooth.c
ohmu  ·  2026-10-15
  1#include "common/percent.h"
  2#include "common/printing.h"
  3#include "common/jsonconfig.h"
  4#include "detection/bluetooth/bluetooth.h"
  5#include "modules/bluetooth/bluetooth.h"
  6
  7static void printDevice(FFBluetoothOptions* options, const FFBluetoothResult* device, uint8_t index) {
  8    FFPercentageTypeFlags percentType = options->percent.type == 0 ? instance.config.display.percentType : options->percent.type;
  9    if (options->moduleArgs.outputFormat.length == 0) {
 10        ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
 11
 12        FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
 13        bool showBatteryLevel = device->battery > 0 && device->battery <= 100;
 14
 15        if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_BAR_BIT)) {
 16            ffPercentAppendBar(&buffer, device->battery, options->percent, &options->moduleArgs);
 17            ffStrbufAppendC(&buffer, ' ');
 18        }
 19
 20        if (!(percentType & FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT)) {
 21            ffStrbufAppend(&buffer, &device->name);
 22        }
 23
 24        if (showBatteryLevel && (percentType & FF_PERCENTAGE_TYPE_NUM_BIT)) {
 25            if (buffer.length) {
 26                ffStrbufAppendC(&buffer, ' ');
 27            }
 28            ffPercentAppendNum(&buffer, device->battery, options->percent, buffer.length > 0, &options->moduleArgs);
 29        }
 30
 31        if (!device->connected) {
 32            ffStrbufAppendS(&buffer, " [disconnected]");
 33        }
 34
 35        ffStrbufPutTo(&buffer, stdout);
 36    } else {
 37        // The battery is `uint8_t` with 0 meaning "unknown", so the format string can only be filled
 38        // unconditionally; the signal quality has a sentinel of its own and is left empty instead.
 39        FF_STRBUF_AUTO_DESTROY batteryNum = ffStrbufCreate();
 40        if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
 41            ffPercentAppendNum(&batteryNum, device->battery, options->percent, false, &options->moduleArgs);
 42        }
 43        FF_STRBUF_AUTO_DESTROY batteryBar = ffStrbufCreate();
 44        if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
 45            ffPercentAppendBar(&batteryBar, device->battery, options->percent, &options->moduleArgs);
 46        }
 47
 48        FF_STRBUF_AUTO_DESTROY signalNum = ffStrbufCreate();
 49        FF_STRBUF_AUTO_DESTROY signalBar = ffStrbufCreate();
 50        if (device->signalQuality != -DBL_MAX) {
 51            if (percentType & FF_PERCENTAGE_TYPE_NUM_BIT) {
 52                ffPercentAppendNum(&signalNum, device->signalQuality, options->percent, false, &options->moduleArgs);
 53            }
 54            if (percentType & FF_PERCENTAGE_TYPE_BAR_BIT) {
 55                ffPercentAppendBar(&signalBar, device->signalQuality, options->percent, &options->moduleArgs);
 56            }
 57        }
 58
 59        FF_LIST_AUTO_DESTROY deviceTypes = ffListCreate();
 60        if (device->deviceType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) {
 61            ffStrbufInitStatic(FF_LIST_ADD(FFstrbuf, deviceTypes), "Classic");
 62        }
 63        if (device->deviceType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) {
 64            ffStrbufInitStatic(FF_LIST_ADD(FFstrbuf, deviceTypes), "Low Energy");
 65        }
 66
 67        FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
 68                                                                                                                  FF_ARG(device->name, "name"),
 69                                                                                                                  FF_ARG(device->address, "address"),
 70                                                                                                                  FF_ARG(device->type, "type"),
 71                                                                                                                  FF_ARG(deviceTypes, "device-type"),
 72                                                                                                                  FF_ARG(batteryNum, "battery-percentage"),
 73                                                                                                                  FF_ARG(batteryBar, "battery-percentage-bar"),
 74                                                                                                                  FF_ARG(signalNum, "signal-quality"),
 75                                                                                                                  FF_ARG(signalBar, "signal-quality-bar"),
 76                                                                                                                  FF_ARG(device->connected, "connected"),
 77                                                                                                              }));
 78    }
 79}
 80
 81bool ffPrintBluetooth(FFBluetoothOptions* options) {
 82    FF_LIST_AUTO_DESTROY devices = ffListCreate();
 83    const char* error = ffDetectBluetooth(options, &devices);
 84
 85    if (error) {
 86        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
 87        return false;
 88    }
 89
 90    if (devices.length == 0) {
 91        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "No bluetooth devices found");
 92        return false;
 93    }
 94
 95    uint8_t i = 1;
 96    FF_LIST_FOR_EACH (FFBluetoothResult, device, devices) {
 97        printDevice(options, device, devices.length == 1 ? 0 : i);
 98        ++i;
 99    }
100
101    FF_LIST_FOR_EACH (FFBluetoothResult, device, devices) {
102        ffStrbufDestroy(&device->name);
103        ffStrbufDestroy(&device->type);
104        ffStrbufDestroy(&device->address);
105    }
106    return true;
107}
108
109void ffParseBluetoothJsonObject(FFBluetoothOptions* options, yyjson_val* module) {
110    yyjson_val *key, *val;
111    size_t idx, max;
112    yyjson_obj_foreach (module, idx, max, key, val) {
113        if (ffJsonConfigParseModuleArgs(key, val, &options->moduleArgs)) {
114            continue;
115        }
116
117        if (unsafe_yyjson_equals_str(key, "showDisconnected")) {
118            options->showDisconnected = yyjson_get_bool(val);
119            continue;
120        }
121
122        if (unsafe_yyjson_equals_str(key, "showType")) {
123            int value;
124            const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) {
125                                                                       { "classic", FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT },
126                                                                       { "le", FF_BLUETOOTH_DEVICE_TYPE_LE_BIT },
127                                                                       { "both", FF_BLUETOOTH_DEVICE_TYPE_BOTH },
128                                                                       {},
129                                                                   });
130            if (error) {
131                ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error);
132            } else {
133                options->showType = (FFBluetoothDeviceType) value;
134            }
135            continue;
136        }
137
138        if (ffPercentParseJsonObject(key, val, &options->percent)) {
139            continue;
140        }
141
142        ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Bluetooth), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown JSON key %s", unsafe_yyjson_get_str(key));
143    }
144}
145
146[[gnu::cold]]
147void ffGenerateBluetoothJsonConfig(FFBluetoothOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
148    ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
149
150    yyjson_mut_obj_add_bool(doc, module, "showDisconnected", options->showDisconnected);
151
152    switch (options->showType) {
153        case FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT:
154            yyjson_mut_obj_add_str(doc, module, "showType", "classic");
155            break;
156        case FF_BLUETOOTH_DEVICE_TYPE_LE_BIT:
157            yyjson_mut_obj_add_str(doc, module, "showType", "le");
158            break;
159        case FF_BLUETOOTH_DEVICE_TYPE_BOTH:
160            yyjson_mut_obj_add_str(doc, module, "showType", "both");
161            break;
162        case FF_BLUETOOTH_DEVICE_TYPE_NONE:
163        default:
164            break;
165    }
166
167    ffPercentGenerateJsonConfig(doc, module, options->percent);
168}
169
170bool ffGenerateBluetoothJsonResult(FFBluetoothOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
171    FF_LIST_AUTO_DESTROY results = ffListCreate();
172
173    const char* error = ffDetectBluetooth(options, &results);
174    if (error) {
175        yyjson_mut_obj_add_str(doc, module, "error", error);
176        return false;
177    }
178
179    yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
180
181    FF_LIST_FOR_EACH (FFBluetoothResult, item, results) {
182        yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
183        yyjson_mut_obj_add_strbuf(doc, obj, "address", &item->address);
184        yyjson_mut_obj_add_uint(doc, obj, "battery", item->battery);
185        yyjson_mut_obj_add_bool(doc, obj, "connected", item->connected);
186
187        yyjson_mut_val* deviceTypes = yyjson_mut_obj_add_arr(doc, obj, "deviceType");
188        if (item->deviceType & FF_BLUETOOTH_DEVICE_TYPE_CLASSIC_BIT) {
189            yyjson_mut_arr_add_str(doc, deviceTypes, "Classic");
190        }
191        if (item->deviceType & FF_BLUETOOTH_DEVICE_TYPE_LE_BIT) {
192            yyjson_mut_arr_add_str(doc, deviceTypes, "Low Energy");
193        }
194
195        yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
196        if (item->signalQuality != -DBL_MAX) {
197            yyjson_mut_obj_add_real(doc, obj, "signalQuality", item->signalQuality);
198        } else {
199            yyjson_mut_obj_add_null(doc, obj, "signalQuality");
200        }
201        yyjson_mut_obj_add_strbuf(doc, obj, "type", &item->type);
202    }
203
204    FF_LIST_FOR_EACH (FFBluetoothResult, device, results) {
205        ffStrbufDestroy(&device->name);
206        ffStrbufDestroy(&device->type);
207        ffStrbufDestroy(&device->address);
208    }
209    return true;
210}
211
212void ffInitBluetoothOptions(FFBluetoothOptions* options) {
213    ffOptionInitModuleArg(&options->moduleArgs, "");
214    options->showDisconnected = false;
215    // Both stacks are walked by default; `showType` exists to switch one off, not to switch one on.
216    options->showType = FF_BLUETOOTH_DEVICE_TYPE_BOTH;
217    options->percent = (FFPercentageModuleConfig) { 50, 20, 0 };
218}
219
220void ffDestroyBluetoothOptions(FFBluetoothOptions* options) {
221    ffOptionDestroyModuleArg(&options->moduleArgs);
222}
223
224FFModuleBaseInfo ffBluetoothModuleInfo = {
225    .name = "Bluetooth",
226    .description = "List connected Bluetooth devices",
227    .displayName = {
228        .en = "Bluetooth",
229        .ar = "بلوتوث",
230        .cs = "Bluetooth",
231        .de = "Bluetooth",
232        .es = "Bluetooth",
233        .fi = "Bluetooth",
234        .fr = "Bluetooth",
235        .gl = "Bluetooth",
236        .he = "בלוטות'",
237        .id = "Bluetooth",
238        .it = "Bluetooth",
239        .ja = "ブルートゥース",
240        .ko = "블루투스",
241        .pl = "Bluetooth",
242        .pt = "Bluetooth",
243        .ru = "Bluetooth",
244        .tr = "Bluetooth",
245        .uk = "Bluetooth",
246        .vi = "Bluetooth",
247        .zh_CN = "蓝牙设备",
248        .zh_TW = "藍牙裝置",
249    },
250    .initOptions = (void*) ffInitBluetoothOptions,
251    .destroyOptions = (void*) ffDestroyBluetoothOptions,
252    .parseJsonObject = (void*) ffParseBluetoothJsonObject,
253    .printModule = (void*) ffPrintBluetooth,
254    .generateJsonResult = (void*) ffGenerateBluetoothJsonResult,
255    .generateJsonConfig = (void*) ffGenerateBluetoothJsonConfig,
256    .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
257        { "Name", "name" },
258        { "Address", "address" },
259        { "Type", "type" },
260        { "Bluetooth stacks the device answers on", "device-type" },
261        { "Battery percentage number", "battery-percentage" },
262        { "Battery percentage bar", "battery-percentage-bar" },
263        { "Signal quality number", "signal-quality" },
264        { "Signal quality bar", "signal-quality-bar" },
265        { "Is connected", "connected" },
266    })),
267    .defaultOrder = 59,
268};