Review of "Bluetooth Battery Meter" version 45

Details Page Preview

A GNOME extension that displays indicator icons in the system tray, acting as a meter for Bluetooth device battery levels. It also provides detailed battery information via icons and text in the Bluetooth quick settings menu Note: Certain Bluetooth devices do not report battery level until Bluez's experimental features are enabled in system. Check Readme for details. The extension also supports multiple device-specific modes to provide detailed battery reporting and device controls, depending on the capabilities exposed by the device: UPower: Devices such as Logitech Lightspeed keyboards and mice that report battery levels through UPower. GATT Battery Service: Devices that expose one or more battery levels via the standard GATT Battery Service (BAS), such as ZMK-based split keyboards. Socket Mode: Provides multiple battery levels and advanced device controls—such as Active Noise Cancellation (ANC) and other supported features using a custom socket-based protocol. Supported devices: - AirPods - Beats earbuds / headphones - Sony earbuds / headphones - Nothing earbuds / headphones - CMF earbuds / headphones - Samsung Galaxy Buds

Extension Homepage
https://github.com/maniacx/Bluetooth-Battery-Meter

No comments.

Diff Against

Files

Note: Binary files aren't shown on the web site. To see all files, please download the extension zipfile.

Shexli (experimental) warning 4

Shexli found 4 issues that may need reviewer attention.

EGO026 warning

JavaScript files should be reachable from extension.js or prefs.js

Some JavaScript files are not reachable from `extension.js` or `prefs.js` imports.

Don't include unnecessary files

  • script/moreSettings.js

EGO008 warning

extensions using unlock-dialog must document it in disable() comments

Extensions using `unlock-dialog` should document the reason in `disable()` comments.

Session Modes

  • extension.js
    unlock-dialog declared but no nearby disable() comment found

EGO015 warning

signals connected by extension should be disconnected in disable()

Signals assigned in `enable()` are missing matching disconnect calls in `disable()` or its helper methods.

Disconnect all signals

  • preferences/batteryWidgetSettings.js:61
            colorButton.connect(signalEmitted, () => {
                const color = colorButton.get_rgba();
                const hexColor = rgbaToHex(color);
                entry.set_placeholder_text(hexColor);
                const colors = settings.get_strv(colorKey);
                colors[this._idx] = hexColor;
        
  • preferences/batteryWidgetSettings.js:70
            entry.connect('changed', () => {
                const text = entry.get_text().trim();
                if (isValidHexColor(text)) {
                    const colors = settings.get_strv(colorKey);
                    colors[this._idx] = text;
                    settings.set_strv(colorKey, colors);
                 
  • preferences/batteryWidgetSettings.js:80
            settings.connect('changed::level-indicator-custom-colors', () => {
                const colors = settings.get_strv(colorKey);
                const hex = colors[this._idx] || '#000000';
                entry.set_placeholder_text(hex);
                colorButton.set_rgba(hexToRgba(hex));
            })
  • preferences/device.js:100
                button.connect('clicked', () => {
                    popover.hide();
                    const pairedDevice = settings.get_strv('device-list');
                    const existingPathIndex =
                        pairedDevice.findIndex(item => JSON.parse(item).path === pathInfo.path);
                    
  • preferences/device.js:133
            quickSettingsSwitchRow.connect('notify::active', () => {
                const pairedDevice = settings.get_strv('device-list');
                const existingPathIndex =
                    pairedDevice.findIndex(item => JSON.parse(item).path === pathInfo.path);
                if (existingPathIndex !== -1)
  • preferences/device.js:176
            dropDown.connect('notify::selected', () => {
                const index = dropDown.get_selected();
                const selectedId = indicatorOptions[index].id;
                const pairedDevice = settings.get_strv('device-list');
                const existingPathIndex =
                    pairedDevice.fin
  • preferences/device.js:216
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow =
                    new ConfigureWindow(settings, this._macAddress,
                        deviceItem, this._pathInfo, parentWindow);
  • preferences/device.js:232
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('device-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === pathIn
  • preferences/devices/airpods/devicePrefs.js:29
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow = new ConfigureWindow(settings, this._macAddress,
                    pathInfo.path, parentWindow, _, true);
    
                configureW
  • preferences/devices/airpods/devicePrefs.js:45
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('airpods-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === pathI
  • preferences/devices/galaxyBuds/devicePrefs.js:29
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow = new ConfigureWindow(settings, this._macAddress,
                    pathInfo.path, parentWindow, _, true);
    
                configureW
  • preferences/devices/galaxyBuds/devicePrefs.js:46
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('galaxy-buds-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === p
  • preferences/devices/nothingBuds/devicePrefs.js:29
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow = new ConfigureWindow(settings, this._macAddress,
                    pathInfo.path, parentWindow, _, true);
    
                configureW
  • preferences/devices/nothingBuds/devicePrefs.js:46
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('nothing-buds-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === 
  • preferences/devices/sony/devicePrefs.js:29
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow = new ConfigureWindow(settings, this._macAddress,
                    pathInfo.path, parentWindow, _, true);
    
                configureW
  • preferences/devices/sony/devicePrefs.js:45
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('sony-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === pathInfo
  • preferences/gattBas.js:134
                button.connect('clicked', () => {
                    popover.hide();
                    const pairedDevice = settings.get_strv('gattbas-list');
                    const existingPathIndex =
                    pairedDevice.findIndex(item => JSON.parse(item).path === pathInfo.path);
                    con
  • preferences/gattBas.js:177
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow =
                    new ConfigureWindow(settings, this._macAddress, deviceItem,
                        this._pathInfo, parentWindow);
  • preferences/gattBas.js:193
            this._deleteButton.connect('clicked', () => {
                const pairedDevices = settings.get_strv('gattbas-list');
                const existingPathIndex = pairedDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path === pathI
  • preferences/upowerDevices.js:111
            aliasRow.connect('apply', row => {
                const newAlias = row.text.trim();
                const onlineDevice = settings.get_strv('upower-device-list');
                const existingPathIndex =
            onlineDevice.findIndex(item => JSON.parse(item).path === pathInfo.path);
    
                if (e
  • preferences/upowerDevices.js:81
                button.connect('clicked', () => {
                    popover.hide();
                    const onlineDevice = settings.get_strv('upower-device-list');
                    const existingPathIndex =
                        onlineDevice.findIndex(item => JSON.parse(item).path === pathInfo.path);
             
  • preferences/upowerDevices.js:149
            dropDown.connect('notify::selected', () => {
                const index = dropDown.get_selected();
                const selectedId = indicatorOptions[index].id;
    
                const onlineDevice = settings.get_strv('upower-device-list');
                const existingPathIndex =
                    onlineDe
  • preferences/upowerDevices.js:190
            this._customiseButton.connect('clicked', () => {
                const parentWindow = this._customiseButton.get_ancestor(Gtk.Window);
                const configureWindow =
                    new ConfigureWindow(settings, deviceItem, this._pathInfo, parentWindow);
                configureWindow.present()
  • preferences/upowerDevices.js:205
            this._deleteButton.connect('clicked', () => {
                const upowerDevices = settings.get_strv('upower-device-list');
                const existingPathIndex = upowerDevices.findIndex(entry => {
                    const parsedEntry = JSON.parse(entry);
                    return parsedEntry.path ===

EGO033 warning

preferences classes should not retain window-scoped objects on instance fields without close-request cleanup

Preferences code stores window-scoped objects on the exported prefs class without `close-request` cleanup.

Destroy all objects

  • prefs.js:84
    this._sidebarListBox = new Gtk.ListBox()
  • prefs.js:94
    this._contentToastOverlay = new Adw.ToastOverlay()

All Versions

Previous Reviews on this Version

maniacx posted a review
Regarding Shexli warning EGO026: this can be ignored as it is a GJS script that launches a separate configuration window to address the issue of the extension’s preferences window not opening when another extension’s preferences window is already open. EGO008: Comments are added. Just not in disable fn but above enable fn. Will move it to disable on next submission.
dlandau active
Thanks for checking and reacting to the shexli messages!