-------------------------------------- -- This is the bluetooth controller -- -------------------------------------- -- Awesome Libs local abutton = require("awful.button") local aspawn = require("awful.spawn") local base = require("wibox.widget.base") local dbus_proxy = require("src.lib.lua-dbus_proxy.src.dbus_proxy") local dpi = require("beautiful").xresources.apply_dpi local gcolor = require("gears").color local gfilesystem = require("gears").filesystem local gshape = require("gears").shape local gtable = require("gears").table local gtimer = require("gears.timer") local lgi = require("lgi") local naughty = require("naughty") local wibox = require("wibox") -- Third party libs local rubato = require("src.lib.rubato") -- Own libs local bt_device = require("src.modules.bluetooth.device") local dnd_widget = require("awful.widget.toggle_widget") local icondir = gfilesystem.get_configuration_dir() .. "src/assets/icons/bluetooth/" local capi = { awesome = awesome, mouse = mouse, mousegrabber = mousegrabber, } local bluetooth = { mt = {} } --#region wibox.widget.base boilerplate function bluetooth:layout(_, width, height) if self._private.widget then return { base.place_widget_at(self._private.widget, 0, 0, width, height) } end end function bluetooth:fit(context, width, height) local w, h = 0, 0 if self._private.widget then w, h = base.fit_widget(self, context, self._private.widget, width, height) end return w, h end bluetooth.set_widget = base.set_widget_common function bluetooth:get_widget() return self._private.widget end --#endregion ---Get the list of paired devices ---@return table devices table of paired devices function bluetooth:get_paired_devices() return self:get_children_by_id("connected_device_list")[1].children end ---Get the list of discovered devices ---@return table devices table of discovered devices function bluetooth:get_discovered_devices() return self:get_children_by_id("discovered_device_list")[1].children end --- Remove a device by first disconnecting it async then removing it function bluetooth:remove_device_information(device) device:DisconnectAsync(function(_, _, out, err) self._private.Adapter1:RemoveDevice(device.object_path) end) end --- Add a new device into the devices list function bluetooth:add_device(device, object_path) -- Get a reference to both lists local plist = self:get_children_by_id("connected_device_list")[1] local dlist = self:get_children_by_id("discovered_device_list")[1] -- For the first list check if the device already exists and if its connection state changed -- if it changed then remove it from the current list and put it into the other one for _, value in pairs(dlist.children) do -- I'm not sure why Connected is in both cases true when its a new connection but eh just take it, it works if value.device.Address:match(device.Address) and (device.Connected ~= value.device.Connected) then return elseif value.device.Address:match(device.Address) and (device.Connected == value.device.Connected) then dlist:remove_widgets(value) plist:add(plist:add(bt_device { device = device, path = object_path, remove_callback = function() self:remove_device_information(device) end, })) return; end end -- Just check if the device already exists in the list for _, value in pairs(plist.children) do if value.device.Address:match(device.Address) then return end end -- If its paired add it to the paired list -- else add it to the discovered list if device.Paired then plist:add(bt_device { device = device, path = object_path, remove_callback = function() self:remove_device_information(device) end, }) else dlist:add(bt_device { device = device, path = object_path, remove_callback = function() self:remove_device_information(device) end, }) end end ---Remove a device from any list ---@param object_path string the object path of the device function bluetooth:remove_device(object_path) local plist = self:get_children_by_id("connected_device_list")[1] local dlist = self:get_children_by_id("discovered_device_list")[1] for _, d in ipairs(dlist.children) do if d.device.object_path == object_path then dlist:remove_widgets(d) end end for _, d in ipairs(plist.children) do if d.device.object_path == object_path then plist:remove_widgets(d) end end end ---Start scanning for devices function bluetooth:scan() self._private.Adapter1:StartDiscovery() end ---Stop scanning for devices function bluetooth:stop_scan() self._private.Adapter1:StopDiscovery() end ---Toggle bluetooth on or off function bluetooth:toggle() local powered = self._private.Adapter1.Powered self._private.Adapter1:Set("org.bluez.Adapter1", "Powered", lgi.GLib.Variant("b", not powered)) self._private.Adapter1.Powered = { signature = "b", value = not powered } end --- Open blueman-manager function bluetooth:open_settings() aspawn("blueman-manager") end ---Get a new device proxy and connect a PropertyChanged signal to it and ---add the device to the list ---@param object_path string the object path of the device function bluetooth:get_device_info(object_path) if (not object_path) or (not object_path:match("/org/bluez/hci0/dev")) then return end -- New Device1 proxy local Device1 = dbus_proxy.Proxy:new { bus = dbus_proxy.Bus.SYSTEM, name = "org.bluez", interface = "org.bluez.Device1", path = object_path } -- New Properties proxy for the object_path local Device1Properties = dbus_proxy.Proxy:new { bus = dbus_proxy.Bus.SYSTEM, name = "org.bluez", interface = "org.freedesktop.DBus.Properties", path = object_path } -- Just return if the Device1 has no name, this usually means random devices with just a mac address if (not Device1.Name) or (Device1.Name == "") then return end -- For some reason it notifies twice or thrice local just_notified = false local notify_timer = gtimer { timeout = 3, autostart = false, single_shot = true, callback = function() just_notified = false end } -- Connect the PropertyChanged signal to update the device when a property changes and send a notification Device1Properties:connect_signal(function(_, _, changed_props) if changed_props["Connected"] ~= nil then if not just_notified then naughty.notification({ app_icon = icondir .. "bluetooth-on.svg", app_name = "Bluetooth", title = Device1.Name, icon = gcolor.recolor_image(icondir .. Device1.Icon .. ".svg", Theme_config.bluetooth_controller.icon_color), timeout = 5, message = "Device " .. Device1.Name .. " is now " .. (changed_props["Connected"] and "connected" or "disconnected"), category = Device1.Connected and "device.added" or "device.removed", }) just_notified = true notify_timer:start() end end capi.awesome.emit_signal(object_path .. "_updated", Device1) end, "PropertiesChanged") self:add_device(Device1, object_path) end ---Send a notification ---@param powered boolean the powered state of the adapter local function send_state_notification(powered) naughty.notification { app_icon = gcolor.recolor_image(icondir .. "bluetooth-on.svg", Theme_config.bluetooth_controller.icon_color), app_name = "Bluetooth", title = "Bluetooth", message = powered and "Enabled" or "Disabled", icon = gcolor.recolor_image(powered and icondir .. "bluetooth-on.svg" or icondir .. "bluetooth-off.svg", Theme_config.bluetooth_controller.icon_color), category = powered and "device.added" or "device.removed", } end function bluetooth.new(args) args = args or {} -- For some reason the first widget isn't read so the first container is a duplicate local ret = base.make_widget_from_value({ { { { { { { resize = false, image = gcolor.recolor_image(icondir .. "menu-down.svg", Theme_config.bluetooth_controller.connected_icon_color), widget = wibox.widget.imagebox, valign = "center", halign = "center", id = "connected_icon" }, { { text = "Paired Devices", valign = "center", halign = "center", widget = wibox.widget.textbox, }, margins = dpi(5), widget = wibox.container.margin }, layout = wibox.layout.fixed.horizontal }, bg = Theme_config.bluetooth_controller.connected_bg, fg = Theme_config.bluetooth_controller.connected_fg, shape = Theme_config.bluetooth_controller.connected_shape, widget = wibox.container.background, id = "connected_bg" }, id = "connected_margin", widget = wibox.container.margin }, { { { step = dpi(50), spacing = dpi(10), layout = require("src.lib.overflow_widget.overflow").vertical, scrollbar_width = 0, id = "connected_device_list" }, id = "margin", margins = dpi(10), widget = wibox.container.margin }, border_color = Theme_config.bluetooth_controller.con_device_border_color, border_width = Theme_config.bluetooth_controller.con_device_border_width, shape = Theme_config.bluetooth_controller.con_device_shape, widget = wibox.container.background, forced_height = 0, id = "connected_list", }, { { { { resize = false, image = gcolor.recolor_image(icondir .. "menu-down.svg", Theme_config.bluetooth_controller.discovered_icon_color), widget = wibox.widget.imagebox, valign = "center", halign = "center", id = "discovered_icon", }, { { text = "Nearby Devices", valign = "center", halign = "center", widget = wibox.widget.textbox, }, margins = dpi(5), widget = wibox.container.margin }, layout = wibox.layout.fixed.horizontal }, id = "discovered_bg", bg = Theme_config.bluetooth_controller.discovered_bg, fg = Theme_config.bluetooth_controller.discovered_fg, shape = Theme_config.bluetooth_controller.discovered_shape, widget = wibox.container.background }, id = "discovered_margin", top = dpi(10), widget = wibox.container.margin }, { { { id = "discovered_device_list", spacing = dpi(10), step = dpi(50), layout = require("src.lib.overflow_widget.overflow").vertical, scrollbar_width = 0, }, margins = dpi(10), widget = wibox.container.margin }, border_color = Theme_config.bluetooth_controller.con_device_border_color, border_width = Theme_config.bluetooth_controller.con_device_border_width, shape = Theme_config.bluetooth_controller.con_device_shape, widget = wibox.container.background, forced_height = 0, id = "discovered_list", }, { { -- action buttons { dnd_widget { color = Theme_config.bluetooth_controller.power_bg, size = dpi(40) }, id = "dnd", widget = wibox.container.place, valign = "center", halign = "center" }, nil, { -- refresh { { image = gcolor.recolor_image(icondir .. "refresh.svg", Theme_config.bluetooth_controller.refresh_icon_color), resize = false, valign = "center", halign = "center", widget = wibox.widget.imagebox, }, widget = wibox.container.margin, margins = dpi(5), }, shape = Theme_config.bluetooth_controller.refresh_shape, bg = Theme_config.bluetooth_controller.refresh_bg, id = "scan", widget = wibox.container.background }, layout = wibox.layout.align.horizontal }, widget = wibox.container.margin, top = dpi(10), }, layout = wibox.layout.fixed.vertical }, margins = dpi(15), widget = wibox.container.margin, }, margins = dpi(15), widget = wibox.container.margin, }) -- Get a reference to the dnd button local dnd = ret:get_children_by_id("dnd")[1]:get_widget() -- Toggle bluetooth on or off dnd:connect_signal("dnd::toggle", function(enable) ret:toggle() end) gtable.crush(ret, bluetooth, true) --#region Bluetooth Proxies -- Create a proxy for the freedesktop ObjectManager ret._private.ObjectManager = dbus_proxy.Proxy:new { bus = dbus_proxy.Bus.SYSTEM, name = "org.bluez", interface = "org.freedesktop.DBus.ObjectManager", path = "/" } -- Create a proxy for the bluez Adapter1 interface ret._private.Adapter1 = dbus_proxy.Proxy:new { bus = dbus_proxy.Bus.SYSTEM, name = "org.bluez", interface = "org.bluez.Adapter1", path = "/org/bluez/hci0" } -- Create a proxy for the bluez Adapter1 Properties interface ret._private.Adapter1Properties = dbus_proxy.Proxy:new { bus = dbus_proxy.Bus.SYSTEM, name = "org.bluez", interface = "org.freedesktop.DBus.Properties", path = "/org/bluez/hci0" } -- Connect to the ObjectManager's InterfacesAdded signal ret._private.ObjectManager:connect_signal(function(_, interface) ret:get_device_info(interface) end, "InterfacesAdded") -- Connect to the ObjectManager's InterfacesRemoved signal ret._private.ObjectManager:connect_signal(function(_, interface) ret:remove_device(interface) end, "InterfacesRemoved") -- Connect to the Adapter1's PropertiesChanged signal ret._private.Adapter1Properties:connect_signal(function(_, _, data) if data.Powered ~= nil then send_state_notification(data.Powered) if data.Powered then dnd:set_enabled() ret:scan() else dnd:set_disabled() end ret:emit_signal("bluetooth::status", data.Powered) end end, "PropertiesChanged") gtimer.delayed_call(function() for path, _ in pairs(ret._private.ObjectManager:GetManagedObjects()) do ret:get_device_info(path) end if ret._private.Adapter1.Powered then dnd:set_enabled() ret:scan() else dnd:set_disabled() end ret:emit_signal("bluetooth::status", ret._private.Adapter1.Powered) send_state_notification(ret._private.Adapter1.Powered) end) --#endregion --#region Dropdown logic local connected_margin = ret:get_children_by_id("connected_margin")[1] local connected_list = ret:get_children_by_id("connected_list")[1] local connected_icon = ret:get_children_by_id("connected_icon")[1] connected_margin:connect_signal( "button::press", function() local rubato_timer = rubato.timed { duration = 0.2, pos = connected_list.forced_height, easing = rubato.linear, subscribed = function(v) connected_list.forced_height = v end } if connected_list.forced_height == 0 then local size = (#ret:get_paired_devices() * 60) if size < 210 then rubato_timer.target = dpi(size) end if size > 0 then connected_margin.connected_bg.shape = function(cr, width, height) gshape.partially_rounded_rect(cr, width, height, true, true, false, false, dpi(4)) end connected_icon:set_image(gcolor.recolor_image(icondir .. "menu-up.svg", Theme_config.bluetooth_controller.connected_icon_color)) end else rubato_timer.target = 0 connected_margin.connected_bg.shape = function(cr, width, height) gshape.rounded_rect(cr, width, height, 4) end connected_icon:set_image(gcolor.recolor_image(icondir .. "menu-down.svg", Theme_config.bluetooth_controller.connected_icon_color)) end end ) local discovered_margin = ret:get_children_by_id("discovered_margin")[1] local discovered_list = ret:get_children_by_id("discovered_list")[1] local discovered_bg = ret:get_children_by_id("discovered_bg")[1] local discovered_icon = ret:get_children_by_id("discovered_icon")[1] discovered_margin:connect_signal( "button::press", function() local rubato_timer = rubato.timed { duration = 0.2, pos = discovered_list.forced_height, easing = rubato.linear, subscribed = function(v) discovered_list.forced_height = v end } if discovered_list.forced_height == 0 then local size = (#ret:get_discovered_devices() * 60) if size > 210 then size = 210 end if size > 0 then rubato_timer.target = dpi(size) discovered_margin.discovered_bg.shape = function(cr, width, height) gshape.partially_rounded_rect(cr, width, height, true, true, false, false, dpi(4)) end discovered_icon:set_image(gcolor.recolor_image(icondir .. "menu-up.svg", Theme_config.bluetooth_controller.discovered_icon_color)) end else rubato_timer.target = 0 discovered_bg.shape = function(cr, width, height) gshape.rounded_rect(cr, width, height, 4) end discovered_icon:set_image(gcolor.recolor_image(icondir .. "menu-down.svg", Theme_config.bluetooth_controller.discovered_icon_color)) end end ) --#endregion -- Add buttons to the scan button ret:get_children_by_id("scan")[1]:buttons({ abutton({}, 1, function() ret:scan() end) }) Hover_signal(ret:get_children_by_id("scan")[1]) return ret end function bluetooth.mt:__call(...) return bluetooth.new(...) end return setmetatable(bluetooth, bluetooth.mt)