From a76d920a577bc51bb885487c5d470419b942e436 Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Tue, 5 Dec 2023 11:35:40 -0500
Subject: [PATCH] feat(ags): add bluetooth stuff and some refactor

---
 common/overlays/blueberry/default.nix         |  14 --
 common/overlays/blueberry/wayland.patch       |  95 ---------
 common/overlays/default.nix                   |   1 -
 devices/wim/config/ags/bin/qs-toggles.sh      |  33 ---
 devices/wim/config/ags/bin/startup.sh         |  10 -
 .../wim/config/ags/js/bar/buttons/battery.js  |  79 ++-----
 .../config/ags/js/bar/buttons/bluetooth.js    |  64 ++++++
 .../ags/js/bar/buttons/quick-settings.js      |   4 +-
 devices/wim/config/ags/js/bar/main.js         |  13 +-
 devices/wim/config/ags/js/date.js             |   5 +-
 .../config/ags/js/quick-settings/bluetooth.js | 192 ++++++++++++++++++
 .../ags/js/quick-settings/button-grid.js      |  72 +++----
 .../config/ags/js/quick-settings/network.js   |   9 +-
 devices/wim/config/ags/js/setup.js            |   4 -
 devices/wim/config/ags/scss/widgets/date.scss |   1 -
 .../ags/scss/widgets/quick-settings.scss      |   2 +-
 .../config/ags/scss/widgets/traybuttons.scss  |   4 +-
 devices/wim/config/hypr/main.conf             |   3 +-
 modules/ags/default.nix                       |   1 -
 19 files changed, 328 insertions(+), 278 deletions(-)
 delete mode 100644 common/overlays/blueberry/default.nix
 delete mode 100644 common/overlays/blueberry/wayland.patch
 delete mode 100755 devices/wim/config/ags/bin/qs-toggles.sh
 delete mode 100755 devices/wim/config/ags/bin/startup.sh
 create mode 100644 devices/wim/config/ags/js/bar/buttons/bluetooth.js
 create mode 100644 devices/wim/config/ags/js/quick-settings/bluetooth.js

diff --git a/common/overlays/blueberry/default.nix b/common/overlays/blueberry/default.nix
deleted file mode 100644
index efc81ba7..00000000
--- a/common/overlays/blueberry/default.nix
+++ /dev/null
@@ -1,14 +0,0 @@
-final: prev: {
-  blueberry = prev.blueberry.overrideAttrs (o: {
-    patches =
-      (o.patches or [])
-      ++ [
-        ./wayland.patch
-      ];
-    buildInputs =
-      (o.buildInputs or [])
-      ++ [
-        prev.libappindicator
-      ];
-  });
-}
diff --git a/common/overlays/blueberry/wayland.patch b/common/overlays/blueberry/wayland.patch
deleted file mode 100644
index 54a27cfc..00000000
--- a/common/overlays/blueberry/wayland.patch
+++ /dev/null
@@ -1,95 +0,0 @@
-# https://github.com/linuxmint/blueberry/issues/120
---- /usr/lib/blueberry/blueberry-tray.py.org	2021-12-13 01:02:56.923349069 -0800
-+++ /usr/lib/blueberry/blueberry-tray.py	2021-12-13 02:21:23.253300141 -0800
-@@ -5,8 +5,8 @@
- import gi
- gi.require_version('Gtk', '3.0')
- gi.require_version('GnomeBluetooth', '1.0')
--gi.require_version('XApp', '1.0')
--from gi.repository import Gtk, Gdk, GnomeBluetooth, Gio, XApp
-+gi.require_version('AppIndicator3', '0.1')
-+from gi.repository import AppIndicator3, Gtk, Gdk, GnomeBluetooth, Gio
- import rfkillMagic
- import setproctitle
- import subprocess
-@@ -53,12 +53,16 @@
-         self.model.connect('row-deleted', self.update_icon_callback)
-         self.model.connect('row-inserted', self.update_icon_callback)
-
--        self.icon = XApp.StatusIcon()
--        self.icon.set_name("blueberry")
--        self.icon.set_tooltip_text(_("Bluetooth"))
--        self.icon.connect("activate", self.on_statusicon_activated)
--        self.icon.connect("button-release-event", self.on_statusicon_released)
--
-+        self.paired_devices = {}
-+
-+        self.icon = AppIndicator3.Indicator.new(
-+            'BlueBerry',
-+            'blueberry',
-+            AppIndicator3.IndicatorCategory.SYSTEM_SERVICES
-+        )
-+        self.icon.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
-+        self.icon.set_menu(self.build_menu())
-+
-         self.update_icon_callback(None, None, None)
-
-     def on_settings_changed_cb(self, setting, key, data=None):
-@@ -71,21 +75,23 @@
-             return
-
-         if self.rfkill.hard_block or self.rfkill.soft_block:
--            self.icon.set_icon_name(self.tray_disabled_icon)
--            self.icon.set_tooltip_text(_("Bluetooth is disabled"))
-+            self.icon.set_title(_("Bluetooth is disabled"))
-+            self.icon.set_icon(self.tray_disabled_icon)
-+            self.icon.set_menu(self.build_menu())
-         else:
--            self.icon.set_icon_name(self.tray_icon)
-+            self.icon.set_icon(self.tray_icon)
-+            self.icon.set_menu(self.build_menu())
-             self.update_connected_state()
-
-     def update_connected_state(self):
-         self.get_devices()
-
-         if len(self.connected_devices) > 0:
--            self.icon.set_icon_name(self.tray_active_icon)
--            self.icon.set_tooltip_text(_("Bluetooth: Connected to %s") % (", ".join(self.connected_devices)))
-+            self.icon.set_title(_("Bluetooth: Connected to %s") % (", ".join(self.connected_devices)))
-+            self.icon.set_icon(self.tray_active_icon)
-         else:
--            self.icon.set_icon_name(self.tray_icon)
--            self.icon.set_tooltip_text(_("Bluetooth"))
-+            self.icon.set_title(_("Bluetooth"))
-+            self.icon.set_icon(self.tray_icon)
-
-     def get_devices(self):
-         self.connected_devices = []
-@@ -117,13 +118,11 @@
-
-                 iter = self.model.iter_next(iter)
-
--    def on_statusicon_activated(self, icon, button, time):
--        if button == Gdk.BUTTON_PRIMARY:
--            subprocess.Popen(["blueberry"])
-+    def start_blueberry(self, _ignored):
-+        subprocess.Popen(["blueberry"])
-
--    def on_statusicon_released(self, icon, x, y, button, time, position):
--        if button == 3:
-+    def build_menu(self):
-             menu = Gtk.Menu()
-
-             if not self.rfkill.hard_block:
-                 if self.rfkill.soft_block:
-@@ -168,7 +170,7 @@
-             menu.append(item)
-
-             menu.show_all()
--            icon.popup_menu(menu, x, y, button, time, position)
-+            return menu
-
-     def toggle_connect_cb(self, item, data = None):
-         proxy = self.paired_devices[data]
-
diff --git a/common/overlays/default.nix b/common/overlays/default.nix
index 6ed68ce6..192f23ec 100644
--- a/common/overlays/default.nix
+++ b/common/overlays/default.nix
@@ -9,7 +9,6 @@
   ];
 
   nixpkgs.overlays = [
-    (import ./blueberry)
     (import ./spotifywm)
     (import ./squeekboard)
 
diff --git a/devices/wim/config/ags/bin/qs-toggles.sh b/devices/wim/config/ags/bin/qs-toggles.sh
deleted file mode 100755
index b7be84ca..00000000
--- a/devices/wim/config/ags/bin/qs-toggles.sh
+++ /dev/null
@@ -1,33 +0,0 @@
-#!/usr/bin/env bash
-
-radio_status () {
-  radio_status=$(nmcli radio wifi)
-  if [[ $radio_status == "enabled" ]]; then
-    echo "on"
-  else
-    echo "off"
-  fi
-}
-
-if [[ $1 == "toggle-radio" ]]; then
-  stat=$(radio_status)
-  if [[ $stat == "on" ]]; then
-    nmcli radio wifi off
-  else
-    nmcli radio wifi on
-  fi
-fi
-
-FILE='/home/matt/.config/.bluetooth'
-get_state() {
-  if [[ "$(rfkill list | grep -A 1 hci0 | grep -o no)" == "no" ]]; then
-    echo " 󰂯 " > "$FILE"
-  else
-    echo " 󰂲 " > "$FILE"
-  fi
-}
-
-if [[ "$1" == "blue-toggle" ]]; then
-  rfkill toggle bluetooth
-  get_state
-fi
diff --git a/devices/wim/config/ags/bin/startup.sh b/devices/wim/config/ags/bin/startup.sh
deleted file mode 100755
index af1ede43..00000000
--- a/devices/wim/config/ags/bin/startup.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env bash
-
-## Make bluetooth status persistent between reboots
-if [[ ! -f "$HOME/.config/.bluetooth" ]]; then
-  echo 󰂲 > "$FILE"
-fi
-
-if grep -q 󰂲 "$HOME/.config/.bluetooth"; then
-  rfkill block bluetooth
-fi
diff --git a/devices/wim/config/ags/js/bar/buttons/battery.js b/devices/wim/config/ags/js/bar/buttons/battery.js
index bda8c764..e0f81e6b 100644
--- a/devices/wim/config/ags/js/bar/buttons/battery.js
+++ b/devices/wim/config/ags/js/bar/buttons/battery.js
@@ -1,37 +1,24 @@
 import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
 
-import { Box, EventBox, Icon, Label, Overlay, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
+import { Label, Icon, Box } from 'resource:///com/github/Aylur/ags/widget.js';
 
 import Separator from '../../misc/separator.js';
 
 const LOW_BATT = 20;
 
 
-const NumOverlay = () => Label({
-    className: 'bg-text',
-    hpack: 'center',
-    vpack: 'center',
+const Indicator = () => Icon({
+    className: 'battery-indicator',
+
+    binds: [['icon', Battery, 'icon-name']],
+
     connections: [[Battery, (self) => {
-        self.label = `${Math.floor(Battery.percent / 10)}`;
-        self.visible = !Battery.charging;
+        self.toggleClassName('charging', Battery.charging);
+        self.toggleClassName('charged', Battery.charged);
+        self.toggleClassName('low', Battery.percent < LOW_BATT);
     }]],
 });
 
-const Indicator = (overlay) => Overlay({
-    child: Icon({
-        className: 'battery-indicator',
-
-        binds: [['icon', Battery, 'icon-name']],
-
-        connections: [[Battery, (self) => {
-            self.toggleClassName('charging', Battery.charging);
-            self.toggleClassName('charged', Battery.charged);
-            self.toggleClassName('low', Battery.percent < LOW_BATT);
-        }]],
-    }),
-    overlays: [overlay],
-});
-
 const LevelLabel = (props) => Label({
     ...props,
     className: 'label',
@@ -43,44 +30,12 @@ const LevelLabel = (props) => Label({
 
 const SPACING = 5;
 
-export default () => {
-    const rev1 = NumOverlay();
-    const rev = Revealer({
-        transition: 'slide_right',
-        child: Box({
-            children: [
-                Separator(SPACING),
-                LevelLabel(),
-            ],
-        }),
-    });
+export default () => Box({
+    className: 'toggle-off battery',
 
-    const widget = EventBox({
-        onHover: () => {
-            rev.revealChild = true;
-
-            if (!Battery.charging) {
-                rev1.visible = false;
-            }
-        },
-        onHoverLost: () => {
-            rev.revealChild = false;
-
-            if (!Battery.charging) {
-                rev1.visible = true;
-            }
-        },
-        child: Box({
-            className: 'battery',
-            children: [
-                Indicator(rev1),
-
-                rev,
-            ],
-        }),
-    });
-
-    widget.rev = rev;
-
-    return widget;
-};
+    children: [
+        Indicator(),
+        Separator(SPACING),
+        LevelLabel(),
+    ],
+});
diff --git a/devices/wim/config/ags/js/bar/buttons/bluetooth.js b/devices/wim/config/ags/js/bar/buttons/bluetooth.js
new file mode 100644
index 00000000..c9c19512
--- /dev/null
+++ b/devices/wim/config/ags/js/bar/buttons/bluetooth.js
@@ -0,0 +1,64 @@
+import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
+
+import { Label, Box, EventBox, Icon, Revealer } from 'resource:///com/github/Aylur/ags/widget.js';
+
+import Separator from '../../misc/separator.js';
+
+
+const Indicator = (props) => Icon({
+    ...props,
+    connections: [[Bluetooth, (self) => {
+        if (Bluetooth.enabled) {
+            self.icon = Bluetooth.connectedDevices[0] ?
+                Bluetooth.connectedDevices[0].iconName :
+                'bluetooth-active-symbolic';
+        }
+        else {
+            self.icon = 'bluetooth-disabled-symbolic';
+        }
+    }]],
+});
+
+const ConnectedLabel = (props) => Label({
+    ...props,
+    connections: [[Bluetooth, (self) => {
+        self.label = Bluetooth.connectedDevices[0] ?
+            `${Bluetooth.connectedDevices[0]}` :
+            'Disconnected';
+    }, 'notify::connected-devices']],
+});
+
+const SPACING = 5;
+
+export default () => {
+    const rev = Revealer({
+        transition: 'slide_right',
+        child: Box({
+            children: [
+                Separator(SPACING),
+                ConnectedLabel(),
+            ],
+        }),
+    });
+
+    const widget = EventBox({
+        onHover: () => {
+            rev.revealChild = true;
+        },
+        onHoverLost: () => {
+            rev.revealChild = false;
+        },
+        child: Box({
+            className: 'bluetooth',
+            children: [
+                Indicator(),
+
+                rev,
+            ],
+        }),
+    });
+
+    widget.rev = rev;
+
+    return widget;
+};
diff --git a/devices/wim/config/ags/js/bar/buttons/quick-settings.js b/devices/wim/config/ags/js/bar/buttons/quick-settings.js
index 4b71bdeb..fc558aae 100644
--- a/devices/wim/config/ags/js/bar/buttons/quick-settings.js
+++ b/devices/wim/config/ags/js/bar/buttons/quick-settings.js
@@ -3,7 +3,7 @@ import App from 'resource:///com/github/Aylur/ags/app.js';
 import { Box, Label } from 'resource:///com/github/Aylur/ags/widget.js';
 
 import Audio from './audio.js';
-import Battery from './battery.js';
+import Bluetooth from './bluetooth.js';
 import KeyboardLayout from './keyboard-layout.js';
 import Network from './network.js';
 
@@ -36,7 +36,7 @@ export default () => EventBox({
 
             Audio(),
 
-            Battery(),
+            Bluetooth(),
 
             Network(),
 
diff --git a/devices/wim/config/ags/js/bar/main.js b/devices/wim/config/ags/js/bar/main.js
index aff4c337..e7a277fd 100644
--- a/devices/wim/config/ags/js/bar/main.js
+++ b/devices/wim/config/ags/js/bar/main.js
@@ -2,6 +2,7 @@ import { Window, CenterBox, Box } from 'resource:///com/github/Aylur/ags/widget.
 
 import Separator from '../misc/separator.js';
 
+import Battery from './buttons/battery.js';
 import Brightness from './buttons/brightness.js';
 import Clock from './buttons/clock.js';
 import CurrentWindow from './buttons/current-window.js';
@@ -41,11 +42,11 @@ export default () => Window({
 
                     SysTray(),
 
-                    Brightness(),
+                    Workspaces(),
 
                     Separator(SPACING),
 
-                    Workspaces(),
+                    CurrentWindow(),
 
                 ],
             }),
@@ -54,7 +55,7 @@ export default () => Window({
                 children: [
                     Separator(SPACING),
 
-                    CurrentWindow(),
+                    Clock(),
 
                     Separator(SPACING),
                 ],
@@ -63,7 +64,11 @@ export default () => Window({
             endWidget: Box({
                 hpack: 'end',
                 children: [
-                    Clock(),
+                    Brightness(),
+
+                    Separator(SPACING),
+
+                    Battery(),
 
                     Separator(SPACING),
 
diff --git a/devices/wim/config/ags/js/date.js b/devices/wim/config/ags/js/date.js
index b4774531..82df7413 100644
--- a/devices/wim/config/ags/js/date.js
+++ b/devices/wim/config/ags/js/date.js
@@ -72,11 +72,10 @@ const CalendarWidget = () => Box({
 });
 
 const TOP_MARGIN = 6;
-const RIGHT_MARGIN = 182;
 
 export default () => PopupWindow({
-    anchor: ['top', 'right'],
-    margins: [TOP_MARGIN, RIGHT_MARGIN, 0, 0],
+    anchor: ['top'],
+    margins: [TOP_MARGIN, 0, 0, 0],
     name: 'calendar',
     child: Box({
         className: 'date',
diff --git a/devices/wim/config/ags/js/quick-settings/bluetooth.js b/devices/wim/config/ags/js/quick-settings/bluetooth.js
new file mode 100644
index 00000000..1135532b
--- /dev/null
+++ b/devices/wim/config/ags/js/quick-settings/bluetooth.js
@@ -0,0 +1,192 @@
+import App from 'resource:///com/github/Aylur/ags/app.js';
+import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
+
+import { Box, Icon, Label, ListBox, Overlay, Revealer, Scrollable } from 'resource:///com/github/Aylur/ags/widget.js';
+
+import EventBox from '../misc/cursorbox.js';
+
+const SCROLL_THRESHOLD_H = 200;
+const SCROLL_THRESHOLD_N = 7;
+
+
+const BluetoothDevice = (dev) => {
+    const widget = Box({
+        className: 'menu-item',
+    });
+
+    const child = Box({
+        hexpand: true,
+        children: [
+            Icon({
+                binds: [['icon', dev, 'icon-name']],
+            }),
+
+            Label({
+                binds: [['label', dev, 'name']],
+            }),
+
+            Icon({
+                icon: 'object-select-symbolic',
+                hexpand: true,
+                hpack: 'end',
+                connections: [[dev, (self) => {
+                    self.setCss(`opacity: ${dev.paired ? '1' : '0'};`);
+                }]],
+            }),
+        ],
+    });
+
+    widget.dev = dev;
+    widget.add(Revealer({
+        revealChild: true,
+        transition: 'slide_down',
+        child: EventBox({
+            onPrimaryClickRelease: () => dev.setConnection(true),
+            child,
+        }),
+    }));
+
+    return widget;
+};
+
+export const BluetoothMenu = () => {
+    const DevList = new Map();
+    const topArrow = Revealer({
+        transition: 'slide_down',
+        child: Icon({
+            icon: `${App.configDir }/icons/down-large.svg`,
+            className: 'scrolled-indicator',
+            size: 16,
+            css: '-gtk-icon-transform: rotate(180deg);',
+        }),
+    });
+
+    const bottomArrow = Revealer({
+        transition: 'slide_up',
+        child: Icon({
+            icon: `${App.configDir }/icons/down-large.svg`,
+            className: 'scrolled-indicator',
+            size: 16,
+        }),
+    });
+
+    return Overlay({
+        pass_through: true,
+        overlays: [
+            Box({
+                vpack: 'start',
+                hpack: 'center',
+                css: 'margin-top: 12px',
+                children: [topArrow],
+            }),
+
+            Box({
+                vpack: 'end',
+                hpack: 'center',
+                css: 'margin-bottom: 12px',
+                children: [bottomArrow],
+            }),
+        ],
+
+        child: Box({
+            className: 'menu',
+
+            child: Scrollable({
+                hscroll: 'never',
+                vscroll: 'never',
+
+                connections: [['edge-reached', (_, pos) => {
+                    // Manage scroll indicators
+                    if (pos === 2) {
+                        topArrow.revealChild = false;
+                        bottomArrow.revealChild = true;
+                    }
+                    else if (pos === 3) {
+                        topArrow.revealChild = true;
+                        bottomArrow.revealChild = false;
+                    }
+                }]],
+
+                child: ListBox({
+                    setup: (self) => {
+                        self.set_sort_func((a, b) => {
+                            return b.get_children()[0].dev.paired -
+                                a.get_children()[0].dev.paired;
+                        });
+                    },
+
+                    connections: [[Bluetooth, (box) => {
+                        // Get all devices
+                        const Devices = [].concat(
+                            Bluetooth.devices,
+                            Bluetooth.connectedDevices,
+                        );
+
+                        // Add missing devices
+                        Devices.forEach((dev) => {
+                            if (!DevList.has(dev) && dev.name) {
+                                DevList.set(dev, BluetoothDevice(dev));
+
+                                box.add(DevList.get(dev));
+                                box.show_all();
+                            }
+                        });
+
+                        // Delete ones that don't exist anymore
+                        const difference = Array.from(DevList.keys())
+                            .filter((dev) => !Devices
+                                .find((d) => dev === d) &&
+                                    dev.name);
+
+                        difference.forEach((dev) => {
+                            const devWidget = DevList.get(dev);
+
+                            if (devWidget) {
+                                if (devWidget.toDestroy) {
+                                    devWidget.get_parent().destroy();
+                                    DevList.delete(dev);
+                                }
+                                else {
+                                    devWidget.children[0].revealChild = false;
+                                    devWidget.toDestroy = true;
+                                }
+                            }
+                        });
+
+                        // Start scrolling after a specified height
+                        // is reached by the children
+                        const height = Math.max(
+                            box.get_parent().get_allocated_height(),
+                            SCROLL_THRESHOLD_H,
+                        );
+
+                        const scroll = box.get_parent().get_parent();
+
+                        if (box.get_children().length > SCROLL_THRESHOLD_N) {
+                            scroll.vscroll = 'always';
+                            scroll.setCss(`min-height: ${height}px;`);
+
+                            // Make bottom scroll indicator appear only
+                            // when first getting overflowing children
+                            if (!(bottomArrow.revealChild === true ||
+                                topArrow.revealChild === true)) {
+                                bottomArrow.revealChild = true;
+                            }
+                        }
+                        else {
+                            scroll.vscroll = 'never';
+                            scroll.setCss('');
+                            topArrow.revealChild = false;
+                            bottomArrow.revealChild = false;
+                        }
+
+                        // Trigger sort_func
+                        box.get_children().forEach((ch) => {
+                            ch.changed();
+                        });
+                    }]],
+                }),
+            }),
+        }),
+    });
+};
diff --git a/devices/wim/config/ags/js/quick-settings/button-grid.js b/devices/wim/config/ags/js/quick-settings/button-grid.js
index 1c6a5c48..40d2120b 100644
--- a/devices/wim/config/ags/js/quick-settings/button-grid.js
+++ b/devices/wim/config/ags/js/quick-settings/button-grid.js
@@ -11,6 +11,7 @@ import EventBox from '../misc/cursorbox.js';
 import Separator from '../misc/separator.js';
 
 import { NetworkMenu } from './network.js';
+import { BluetoothMenu } from './bluetooth.js';
 
 const SPACING = 28;
 
@@ -19,6 +20,7 @@ const ButtonStates = [];
 const GridButton = ({
     command = () => { /**/ },
     secondaryCommand = () => { /**/ },
+    onOpen = () => { /**/ },
     icon,
     indicator,
     menu,
@@ -129,6 +131,7 @@ const GridButton = ({
 
                                 if (Activated.value) {
                                     deg = menu ? 360 : 450;
+                                    onOpen(menu);
                                 }
                                 self.setCss(`
                                     -gtk-icon-transform: rotate(${deg}deg);
@@ -156,7 +159,7 @@ const Row = ({ buttons } = {}) => {
                 hpack: 'center',
             }),
 
-            Box(),
+            Box({ vertical: true }),
         ],
     });
 
@@ -180,8 +183,7 @@ const FirstRow = () => Row({
             command: () => Network.toggleWifi(),
 
             secondaryCommand: () => {
-                execAsync(['bash', '-c', 'nm-connection-editor'])
-                    .catch(print);
+                // TODO: connection editor
             },
 
             icon: [Network, (icon) => {
@@ -193,62 +195,52 @@ const FirstRow = () => Row({
             }],
 
             menu: NetworkMenu(),
+            onOpen: () => Network.wifi.scan(),
         }),
 
+        // TODO: do vpn
         GridButton({
             command: () => {
-                execAsync(['bash', '-c', '$AGS_PATH/qs-toggles.sh blue-toggle'])
-                    .catch(print);
+                //
             },
 
             secondaryCommand: () => {
-                execAsync(['bash', '-c', 'blueberry'])
-                    .catch(print);
+                //
+            },
+
+            icon: 'airplane-mode-disabled-symbolic',
+        }),
+
+        GridButton({
+            command: () => Bluetooth.toggle(),
+
+            secondaryCommand: () => {
+                // TODO: bluetooth connection editor
             },
 
             icon: [Bluetooth, (self) => {
                 if (Bluetooth.enabled) {
-                    self.icon = 'bluetooth-active-symbolic';
-                    execAsync(['bash', '-c',
-                        'echo 󰂯 > $HOME/.config/.bluetooth']).catch(print);
+                    self.icon = Bluetooth.connectedDevices[0] ?
+                        Bluetooth.connectedDevices[0].iconName :
+                        'bluetooth-active-symbolic';
                 }
                 else {
                     self.icon = 'bluetooth-disabled-symbolic';
-                    execAsync(['bash', '-c',
-                        'echo 󰂲 > $HOME/.config/.bluetooth']).catch(print);
                 }
-            }, 'changed'],
+            }],
 
             indicator: [Bluetooth, (self) => {
-                if (Bluetooth.connectedDevices[0]) {
-                    self.label = String(Bluetooth.connectedDevices[0]);
-                }
-                else {
-                    self.label = 'Disconnected';
-                }
-            }, 'changed'],
-        }),
+                self.label = Bluetooth.connectedDevices[0] ?
+                    `${Bluetooth.connectedDevices[0]}` :
+                    'Disconnected';
+            }, 'notify::connected-devices'],
 
-        // TODO: replace with vpn
-        GridButton({
-            command: () => {
-                execAsync(['bash', '-c',
-                    '$AGS_PATH/qs-toggles.sh toggle-radio']).catch(print);
+            menu: BluetoothMenu(),
+            onOpen: (menu) => {
+                execAsync(`bluetoothctl scan ${menu.revealChild ?
+                    'on' :
+                    'off'}`);
             },
-
-            secondaryCommand: () => {
-                execAsync(['notify-send', 'set this up moron'])
-                    .catch(print);
-            },
-
-            icon: [Network, (self) => {
-                if (Network.wifi.enabled) {
-                    self.icon = 'airplane-mode-disabled-symbolic';
-                }
-                else {
-                    self.icon = 'airplane-mode-symbolic';
-                }
-            }, 'changed'],
         }),
 
     ],
diff --git a/devices/wim/config/ags/js/quick-settings/network.js b/devices/wim/config/ags/js/quick-settings/network.js
index 9dc0da95..b6e5fc44 100644
--- a/devices/wim/config/ags/js/quick-settings/network.js
+++ b/devices/wim/config/ags/js/quick-settings/network.js
@@ -13,7 +13,7 @@ const SCROLL_THRESHOLD_N = 7;
 
 const AccessPoint = (ap) => {
     const widget = Box({
-        className: 'ap',
+        className: 'menu-item',
     });
 
     widget.ap = Variable(ap);
@@ -38,7 +38,11 @@ const AccessPoint = (ap) => {
                 hexpand: true,
                 hpack: 'end',
                 connections: [[Network, (self) => {
-                    self.visible = widget.ap.value.ssid === Network.wifi.ssid;
+                    self.setCss(`opacity: ${
+                        widget.ap.value.ssid === Network.wifi.ssid ?
+                            '1' :
+                            '0'
+                    };`);
                 }]],
             }),
         ],
@@ -55,7 +59,6 @@ const AccessPoint = (ap) => {
             child,
         }),
     }));
-    widget.show_all();
 
     return widget;
 };
diff --git a/devices/wim/config/ags/js/setup.js b/devices/wim/config/ags/js/setup.js
index 50987e50..48f3249d 100644
--- a/devices/wim/config/ags/js/setup.js
+++ b/devices/wim/config/ags/js/setup.js
@@ -1,8 +1,6 @@
 import App from 'resource:///com/github/Aylur/ags/app.js';
 import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
 
-import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
-
 import Brightness from '../services/brightness.js';
 import Pointers from '../services/pointers.js';
 import Tablet from '../services/tablet.js';
@@ -16,8 +14,6 @@ export default () => {
     globalThis.Tablet = Tablet;
     globalThis.closeAll = closeAll;
 
-    execAsync(['bash', '-c', '$AGS_PATH/startup.sh']).catch(print);
-
     TouchGestures.addGesture({
         name: 'openAppLauncher',
         gesture: 'UD',
diff --git a/devices/wim/config/ags/scss/widgets/date.scss b/devices/wim/config/ags/scss/widgets/date.scss
index 050b15ad..4cf7c1e4 100644
--- a/devices/wim/config/ags/scss/widgets/date.scss
+++ b/devices/wim/config/ags/scss/widgets/date.scss
@@ -2,7 +2,6 @@
   background-color: $bg;
   color: $fg;
   border-radius: 30px;
-  border-top-right-radius: 0;
   border: 2px solid $contrast-bg;
 }
 
diff --git a/devices/wim/config/ags/scss/widgets/quick-settings.scss b/devices/wim/config/ags/scss/widgets/quick-settings.scss
index 468623ae..40b9d7a9 100644
--- a/devices/wim/config/ags/scss/widgets/quick-settings.scss
+++ b/devices/wim/config/ags/scss/widgets/quick-settings.scss
@@ -40,7 +40,7 @@
     margin: 0;
   }
 
-  .ap {
+  .menu-item {
     margin: 5px;
 
     label {
diff --git a/devices/wim/config/ags/scss/widgets/traybuttons.scss b/devices/wim/config/ags/scss/widgets/traybuttons.scss
index d8efa148..6ea5bed5 100644
--- a/devices/wim/config/ags/scss/widgets/traybuttons.scss
+++ b/devices/wim/config/ags/scss/widgets/traybuttons.scss
@@ -53,7 +53,8 @@
   padding: 0 15px;
 }
 
-.audio {
+.audio,
+.bluetooth {
   padding: 0 10px;
   font-size: 20px;
   margin-right: -10px;
@@ -72,7 +73,6 @@
 .battery {
   padding: 0 10px;
   font-size: 20px;
-  margin-right: -10px;
 
   .battery-indicator {
     &.charging {
diff --git a/devices/wim/config/hypr/main.conf b/devices/wim/config/hypr/main.conf
index 923e1a39..4dc64a22 100644
--- a/devices/wim/config/hypr/main.conf
+++ b/devices/wim/config/hypr/main.conf
@@ -16,8 +16,7 @@ plugin {
 }
 
 # Autostart programs
-exec-once = sleep 4; blueberry-tray
-exec-once = sleep 6; nextcloud --background
+exec-once = sleep 3; nextcloud --background
 exec-once = squeekboard
 exec-once = ags
 exec-once = sleep 3; ags -t applauncher
diff --git a/modules/ags/default.nix b/modules/ags/default.nix
index e3ca9101..34ea7b7c 100644
--- a/modules/ags/default.nix
+++ b/modules/ags/default.nix
@@ -42,7 +42,6 @@ in {
           lisgd
           squeekboard
           ydotool
-          blueberry
         ]));
     })
   ];