import Gtk from "gi://Gtk?version=3.0" import Gio from "gi://Gio" import AstalBluetooth from "gi://AstalBluetooth" import { bind, Variable } from "astal" import { ArrowToggleButton, Menu } from "./ToggleButton" const bt = AstalBluetooth.get_default() function power(on: boolean) { if (bt.adapter) bt.adapter.powered = on } function setDiscovery(on: boolean) { if (!bt.adapter) return try { if (on) bt.adapter.start_discovery() else bt.adapter.stop_discovery() } catch { // expected when already in the requested state: bluez stops // discovery itself on connect, auto-starts it, etc. } } // bluez only registers devices that are paired or seen during discovery; // the list is mirrored into a Variable refreshed on device add/remove const devices = Variable(bt.get_devices()) const refreshDevices = () => devices.set(bt.get_devices()) bt.connect("notify::devices", refreshDevices) bt.connect("device_added", refreshDevices) bt.connect("device_removed", refreshDevices) const btIcon = bind(bt, "is_powered").as((p) => p ? "bluetooth-active-symbolic" : "bluetooth-disabled-symbolic", ) const connectedCount = devices().as( (ds) => ds.filter((d) => d.connected).length, ) const pillLabel = Variable.derive( [bind(bt, "is_powered"), devices], (powered, ds) => { if (!powered) return "Disabled" const connected = ds.filter((d) => d.connected) if (connected.length === 1) return connected[0].alias return `${connected.length} Connected` }, ) // ported from AlfheimOS: icon, name, battery, busy icon while connecting, // whole row toggles the bluez connected state function DeviceItem(device: AstalBluetooth.Device) { const battery = bind(device, "battery_percentage") const connecting = bind(device, "connecting") const connected = Variable.derive( [bind(device, "connected"), connecting], (c, busy) => c && !busy, ) // bluez reports br-connection-busy when attempts pile up, so debounce let lastAttempt = 0 function toggle() { if (connecting.get()) return // skip retries within the debounce window const now = Date.now() if (now - lastAttempt < 1500) return lastAttempt = now const state = !device.connected // bluez rejects connects while the controller is busy scanning if (state) setDiscovery(false) // vala async: begin with a callback, finish() throws const done = (_source: AstalBluetooth.Device, res: Gio.AsyncResult) => { try { if (state) device.connect_device_finish(res) else device.disconnect_device_finish(res) } catch (e) { console.error(e) } } if (state) device.connect_device(done) else device.disconnect_device(done) } return ( ) } export function BluetoothSection({ open }: { open: Variable }) { if (!bt.adapter) return null const expanded = Variable(false) open.subscribe((v) => { if (!v) { expanded.set(false) setDiscovery(false) } }) const toggle = ( { if (bt.is_powered) { power(false) expanded.set(false) setDiscovery(false) } else { power(true) } }} onArrowClick={() => { const next = !expanded.get() expanded.set(next) if (next) { if (!bt.is_powered) power(true) setDiscovery(true) } else { setDiscovery(false) } }} /> ) const menu = ( ) return { toggle, menu } } // bar cluster indicator: shown while bluetooth is powered, with a count // badge when devices are connected export const bluetoothIndicator: JSX.Element | null = bt.adapter ? ( ) : null