From 62a32c456b90fcc3a5dbf1ec74edb2c36b6cb1ed Mon Sep 17 00:00:00 2001 From: Adriano Caloiaro Date: Mon, 18 Aug 2025 13:48:53 -0700 Subject: [PATCH] Initial commit --- .envrc | 10 + .gitignore | 2 + LICENSE | 24 + README.md | 86 ++ default.nix | 22 + flake.lock | 112 ++ flake.nix | 55 + go.mod | 5 + go.sum | 2 + main.go | 253 +++++ .../github.com/godbus/dbus/v5/CONTRIBUTING.md | 50 + vendor/github.com/godbus/dbus/v5/LICENSE | 25 + vendor/github.com/godbus/dbus/v5/MAINTAINERS | 3 + vendor/github.com/godbus/dbus/v5/README.md | 46 + vendor/github.com/godbus/dbus/v5/auth.go | 257 +++++ .../godbus/dbus/v5/auth_anonymous.go | 16 + .../godbus/dbus/v5/auth_external.go | 26 + vendor/github.com/godbus/dbus/v5/auth_sha1.go | 102 ++ vendor/github.com/godbus/dbus/v5/call.go | 69 ++ vendor/github.com/godbus/dbus/v5/conn.go | 996 ++++++++++++++++++ .../github.com/godbus/dbus/v5/conn_darwin.go | 37 + .../github.com/godbus/dbus/v5/conn_other.go | 90 ++ vendor/github.com/godbus/dbus/v5/conn_unix.go | 17 + .../github.com/godbus/dbus/v5/conn_windows.go | 15 + vendor/github.com/godbus/dbus/v5/dbus.go | 430 ++++++++ vendor/github.com/godbus/dbus/v5/decoder.go | 292 +++++ .../godbus/dbus/v5/default_handler.go | 342 ++++++ vendor/github.com/godbus/dbus/v5/doc.go | 71 ++ vendor/github.com/godbus/dbus/v5/encoder.go | 235 +++++ vendor/github.com/godbus/dbus/v5/escape.go | 84 ++ vendor/github.com/godbus/dbus/v5/export.go | 463 ++++++++ vendor/github.com/godbus/dbus/v5/homedir.go | 25 + vendor/github.com/godbus/dbus/v5/match.go | 89 ++ vendor/github.com/godbus/dbus/v5/message.go | 390 +++++++ vendor/github.com/godbus/dbus/v5/object.go | 174 +++ vendor/github.com/godbus/dbus/v5/sequence.go | 24 + .../godbus/dbus/v5/sequential_handler.go | 125 +++ .../godbus/dbus/v5/server_interfaces.go | 107 ++ vendor/github.com/godbus/dbus/v5/sig.go | 293 ++++++ .../godbus/dbus/v5/transport_darwin.go | 6 + .../godbus/dbus/v5/transport_generic.go | 52 + .../godbus/dbus/v5/transport_nonce_tcp.go | 39 + .../godbus/dbus/v5/transport_tcp.go | 41 + .../godbus/dbus/v5/transport_unix.go | 212 ++++ .../dbus/v5/transport_unixcred_dragonfly.go | 95 ++ .../dbus/v5/transport_unixcred_freebsd.go | 92 ++ .../dbus/v5/transport_unixcred_linux.go | 25 + .../dbus/v5/transport_unixcred_netbsd.go | 14 + .../dbus/v5/transport_unixcred_openbsd.go | 14 + .../godbus/dbus/v5/transport_zos.go | 6 + vendor/github.com/godbus/dbus/v5/variant.go | 150 +++ .../godbus/dbus/v5/variant_lexer.go | 284 +++++ .../godbus/dbus/v5/variant_parser.go | 817 ++++++++++++++ vendor/modules.txt | 3 + version.txt | 1 + 55 files changed, 7315 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 default.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md create mode 100644 vendor/github.com/godbus/dbus/v5/LICENSE create mode 100644 vendor/github.com/godbus/dbus/v5/MAINTAINERS create mode 100644 vendor/github.com/godbus/dbus/v5/README.md create mode 100644 vendor/github.com/godbus/dbus/v5/auth.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_anonymous.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_external.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_sha1.go create mode 100644 vendor/github.com/godbus/dbus/v5/call.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_darwin.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_other.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_unix.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_windows.go create mode 100644 vendor/github.com/godbus/dbus/v5/dbus.go create mode 100644 vendor/github.com/godbus/dbus/v5/decoder.go create mode 100644 vendor/github.com/godbus/dbus/v5/default_handler.go create mode 100644 vendor/github.com/godbus/dbus/v5/doc.go create mode 100644 vendor/github.com/godbus/dbus/v5/encoder.go create mode 100644 vendor/github.com/godbus/dbus/v5/escape.go create mode 100644 vendor/github.com/godbus/dbus/v5/export.go create mode 100644 vendor/github.com/godbus/dbus/v5/homedir.go create mode 100644 vendor/github.com/godbus/dbus/v5/match.go create mode 100644 vendor/github.com/godbus/dbus/v5/message.go create mode 100644 vendor/github.com/godbus/dbus/v5/object.go create mode 100644 vendor/github.com/godbus/dbus/v5/sequence.go create mode 100644 vendor/github.com/godbus/dbus/v5/sequential_handler.go create mode 100644 vendor/github.com/godbus/dbus/v5/server_interfaces.go create mode 100644 vendor/github.com/godbus/dbus/v5/sig.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_darwin.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_generic.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_nonce_tcp.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_tcp.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unix.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_dragonfly.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_freebsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_linux.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_netbsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_openbsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_zos.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant_lexer.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant_parser.go create mode 100644 vendor/modules.txt create mode 100644 version.txt diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8e5cd13 --- /dev/null +++ b/.envrc @@ -0,0 +1,10 @@ +if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" +fi + +watch_file flake.nix + +if ! use flake . +then + echo "development shell could not be built. The nix environment was not loaded." >&2 +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..711e0d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +result +.direnv diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2da8d57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2025, Adriano Caloiaro + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8900099 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# btsw + +A tiny Linux CLI that quickly connects and disconnects paired bluetooth devices. + +- List Bluetooth adapters +- List paired devices on any bluetooth adapter +- Connect/disconnect paired devices by id + +btsw talks to BlueZ over the system D‑Bus using [github.com/godbus/dbus](github.com/godbus/dbus). + +## Requirements + +- Linux with BlueZ running (bluetoothd on the system bus) + +Note On NixOS. Enable Bluetooth in your system configuration: +> hardware.bluetooth.enable = true + +## Install + +### Nix flakes + +Run with flakes: +- `nix run https://flakes.adriano.fyi/btsw -- adapters` +- `nix run https://flakes.adriano.fyi/btsw -- devices` +- `nix run https://flakes.adriano.fyi/btsw -- connect 0` +- `nix run https://flakes.adriano.fyi/btsw -- disconnect 4` + +### Flake input + +``` +inputs.btsw = { + url = "https://flakes.adriano.fyi/btsw"; + inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +### Build from source (Go) + +- `git clone http://github.com/acaloiaro/btsw` +- Build: + - `go build -o btsw .` +- Optionally install to your PATH: + - `install -m 0755 btsw /usr/local/bin/` + +## Usage + +``` +Usage: + btsw [--adapter=hciX] [args] + +Commands: + adapters List all available Bluetooth adapters. + devices List paired devices on the selected adapter (default: hci0). + connect Connect to a paired device by its numeric ID (from 'devices'). + disconnect Disconnect from a paired device by its numeric ID (from 'devices'). + +Options: + --adapter=hciX Specify which Bluetooth adapter to use (default: hci0). + --version Print the version. + +Examples: + btsw adapters # List available bluetooth adapaters + btsw devices # List available devices on the chosen bluetooth adapter + btsw --adapter=hci1 devices # List devics on hci1 + btsw connect 0 # Connect to device 0 (from 'devices' list) + btsw disconnect 1 # Disconnect from device 1 (from 'devics' list) +``` + +Notes: +- The device index is stable per run, sorted by MAC address. +- Only paired devices are shown and targetable. + +## Troubleshooting + +- No adapters found: + - Ensure bluetoothd is running (systemctl status bluetooth) + - On NixOS: hardware.bluetooth.enable = true +- No paired devices: + - Pair first using bluetoothctl (scan on, pair, trust) +- Permission denied / not authorized: + - Some systems require Polkit authorization for connect/disconnect + - Try from a logged-in user session; avoid sudo unless necessary + +## Acknowledgments + +- Uses [github.com/godbus/dbus](github.com/godbus/dbus) to speak to BlueZ on the system D‑Bus. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..55753a5 --- /dev/null +++ b/default.nix @@ -0,0 +1,22 @@ +{ + self, + pkgs ? import {}, + ... +}: +pkgs.buildGoModule rec { + pname = "btsw"; + version = pkgs.lib.strings.removeSuffix "\n" (builtins.readFile ./version.txt); + src = ./.; + vendorHash = null; + ldflags = [ + "-X 'main.version=${version}-nix'" + "-X 'main.commit=${self.rev or "dev"}'" + ]; + env.CGO_ENABLED = 0; + + meta = { + description = "A tiny cli for Linux that quickly connects and disconnects paired bluetooth devices"; + homepage = "https://github.com/acaloiaro/btsw"; + license = pkgs.lib.licenses.bsd2; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..31e1dd4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,112 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1741473158, + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", + "owner": "numtide", + "repo": "devshell", + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1754487366, + "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1722073938, + "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1753579242, + "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1755268003, + "narHash": "sha256-nNaeJjo861wFR0tjHDyCnHs1rbRtrMgxAKMoig9Sj/w=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "32f313e49e42f715491e1ea7b306a87c16fe0388", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_2", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..43fdafe --- /dev/null +++ b/flake.nix @@ -0,0 +1,55 @@ +{ + description = "A tiny cli for Linux that quickly connects and disconnects paired bluetooth devices"; + inputs = { + devshell.url = "github:numtide/devshell"; + flake-parts.url = "github:hercules-ci/flake-parts"; + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + systems.url = "github:nix-systems/default"; + }; + outputs = inputs @ { + self, + flake-parts, + ... + }: + inputs.flake-parts.lib.mkFlake {inherit inputs;} (top @ { + config, + withSystem, + moduleWithSystem, + ... + }: { + systems = import inputs.systems; + imports = [ + inputs.devshell.flakeModule + ]; + perSystem = { + self, + pkgs, + config, + lib, + system, + ... + }: { + packages.default = pkgs.callPackage ./. { + inherit pkgs; + self = top.self; + }; + + devshells.default = { + env = []; + packages = with pkgs; [ + go_1_24 + ]; + }; + }; + + # Add overlays to the root output set + flake.overlays = { + default = final: prev: { + btsw = final.callPackage ./. { + pkgs = final; + self = self; + }; + }; + }; + }); +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0dd576a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module adriano.fyi/btsw + +go 1.24.5 + +require github.com/godbus/dbus/v5 v5.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..024b269 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..37dd612 --- /dev/null +++ b/main.go @@ -0,0 +1,253 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path" + "sort" + "strconv" + "strings" + "time" + + "github.com/godbus/dbus/v5" +) + +var ( + version = "dev" + commit = "none" + date = time.Now().Format(time.RFC3339) +) + +const ( + bluezBusName = "org.bluez" + objectManagerIface = "org.freedesktop.DBus.ObjectManager" + deviceIface = "org.bluez.Device1" + adapterIface = "org.bluez.Adapter1" +) + +type deviceInfo struct { + path dbus.ObjectPath + addr string + name string + connected bool + paired bool +} + +type adapterInfo struct { + name string // e.g. "hci0" + path dbus.ObjectPath +} + +func main() { + executableTable := path.Base(os.Args[0]) + usage := func() { + + fmt.Printf(`Usage: + %s [--adapter=hciX] [args] + +Commands: + adapters List all available Bluetooth adapters. + devices List paired devices on the selected adapter (default: hci0). + connect Connect to a paired device by its numeric ID (from 'devices'). + disconnect Disconnect from a paired device by its numeric ID (from 'devices'). + +Options: + --adapter=hciX Specify which Bluetooth adapter to use (default: hci0). + --version Print the version. + +Examples: + %s adapters # List available bluetooth adapaters + %s devices # List available devices on the chosen bluetooth adapter + %s --adapter=hci1 devices # List devics on hci1 + %s connect 0 # Connect to device 0 (from 'devices' list) + %s disconnect 1 # Disconnect from device 1 (from 'devics' list) +`, executableTable, executableTable, executableTable, executableTable, executableTable, executableTable) + } + flags := flag.NewFlagSet("global", flag.ContinueOnError) + + flags.Usage = usage + adapter := flags.String("adapter", "hci0", "Bluetooth adapter (e.g., hci0, hci1)") + versionFlag := flags.Bool("version", false, "Print the version") + + err := flags.Parse(os.Args[1:]) + if err != nil { + os.Exit(1) + } + + if versionFlag != nil && *versionFlag { + fmt.Fprintf(os.Stdout, "btsw version: %s built at: %s from commit: %s\n", version, date, commit) + os.Exit(0) + } + + args := flags.Args() + if len(args) < 1 { + usage() + os.Exit(0) + } + + conn, err := dbus.SystemBus() + if err != nil { + log.Fatalf("Failed to connect to system bus: %v", err) + } + + cmd := os.Args[1] + switch cmd { + case "adapters": + adapters, err := getAdapters(conn) + if err != nil { + log.Fatalf("Error fetching bluetooth adapters: %v", err) + } + if len(adapters) == 0 { + fmt.Println("No bluetooth adapters found.") + return + } + for i, a := range adapters { + fmt.Printf("[%d] %s\n", i, a.name) + } + + case "devices": + paired, err := getPairedDevices(conn, *adapter) + if err != nil { + log.Fatalf("Error fetching paired devices: %v", err) + } + if len(paired) == 0 { + fmt.Println("No paired devices found.") + return + } + for i, d := range paired { + fmt.Printf("[%d] %s (%s) Connected=%t\n", i, displayName(d), d.addr, d.connected) + } + + case "connect", "disconnect": + if len(args) < 2 { + usage() + os.Exit(1) + } + id, err := strconv.Atoi(args[1]) + if err != nil { + log.Fatalf("Invalid id: %v", err) + } + + paired, err := getPairedDevices(conn, *adapter) + if err != nil { + log.Fatalf("Error fetching paired devices: %v", err) + } + if id < 0 || id >= len(paired) { + log.Fatalf("Invalid device id %d", id) + } + d := paired[id] + + devObj := conn.Object(bluezBusName, d.path) + if cmd == "connect" { + fmt.Printf("Connecting to %s on %s...\n", displayName(d), *adapter) + if call := devObj.Call(deviceIface+".Connect", 0); call.Err != nil { + log.Fatalf("Failed to connect: %v", call.Err) + } + fmt.Println("Connected.") + } else { + fmt.Printf("Disconnecting from %s on %s...\n", displayName(d), *adapter) + if call := devObj.Call(deviceIface+".Disconnect", 0); call.Err != nil { + log.Fatalf("Failed to disconnect: %v", call.Err) + } + fmt.Println("Disconnected.") + } + + default: + usage() + os.Exit(0) + } +} + +func getBool(props map[string]dbus.Variant, key string) bool { + if v, ok := props[key]; ok { + if b, ok := v.Value().(bool); ok { + return b + } + } + return false +} + +func getString(props map[string]dbus.Variant, key string) string { + if v, ok := props[key]; ok { + if s, ok := v.Value().(string); ok { + return s + } + } + return "" +} + +func displayName(d deviceInfo) string { + if d.name != "" { + return d.name + } + if d.addr != "" { + return d.addr + } + return string(d.path) +} + +// getPairedDevices queries BlueZ for paired devices on a specific adapter +func getPairedDevices(conn *dbus.Conn, adapter string) ([]deviceInfo, error) { + obj := conn.Object(bluezBusName, dbus.ObjectPath("/")) + + var managedObjects map[dbus.ObjectPath]map[string]map[string]dbus.Variant + if err := obj.Call(objectManagerIface+".GetManagedObjects", 0).Store(&managedObjects); err != nil { + return nil, fmt.Errorf("GetManagedObjects failed: %w", err) + } + + adapterPath := "/org/bluez/" + adapter + var paired []deviceInfo + for objPath, ifaces := range managedObjects { + if props, ok := ifaces[deviceIface]; ok { + // Only include devices under the chosen adapter + if !strings.HasPrefix(string(objPath), adapterPath) { + continue + } + if getBool(props, "Paired") { + paired = append(paired, deviceInfo{ + path: objPath, + addr: getString(props, "Address"), + name: getString(props, "Name"), + connected: getBool(props, "Connected"), + paired: true, + }) + } + } + } + + // Ensure stable order by sorting on MAC address + sort.Slice(paired, func(i, j int) bool { + return paired[i].addr < paired[j].addr + }) + + return paired, nil +} + +// getAdapters queries BlueZ for available adapters +func getAdapters(conn *dbus.Conn) ([]adapterInfo, error) { + obj := conn.Object(bluezBusName, dbus.ObjectPath("/")) + + var managedObjects map[dbus.ObjectPath]map[string]map[string]dbus.Variant + if err := obj.Call(objectManagerIface+".GetManagedObjects", 0).Store(&managedObjects); err != nil { + return nil, fmt.Errorf("GetManagedObjects failed: %w", err) + } + + var adapters []adapterInfo + for objPath, ifaces := range managedObjects { + if _, ok := ifaces[adapterIface]; ok { + parts := strings.Split(string(objPath), "/") + if len(parts) > 0 { + name := parts[len(parts)-1] // e.g. "hci0" + adapters = append(adapters, adapterInfo{name: name, path: objPath}) + } + } + } + + sort.Slice(adapters, func(i, j int) bool { + return adapters[i].name < adapters[j].name + }) + + return adapters, nil +} diff --git a/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md new file mode 100644 index 0000000..c88f9b2 --- /dev/null +++ b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +