Show bluetooth information for all connected bluetooth devices using the `system_profiler` binary.
#!/usr/bin/env ruby
# <xbar.title>Bluetooth Inspector</xbar.title>
# <xbar.version>0.1.4</xbar.version>
# <xbar.author>Ryan Scott Lewis</xbar.author>
# <xbar.author.github>RyanScottLewis</xbar.author.github>
# <xbar.desc>Show bluetooth information for all connected bluetooth devices using the `system_profiler` binary.</xbar.desc>
# <xbar.image>https://raw.githubusercontent.com/RyanScottLewis/bitbar-bluetooth_inspector/master/bitbar-bluetooth_inspector.png</xbar.image>
# <xbar.dependencies>ruby</xbar.dependencies>
# <xbar.abouturl>https://github.com/RyanScottLewis/bitbar-bluetooth_inspector</xbar.abouturl>
# NOTE: Configuration is at the BOTTOM of this file!
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
# -= Code -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
require 'open3'
module BluetoothInspector
# The plugin controller.
class Controller
class << self
# Collect bluetooth devices, format for bitbar output, and print to output.
#
# @return [String]
def run(&block)
new.run(&block)
end
end
def initialize
collect_devices
setup_formatter
end
# Get all devices.
#
# @return [<Device>]
attr_reader :devices
# Get the formatter.
#
# @return [Formatter]
attr_reader :formatter
# Collect bluetooth devices, format for bitbar output, and print to output.
#
# @return [String]
def run(&block)
run_config_block_if_needed(&block)
output = @formatter.format(@devices)
print output
end
protected
def collect_devices
stdout_str, _, _ = Open3.capture3("system_profiler SPBluetoothDataType")
@devices = Parser.parse(stdout_str)
end
def setup_formatter
@formatter = Formatter.new
end
def run_config_block_if_needed(&block)
return nil unless block_given?
block.arity > 0 ? yield(self) : run_in_controller_context(&block)
end
def run_in_controller_context(&block)
ControllerContext.new(self).instance_eval(&block)
end
end
end
module BluetoothInspector
# The DSL context for the configuration block given to the Controller.
class ControllerContext
def initialize(controller)
@controller = controller
end
# Get all devices.
#
# @return [<Device>]
def devices
@controller.devices
end
# Find a device by it's name or shortname.
#
# @param [#to_s] value
# @return [<Device>]
def device(value, &block)
value = value.to_s
device = devices.find { |d| d.name == value || d.shortname == value }
run_device_block_if_needed(device, &block)
device
end
def bar_format(value)
@controller.formatter.bar_format = value
end
def item_format(value)
@controller.formatter.item_format = value
end
protected
def run_device_block_if_needed(device, &block)
return nil unless !device.nil? && block_given?
block.arity > 0 ? yield(device) : run_in_device_context(device, &block)
end
def run_in_device_context(device, &block)
DeviceContext.new(device).instance_eval(&block)
end
end
end
module BluetoothInspector
# A bluetooth device.
class Device
def initialize(attributes={})
@bar_item = true
@menu_item = true
update_attributes(attributes)
raise "name must be given" if @name.nil?
end
# Get the name of the device.
#
# @return [String]
attr_reader :name
# Set the name of the device.
#
# @param [#to_s] value
# @return [String]
def name=(value)
@name = value.to_s
end
# Get the major type of the device.
#
# @return [String]
attr_reader :major_type
# Set the major_type of the device.
#
# @param [#to_s] value
# @return [String]
def major_type=(value)
@major_type = value.to_s
end
# Get the minor type of the device.
#
# @return [String]
attr_reader :minor_type
# Set the minor_type of the device.
#
# @param [#to_s] value
# @return [String]
def minor_type=(value)
@minor_type = value.to_s
end
# Get whether the device is paired.
#
# @return [Boolean]
def paired?
@paired
end
# Get whether the device is not paired.
#
# @return [Boolean]
def unpaired?
!@paired
end
# set whether the device is paired.
#
# @param [Boolean] value
# @return [Boolean]
def paired=(value)
@paired = !!value
end
# Get whether the device is connected.
#
# @return [Boolean]
def connected?
@connected
end
# Get whether the device is not connected.
#
# @return [Boolean]
def disconnected?
!@connected
end
# set whether the device is connected.
#
# @param [Boolean] value
# @return [Boolean]
def connected=(value)
@connected = !!value
end
# Get the shortname of the device.
#
# @return [nil, String]
def shortname
@shortname.nil? ? @name : @shortname
end
# Set the shortname of the device.
#
# @param [nil, #to_s] value
# @return [nil, String]
def shortname=(value)
@shortname = value.nil? ? nil : value.to_s
end
# Get whether this device has a shortname.
#
# @return [Boolean]
def shortname?
[email protected]?
end
# Get the battery level in a range of `0..100`.
#
# @return [nil, Float]
attr_reader :battery
# Set the battery level in a range of `0..100`.
#
# @param [nil, #to_f] value
# @return [nil Float]
def battery=(value)
@battery = value.nil? ? nil : value.to_i
end
# Get whether this device has no battery level.
#
# @return [Boolean]
def no_battery?
@battery.nil?
end
# Get whether this device has a battery level.
#
# @return [Boolean]
def battery?
!no_battery?
end
# Get the color of the device.
#
# @return [nil, String]
attr_reader :color
# Set the name of the device.
#
# @param [nil, #to_s] value
# @return [nil, String]
def color=(value)
@color = value.nil? ? nil : value.to_s
end
# Get whether this device has a color.
#
# @return [Boolean]
def color?
[email protected]?
end
# Get whether this device is shown within the bar.
#
# @return [Boolean]
def bar_item?
@bar_item
end
# Set whether this device is shown within the bar.
#
# @param [Boolean] value
# @return [Boolean]
def bar_item=(value)
@bar_item = !!value
end
# Get whether this device is shown within the menu.
#
# @return [Boolean]
def menu_item?
@menu_item
end
# Set whether this device is shown within the menu.
#
# @param [Boolean] value
# @return [Boolean]
def menu_item=(value)
@menu_item = !!value
end
# Get device's attributes.
#
# @return [Hash]
def to_h
{
name: @name,
shortname: @shortname,
battery: @battery
}
end
protected
def update_attributes(attributes)
attributes.to_h.each do |name, value|
next unless self.class.method_defined?("#{name}=")
send("#{name}=", value)
end
end
end
end
module BluetoothInspector
# The DSL context for a device.
class DeviceContext
def initialize(device)
@device = device
end
# Get the device.
#
# @param [String]
# @return [String]
attr_reader :device
# Get/set the name of the device.
#
# @param [String]
# @return [String]
def name(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set the major type of the device.
#
# @param [String]
# @return [String]
def major_type(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set the minor type of the device.
#
# @param [String]
# @return [String]
def minor_type(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set whether the device is paired.
#
# @param [String]
# @return [String]
def paired?(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set whether the device is connected.
#
# @param [String]
# @return [String]
def connected?(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set the shortname of the device.
#
# @return [nil, String]
def shortname(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set the battery level of the device.
#
# @return [nil, Integer]
def battery(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set the color of the device.
#
# @return [nil, String]
def color(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set whether the device is shown within the bar.
#
# @return [Boolean]
def bar_item(*arguments)
get_or_set_attribute(__method__, arguments)
end
# Get/set whether the device is shown within the menu.
#
# @return [Boolean]
def menu_item(*arguments)
get_or_set_attribute(__method__, arguments)
end
protected
def validate_arguments(arguments)
raise ArgumentError, "wrong number of arguments (given #{arguments.count}, expected 0..1)" if arguments.length > 1
end
def get_or_set_attribute(name, arguments)
validate_arguments(arguments)
arguments.empty? ? @device.send(name) : @device.send("#{name}=", arguments.first)
end
end
end
module BluetoothInspector
# Formats a list of devices for the expected bitbar output.
class Formatter
def initialize
@bar_format = ":shortname :battery%"
@item_format = ":name"
end
# Get the format for a device as a bar item.
#
# @return [String]
attr_reader :bar_format
# Set the format for a device as a bar item.
#
# @param [#to_s] value
# @return [String]
def bar_format=(value)
@bar_format = value.to_s
end
# Get the format for a device as a menu item.
#
# @return [String]
attr_reader :item_format
# Set the format for a device as a menu item.
#
# @param [#to_s] value
# @return [String]
def item_format=(value)
@item_format = value.to_s
end
def format(devices)
lines = []
devices.find_all(&:bar_item?).each { |device| lines << format_device(@bar_format, device) }
lines << "---"
devices.find_all(&:menu_item?).each { |device| lines << format_device(@item_format, device) }
lines.join("\n")
end
protected
def format_device(format_string, device)
output = format_string
device.to_h.each { |name, value| output = output.gsub(/:#{name}/, value.to_s) }
device.color.nil? ? output : output + " | color=#{device.color}"
end
end
end
require "yaml"
module BluetoothInspector
# Parses the output of the `system_profiler` command and returns an Array of Device instances.
class Parser
class << self
# Parse the command output.
#
# @param [#to_s] data The command output.
# @return [<Device>]
def parse(data)
new.parse(data)
end
end
# Parse the command output.
#
# @param [#to_s] data The command output.
# @return [<Device>]
def parse(data)
data = YAML.load(data.to_s)
data["Bluetooth"]["Connected"].collect do |name, attributes|
Device.new(
name: name,
battery: attributes["Battery Level"],
major_type: attributes["Major Type"],
minor_type: attributes["Minor Type"],
paired: attributes["Paired"],
connected: true,
)
end
end
end
end
module BluetoothInspector
class << self
# Run the plugin controller.
def run(&block)
Controller.run(&block)
end
end
end
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
# -= Configuration =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #
# See https://github.com/RyanScottLewis/bitbar-bluetooth_inspector for configuration documentation.
BluetoothInspector.run do
devices.delete_if(&:no_battery?)
devices.delete_if(&:disconnected?)
# Color all low battery level devices red:
devices.find_all { |device| device.battery < 20 }.each { |device| device.color = "red" }
# Add device emojis to each shortname
devices.each do |device|
device.shortname = case device.minor_type
when 'Mouse' then '🖱'
when 'Keyboard' then '⌨️'
end
end
end