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};