Browse Source

Updated btswitch

master
chodak166 1 month ago
parent
commit
997a8f5018
  1. 13
      .config/btswitch/devices.conf
  2. 26
      .config/nvim/lazy-lock.json
  3. 691
      .local/bin/btswitch

13
.config/btswitch/devices.conf

@ -0,0 +1,13 @@
# btswitch device configuration
# Format: dev NAME MAC (a known device)
# auto NAME (member of the auto-connect list)
# Prefer managing this via: btswitch add/remove/auto
dev EM03 F1:5A:D5:16:57:9D
dev TAH8506 00:1E:7C:CA:77:00
dev XTREME 41:42:5D:FA:26:0A
dev X5 47:0D:FF:C9:62:E1
dev SHB7150 00:1E:7C:30:F3:69
dev HD450BT 00:16:94:33:91:51
dev JLABGS 9F:DA:08:A9:EB:1E
auto TAH8506
auto SHB7150

26
.config/nvim/lazy-lock.json

@ -1,42 +1,42 @@
{
"LazyVim": { "branch": "main", "commit": "c10948c50b18fae7f256433afdef09e432410480" },
"avante.nvim": { "branch": "main", "commit": "b57b7d8e601ec6c7675c67de40cc7f5f0c0d40d0" },
"avante.nvim": { "branch": "main", "commit": "98bfe1c2a41d645e8d1cbc882aa1067e5f1b6cb0" },
"blink.cmp": { "branch": "main", "commit": "78336bc89ee5365633bcf754d93df01678b5c08f" },
"bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" },
"catppuccin": { "branch": "main", "commit": "79e2049a0fdf7ec840f8463fe1d962c94493196b" },
"catppuccin": { "branch": "main", "commit": "edefef779ab08ce1a4a404713e3012b0d202bd35" },
"clangd-container.nvim": { "branch": "master", "commit": "8bfb4f202ea64710ce46d8790ca95c9ee89b6ce6" },
"clangd_extensions.nvim": { "branch": "main", "commit": "78c2ecd659d54972be17aa6ba2deac3c53223b80" },
"conform.nvim": { "branch": "master", "commit": "619363c30309d29ffa631e67c8183f2a72caa373" },
"conform.nvim": { "branch": "master", "commit": "016802de402556da54c36bd7359b441266b01cdd" },
"diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" },
"dotenv.nvim": { "branch": "main", "commit": "7d516e9293c6e3ac21830fb10a4e8674c02747c6" },
"dressing.nvim": { "branch": "master", "commit": "2d7c2db2507fa3c4956142ee607431ddb2828639" },
"flash.nvim": { "branch": "main", "commit": "b6346946d10d07998efee029fb0f7a593806d0cd" },
"flash.nvim": { "branch": "main", "commit": "5f0f270fdc7c5b0c21d903ee85b9cb06f2ac636a" },
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
"gitsigns.nvim": { "branch": "main", "commit": "31d6fb2d618bca1482b9f274751ead5f03461408" },
"grug-far.nvim": { "branch": "main", "commit": "0f6a5d0d7a70659b11bbcd6b3673ee0fe61b6024" },
"gitsigns.nvim": { "branch": "main", "commit": "5be654f2232c10ddcad19c1607a67b6b4b78fc29" },
"grug-far.nvim": { "branch": "main", "commit": "11595bf747edc270bce2069d1020502ad4ae56cf" },
"hybrid-theme": { "branch": "main", "commit": "626ae5426a7c8f21dfccce286c690d9cf4195b46" },
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"just-runner.nvim": { "branch": "main", "commit": "f29d405aa828900df242600720a2b0e57261489f" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
"lazydev.nvim": { "branch": "main", "commit": "ff2cbcba459b637ec3fd165a2be59b7bbaeedf0d" },
"lualine.nvim": { "branch": "master", "commit": "221ce6b2d999187044529f49da6554a92f740a96" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "7adc933dabcc7c86ae6b07aff7ee68eac398491f" },
"mason-lspconfig.nvim": { "branch": "main", "commit": "9d28935aa95fa6c1dc7c4d6747f101a72a8a4bec" },
"mason.nvim": { "branch": "main", "commit": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d" },
"mini.ai": { "branch": "main", "commit": "25248c6aa002391936a6200f12d1466015987133" },
"mini.files": { "branch": "main", "commit": "fe78cd12e5501a7a56413096c20167568ec2ac8e" },
"mini.files": { "branch": "main", "commit": "a249438d15fa6f64085ba83a9d1794d726aaf5ad" },
"mini.icons": { "branch": "main", "commit": "98faae31e9be1cc054ae63485e58ceb185efcad0" },
"mini.pairs": { "branch": "main", "commit": "b1c5a726921b7a8c9321e9a7a208aa0571de5810" },
"noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" },
"nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" },
"nui.nvim": { "branch": "main", "commit": "10fc361835c856ba4233ef5ea135b919bf3dce97" },
"nvim-dev-container": { "branch": "main", "commit": "365b4c815339d42133829006dce3e5f8b182b771" },
"nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" },
"nvim-lspconfig": { "branch": "master", "commit": "7ab79bb1b57160a537f427ac8a6026102f12b701" },
"nvim-treesitter": { "branch": "main", "commit": "61df84986b4b4ec469ee745a182e433d49f8c27e" },
"nvim-lint": { "branch": "master", "commit": "0370fe965697aa8f04252a1562441bdc13723cd1" },
"nvim-lspconfig": { "branch": "master", "commit": "221c43884319e791519f0d6c94a7f2fbcd653278" },
"nvim-treesitter": { "branch": "main", "commit": "8b98b4470eb326f1c7b50dae79f8c963568e5720" },
"nvim-treesitter-textobjects": { "branch": "main", "commit": "898ee307df58f854d11cd7edd06472574d48014e" },
"nvim-ts-autotag": { "branch": "main", "commit": "88c1453db4ba7dd24131086fe51fdf74e587d275" },
"persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" },
"render-markdown.nvim": { "branch": "main", "commit": "4663eb3ecd538bd5062628fb6d95bbe6bdca78f6" },
"snacks-terminal-ctl.nvim": { "branch": "master", "commit": "97727421c8d0ffa33d893f22e38cda93e9010742" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" },

691
.local/bin/btswitch

@ -7,22 +7,37 @@ set -u
WIFI_5G="cintra-5g"
WIFI_24G="cintra-5g"
declare -A BT_DEVICES
BT_DEVICES[TAH8506]="00:1E:7C:CA:77:00"
BT_DEVICES[SHB7150]="00:1E:7C:30:F3:69"
BT_DEVICES[HD450BT]="00:16:94:33:91:51"
BT_DEVICES[XTREME]="41:42:5D:FA:26:0A" # BT Speaker
BT_DEVICES[EM03]="F1:5A:D5:16:57:9D" # BT Speaker/powerbank
BT_DEVICES[X5]="47:0D:FF:C9:62:E1" # Bone Headphones thinkplus-X5
BT_AUTO_DEVICES=("TAH8506" "SHB7150")
BT_CONFIG_DIR="$HOME/.config/btswitch"
BT_CONFIG_FILE="$BT_CONFIG_DIR/devices.conf"
BT_SERVICE="bluetooth"
BT_RETRIES=6
BT_DELAY=3
BT_SCAN_SECONDS=15
# Defaults written to devices.conf on first run. Afterwards the config
# file is the single source of truth (manage it with add/remove/auto).
BT_DEFAULT_DEVICES=(
"TAH8506 00:1E:7C:CA:77:00"
"SHB7150 00:1E:7C:30:F3:69"
"HD450BT 00:16:94:33:91:51"
"XTREME 41:42:5D:FA:26:0A"
"EM03 F1:5A:D5:16:57:9D"
"X5 47:0D:FF:C9:62:E1"
)
BT_DEFAULT_AUTO=("TAH8506" "SHB7150")
# Sentinel file to track that the sudoers rule was installed
BT_SUDOERS_SENTINEL="$HOME/.config/btswitch/.sudoers_rule_installed"
BT_SUDOERS_SENTINEL="$BT_CONFIG_DIR/.sudoers_rule_installed"
declare -A BT_DEVICES
BT_AUTO_DEVICES=()
DEVICES=()
CMD_ARGS=()
COMMAND=""
TIMED_MINUTES=0
HARD_MODE=0
# -------------------------------------------------
# HELPERS
@ -32,26 +47,37 @@ wifi_switch_needed() {
[[ -n "$1" && -n "$2" && "$1" != "$2" ]]
}
wifi_switch() {
local target="$1"
if ! wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
return 0
fi
echo "Switching Wi-Fi to '$target'..."
nmcli connection up "$target" > /dev/null 2>&1 || echo "⚠ nmcli: could not switch to '$target'" >&2
}
ensure_sudoers_rule() {
# Notice the VERSION=2. This ensures it updates your previous rule to include rfkill.
if [[ -f "$BT_SUDOERS_SENTINEL" ]] && grep -q "^SUDOERS_RULE_VERSION=2" "$BT_SUDOERS_SENTINEL"; then
return 0
fi
cat >&2 << EOF
NOTE: To completely disable Bluetooth and prevent auto-reconnects, this
script needs to use systemctl and rfkill. You will be prompted for your
password ONCE to update your passwordless sudoers rule:
NOTE: 'stop --hard' (full system-wide shutdown) and 'restore' after it
need rights to control the bluetooth service and rfkill. You will be
prompted for your password ONCE to install a passwordless sudoers rule:
/etc/sudoers.d/49-btswitch
Regular start/stop/scan/list do NOT need sudo while Bluetooth is
already running.
EOF
read -r -p "Install the updated sudoers rule now? [y/N] " answer
read -r -p "Install the sudoers rule now? [y/N] " answer
case "$answer" in
y | Y | yes | YES) ;;
*)
echo "Skipping. The script cannot control bluetooth completely without it." >&2
echo "Skipping. 'stop --hard' and 'restore' will not work without it." >&2
return 1
;;
esac
@ -84,12 +110,12 @@ SUDOERS
systemctl_bt() {
ensure_sudoers_rule || {
echo "ERROR: Cannot control bluetooth.service without the sudoers rule." >&2
echo "ERROR: Cannot control $BT_SERVICE.service without the sudoers rule." >&2
exit 1
}
if ! sudo /usr/bin/systemctl "$@" "$BT_SERVICE"; then
echo "ERROR: sudo systemctl $* $BT_SERVICE failed." >&2
exit 1
return 1
fi
}
@ -98,8 +124,20 @@ rfkill_bt() {
sudo /usr/bin/rfkill "$1" bluetooth > /dev/null 2>&1
}
bt_start_service() {
ensure_sudoers_rule || return 1
if sudo /usr/bin/systemctl start "$BT_SERVICE" 2>/dev/null; then
return 0
fi
sudo /usr/bin/systemctl unmask "$BT_SERVICE" || return 1
sudo /usr/bin/systemctl start "$BT_SERVICE"
}
# All bluetoothctl queries go through this: if bluetoothd is not running,
# bluetoothctl would block forever ("Waiting to connect to bluetoothd...").
bt_info() {
bluetoothctl info "$1" 2> /dev/null
bt_service_active || return 1
timeout 5 bluetoothctl info "$1" 2> /dev/null
}
bt_is_paired() {
@ -127,30 +165,153 @@ bt_resolve_device() {
fi
}
bt_name_for_mac() {
local mac="${1^^}" name
for name in "${!BT_DEVICES[@]}"; do
if [[ "${BT_DEVICES[$name]}" == "$mac" ]]; then
echo "$name"
return 0
fi
done
echo "$mac"
}
auto_contains() {
local dev
for dev in "${BT_AUTO_DEVICES[@]}"; do
[[ "$dev" == "$1" ]] && return 0
done
return 1
}
# State checks that do NOT need sudo
bt_rfkill_blocked() {
rfkill list bluetooth 2>/dev/null | grep -qi "blocked: yes"
}
bt_service_masked() {
[[ "$(systemctl is-enabled "$BT_SERVICE" 2>/dev/null)" == "masked" ]]
}
bt_service_active() {
systemctl is-active --quiet "$BT_SERVICE" 2>/dev/null
}
# Devices to act on: explicit -d/positional list, or the auto list
target_devices() {
if [[ ${#DEVICES[@]} -gt 0 ]]; then
printf '%s\n' "${DEVICES[@]}"
elif [[ ${#BT_AUTO_DEVICES[@]} -gt 0 ]]; then
printf '%s\n' "${BT_AUTO_DEVICES[@]}"
fi
}
# -------------------------------------------------
# BLUETOOTH CONTROL
# DEVICE CONFIG FILE
# -------------------------------------------------
start_bluetooth() {
echo "Unblocking hardware radio..."
rfkill_bt unblock
config_seed() {
mkdir -p "$BT_CONFIG_DIR"
{
echo "# btswitch device configuration"
echo "# Format: dev NAME MAC (a known device)"
echo "# auto NAME (member of the auto-connect list)"
echo "# Prefer managing this via: btswitch add/remove/auto"
local entry
for entry in "${BT_DEFAULT_DEVICES[@]}"; do echo "dev $entry"; done
for entry in "${BT_DEFAULT_AUTO[@]}"; do echo "auto $entry"; done
} > "$BT_CONFIG_FILE" || {
echo "ERROR: cannot write $BT_CONFIG_FILE" >&2
exit 1
}
echo "Created device configuration: $BT_CONFIG_FILE" >&2
}
config_load() {
if [[ ! -f "$BT_CONFIG_FILE" ]]; then
config_seed
fi
BT_DEVICES=()
BT_AUTO_DEVICES=()
local line _ name mac
while read -r line || [[ -n "$line" ]]; do
case "$line" in
dev\ *)
read -r _ name mac <<< "$line"
[[ -n "$name" && -n "$mac" ]] && BT_DEVICES["$name"]="$mac"
;;
esac
done < "$BT_CONFIG_FILE"
while read -r line || [[ -n "$line" ]]; do
case "$line" in
auto\ *)
read -r _ name <<< "$line"
if [[ -n "${BT_DEVICES[$name]:-}" ]] && ! auto_contains "$name"; then
BT_AUTO_DEVICES+=("$name")
fi
;;
esac
done < "$BT_CONFIG_FILE"
}
config_save() {
local tmp name
tmp=$(mktemp) || return 1
{
echo "# btswitch device configuration"
echo "# Format: dev NAME MAC (a known device)"
echo "# auto NAME (member of the auto-connect list)"
echo "# Prefer managing this via: btswitch add/remove/auto"
for name in "${!BT_DEVICES[@]}"; do echo "dev $name ${BT_DEVICES[$name]}"; done
for name in "${BT_AUTO_DEVICES[@]}"; do echo "auto $name"; done
} > "$tmp" || { rm -f "$tmp"; return 1; }
mv "$tmp" "$BT_CONFIG_FILE" || { rm -f "$tmp"; return 1; }
}
echo "Starting Bluetooth service..."
systemctl_bt unmask
systemctl_bt start
# -------------------------------------------------
# BLUETOOTH CONTROL
# -------------------------------------------------
echo "Waiting for bluetoothd to become ready..."
# Makes Bluetooth usable again (after a hard stop or if it was off).
# Only invokes sudo for the steps that are actually needed.
bt_ensure_available() {
if bt_rfkill_blocked; then
echo "Unblocking Bluetooth radio..."
rfkill_bt unblock || return 1
fi
if bt_service_masked; then
echo "Unmasking $BT_SERVICE service (it was hard-stopped)..."
systemctl_bt unmask || return 1
fi
if ! bt_service_active; then
echo "Starting $BT_SERVICE service..."
bt_start_service || {
echo "ERROR: could not start $BT_SERVICE.service." >&2
return 1
}
fi
if ! timeout 5 bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then
echo "Powering on adapter..."
local i
for i in {1..10}; do
if bluetoothctl show | grep -q "Powered: yes"; then
bluetoothctl pairable on > /dev/null 2>&1
timeout 5 bluetoothctl power on > /dev/null 2>&1
sleep 1
if timeout 5 bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then
return 0
fi
bluetoothctl power on > /dev/null 2>&1
sleep 1
done
echo "ERROR: Bluetooth adapter not found or could not be powered on."
echo "ERROR: Bluetooth adapter not found or could not be powered on." >&2
return 1
fi
return 0
}
# Full system-wide shutdown (opt-in via --hard)
bt_hard_off() {
timeout 5 bluetoothctl power off > /dev/null 2>&1
bt_service_active && systemctl_bt stop
bt_service_masked || systemctl_bt mask
bt_rfkill_blocked || rfkill_bt block
}
bt_pair_device() {
@ -158,7 +319,7 @@ bt_pair_device() {
if bt_is_paired "$mac"; then
return 0
fi
echo "✖ Device $mac is not paired. Please pair it manually first."
echo "✖ Device $mac is not paired. Pair it once via your desktop Bluetooth settings or 'bluetoothctl pair $mac' (use 'btswitch scan' to discover devices)." >&2
return 1
}
@ -167,11 +328,14 @@ bt_ensure_connected() {
echo "----------------------------------------"
echo "Checking device: $mac"
timeout 5 bluetoothctl unblock "$mac" > /dev/null 2>&1
if ! bt_is_trusted "$mac"; then
echo "→ Trusting $mac"
bluetoothctl trust "$mac" > /dev/null
timeout 5 bluetoothctl trust "$mac" > /dev/null 2>&1
fi
local i
for ((i = 1; i <= BT_RETRIES; i++)); do
if bt_is_connected "$mac"; then
echo "✔ Connected: $mac"
@ -183,7 +347,7 @@ bt_ensure_connected() {
bt_pair_device "$mac" || return 1
fi
bluetoothctl connect "$mac" > /dev/null 2>&1
timeout $((BT_DELAY * 4)) bluetoothctl connect "$mac" > /dev/null 2>&1
sleep "$BT_DELAY"
if bt_is_connected "$mac"; then
@ -196,39 +360,63 @@ bt_ensure_connected() {
return 1
}
# Disconnects and blocks the device so it cannot auto-reconnect,
# while leaving Bluetooth running for everything else.
bt_safe_disconnect() {
local mac="$1"
if bt_is_connected "$mac"; then
if ! bt_is_connected "$mac"; then
return 0
fi
echo "Disconnecting $mac..."
bluetoothctl disconnect "$mac" > /dev/null 2>&1
timeout 5 bluetoothctl disconnect "$mac" > /dev/null 2>&1
sleep 1
if bt_is_connected "$mac"; then
echo "⚠ $mac is still connected." >&2
return 0
fi
echo "Blocking $mac to prevent auto-reconnect ('btswitch start' unblocks it automatically)."
timeout 5 bluetoothctl block "$mac" > /dev/null 2>&1
}
bt_stop_all() {
local devices=("${!1}")
for dev in "${devices[@]}"; do
local mac
bt_start_all() {
local dev mac failed=0
while read -r dev; do
[[ -n "$dev" ]] || continue
mac=$(bt_resolve_device "$dev")
[[ -n "$mac" ]] && bt_safe_disconnect "$mac"
done
if [[ -z "$mac" ]]; then
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2
continue
fi
bt_ensure_connected "$mac" || failed=1
done < <(target_devices)
return "$failed"
}
bt_start_all() {
local devices=("${!1}")
for dev in "${devices[@]}"; do
local mac
bt_stop_all() {
local dev mac
while read -r dev; do
[[ -n "$dev" ]] || continue
mac=$(bt_resolve_device "$dev")
[[ -n "$mac" ]] && bt_ensure_connected "$mac"
done
if [[ -z "$mac" ]]; then
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2
continue
fi
bt_safe_disconnect "$mac"
done < <(target_devices)
}
bt_status_all() {
local devices=("${!1}")
for dev in "${devices[@]}"; do
local mac
bt_status_one() {
local dev="$1" mac
mac=$(bt_resolve_device "$dev")
if [[ -n "$mac" ]]; then
echo "--- $dev ($mac) ---"
if [[ -z "$mac" ]]; then
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2
return 0
fi
echo "--- $(bt_name_for_mac "$mac") ($mac) ---"
if ! bt_service_active; then
echo "Status: unknown (Bluetooth service is off)"
return 0
fi
if bt_is_connected "$mac"; then
echo "Status: CONNECTED"
elif bt_is_paired "$mac"; then
@ -236,17 +424,293 @@ bt_status_all() {
else
echo "Status: Not Paired / Unknown"
fi
}
# -------------------------------------------------
# COMMANDS
# -------------------------------------------------
cmd_start() {
if [[ ${#DEVICES[@]} -eq 0 && ${#BT_AUTO_DEVICES[@]} -eq 0 ]]; then
echo "No devices selected and the auto list is empty (see 'btswitch auto NAME on')."
fi
wifi_switch "$WIFI_5G"
bt_ensure_available || exit 1
bt_start_all
if [[ "$TIMED_MINUTES" -gt 0 ]]; then
local action="disconnect devices"
[[ "$HARD_MODE" -eq 1 ]] && action="hard stop (full Bluetooth shutdown)"
echo "Timed mode: will $action in $TIMED_MINUTES minute(s)..."
sleep $((TIMED_MINUTES * 60))
echo "Time elapsed. Stopping..."
cmd_stop
fi
}
cmd_stop() {
if ! bt_service_active; then
echo "Bluetooth service is not running."
if [[ "$HARD_MODE" -eq 1 ]]; then
bt_hard_off
wifi_switch "$WIFI_24G"
else
echo "If it was hard-stopped earlier, run 'btswitch restore' to re-enable it."
fi
return 0
fi
bt_stop_all
if [[ "$HARD_MODE" -ne 1 ]]; then
echo "Bluetooth left running so other applications can keep using it."
echo "(For a full system-wide shutdown use: btswitch stop --hard)"
return 0
fi
echo "Hard stop: powering off adapter, stopping/masking service and blocking radio."
echo "Bluetooth is disabled system-wide until 'btswitch start' or 'btswitch restore'."
bt_hard_off
wifi_switch "$WIFI_24G"
}
cmd_restore() {
bt_ensure_available && echo "✔ Bluetooth restored, other applications can use it again."
}
cmd_status() {
echo "=== Bluetooth adapter ==="
if bt_rfkill_blocked; then
echo "Radio: blocked (rfkill)"
fi
if bt_service_masked; then
echo "Service: masked (hard-stopped; run 'btswitch restore' to re-enable)"
elif bt_service_active; then
timeout 5 bluetoothctl show 2>/dev/null | grep -E "Name|Powered|Discoverable" | sed 's/^[[:space:]]*/ /'
else
echo "Service: inactive (run 'btswitch start' or 'btswitch restore')"
fi
echo "=== Devices ==="
local devs=("${DEVICES[@]}") dev
if [[ ${#devs[@]} -eq 0 ]]; then
devs=("${!BT_DEVICES[@]}")
fi
if [[ ${#devs[@]} -eq 0 ]]; then
echo "(no devices configured; add one with 'btswitch add NAME MAC')"
return 0
fi
for dev in "${devs[@]}"; do
bt_status_one "$dev"
done
}
cmd_scan() {
local duration="${CMD_ARGS[0]:-$BT_SCAN_SECONDS}"
if ! [[ "$duration" =~ ^[0-9]+$ ]] || (( duration < 1 || duration > 300 )); then
echo "ERROR: scan duration must be between 1 and 300 seconds." >&2
exit 1
fi
bt_ensure_available || exit 1
echo "Scanning for $duration seconds (put devices in range/pairing mode)..."
timeout $((duration + 10)) bluetoothctl --timeout "$duration" scan on > /dev/null 2>&1
timeout 5 bluetoothctl scan off > /dev/null 2>&1
local listing
listing=$(timeout 5 bluetoothctl devices 2>/dev/null | sort -k3)
if [[ -z "$listing" ]]; then
echo "No devices found. Make sure they are turned on and in range."
return 0
fi
local -A mac_to_name=()
local name
for name in "${!BT_DEVICES[@]}"; do
mac_to_name["${BT_DEVICES[$name]}"]="$name"
done
printf "%-24s %-19s %s\n" "NAME" "MAC" "FLAGS"
local _ mac flags flagstr
while read -r _ mac name; do
[[ -n "$mac" ]] || continue
flags=()
[[ -n "${mac_to_name[$mac]:-}" ]] && flags+=("configured")
bt_is_paired "$mac" && flags+=("paired")
bt_is_connected "$mac" && flags+=("connected")
flagstr="new"
[[ ${#flags[@]} -gt 0 ]] && flagstr=$(IFS=,; echo "${flags[*]}")
printf "%-24s %-19s %s\n" "${name:-(unknown)}" "$mac" "$flagstr"
done <<< "$listing"
echo
echo "To manage a found device: btswitch add NAME MAC"
}
cmd_list() {
if [[ ${#BT_DEVICES[@]} -eq 0 ]]; then
echo "No devices configured. Add one with: btswitch add NAME MAC"
return 0
fi
local online=1
bt_service_active || online=0
printf "%-12s %-19s %-18s %s\n" "NAME" "MAC" "STATUS" "AUTO"
local names name mac status auto
names=$(printf '%s\n' "${!BT_DEVICES[@]}" | sort)
while read -r name; do
mac="${BT_DEVICES[$name]}"
if [[ "$online" -eq 1 ]]; then
if bt_is_connected "$mac"; then
status="connected"
elif bt_is_paired "$mac"; then
status="paired (offline)"
else
status="not paired"
fi
else
status="unknown (bt off)"
fi
if auto_contains "$name"; then auto="yes"; else auto="-"; fi
printf "%-12s %-19s %-18s %s\n" "$name" "$mac" "$status" "$auto"
done <<< "$names"
}
cmd_add() {
if [[ ${#CMD_ARGS[@]} -ne 2 ]]; then
echo "Usage: btswitch add NAME MAC" >&2
exit 1
fi
local name="${CMD_ARGS[0]}" mac="${CMD_ARGS[1]^^}"
if ! [[ "$name" =~ ^[A-Za-z0-9][A-Za-z0-9_-]*$ ]]; then
echo "ERROR: invalid NAME '$name' (use letters, digits, '_' and '-')." >&2
exit 1
fi
if ! bt_is_mac_address "$mac"; then
echo "ERROR: invalid MAC address '${CMD_ARGS[1]}'." >&2
exit 1
fi
if [[ -n "${BT_DEVICES[$name]:-}" ]]; then
echo "ERROR: device '$name' is already configured (${BT_DEVICES[$name]})." >&2
exit 1
fi
local other
other=$(bt_name_for_mac "$mac")
if [[ "$other" != "$mac" ]]; then
echo "ERROR: MAC $mac is already configured as '$other'." >&2
exit 1
fi
BT_DEVICES["$name"]="$mac"
config_save || exit 1
echo "✔ Added $name ($mac)"
if bt_service_active && ! bt_is_paired "$mac"; then
echo "Note: not paired yet. Pair it once (desktop Bluetooth settings or 'bluetoothctl pair $mac') while it is in pairing mode."
elif ! bt_service_active; then
echo "Note: Bluetooth service is off; pairing status could not be checked."
fi
echo "Add it to the auto-connect list with: btswitch auto $name on"
}
cmd_remove() {
if [[ ${#CMD_ARGS[@]} -ne 1 ]]; then
echo "Usage: btswitch remove NAME" >&2
exit 1
fi
local name="${CMD_ARGS[0]}"
if [[ -z "${BT_DEVICES[$name]:-}" ]]; then
echo "ERROR: device '$name' is not configured. See 'btswitch list'." >&2
exit 1
fi
local mac="${BT_DEVICES[$name]}"
unset "BT_DEVICES[$name]"
local filtered=() dev
for dev in "${BT_AUTO_DEVICES[@]}"; do
[[ "$dev" != "$name" ]] && filtered+=("$dev")
done
BT_AUTO_DEVICES=("${filtered[@]}")
config_save || exit 1
echo "✔ Removed $name ($mac) from the btswitch config."
echo "It stays paired in BlueZ; to fully remove it: bluetoothctl remove $mac"
}
cmd_auto() {
if [[ ${#CMD_ARGS[@]} -eq 0 ]]; then
echo "Auto-connect devices: ${BT_AUTO_DEVICES[*]:-<none>}"
return 0
fi
if [[ ${#CMD_ARGS[@]} -ne 2 ]]; then
echo "Usage: btswitch auto NAME on|off" >&2
exit 1
fi
local name="${CMD_ARGS[0]}" state="${CMD_ARGS[1]}"
if [[ -z "${BT_DEVICES[$name]:-}" ]]; then
echo "ERROR: device '$name' is not configured (add it first)." >&2
exit 1
fi
case "$state" in
on)
if auto_contains "$name"; then
echo "'$name' is already in the auto list."
return 0
fi
BT_AUTO_DEVICES+=("$name")
;;
off)
local filtered=() dev
for dev in "${BT_AUTO_DEVICES[@]}"; do
[[ "$dev" != "$name" ]] && filtered+=("$dev")
done
BT_AUTO_DEVICES=("${filtered[@]}")
;;
*)
echo "Usage: btswitch auto NAME on|off" >&2
exit 1
;;
esac
config_save || exit 1
echo "✔ Auto-connect list: ${BT_AUTO_DEVICES[*]:-<none>}"
}
usage() {
cat << EOF
Usage: btswitch [options] COMMAND [args...]
Connect and manage predefined Bluetooth devices.
Commands:
start [devices...] Make Bluetooth available and connect the auto devices
(or only the given devices: names or MACs)
stop [devices...] Disconnect the devices and block them so they do not
auto-reconnect. Bluetooth stays on for other apps.
stop --hard Additionally: power off adapter, stop+mask service,
rfkill block (Bluetooth off system-wide)
restore Undo a hard stop (unblock radio, unmask/start service)
status [devices...] Adapter state + device status (default: all configured)
list List configured devices and their status
scan [seconds] Scan for nearby devices (default: $BT_SCAN_SECONDS s)
add NAME MAC Add a device to the configuration
remove NAME Remove a device from the configuration
auto [NAME on|off] Show the auto-connect list, or modify it
help Show this help
Options:
-d, --device NAME|MAC Act only on this device (repeatable)
-t, --timed MINUTES Stop automatically after MINUTES minutes
--hard With stop/-t: full system-wide Bluetooth shutdown
-h, --help Show this help
Devices are stored in $BT_CONFIG_FILE.
Devices must be paired once (e.g. via desktop Bluetooth settings) before
btswitch can connect them. Sudo is only needed for --hard/restore, and
only when the service or radio state actually has to be changed.
EOF
}
# -------------------------------------------------
# MAIN
# -------------------------------------------------
DEVICES=()
TIMED_MINUTES=0
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
@ -258,13 +722,30 @@ parse_args() {
TIMED_MINUTES="$2"
shift 2
;;
start | stop | status | scan | list)
--hard)
HARD_MODE=1
shift
;;
-h | --help | help)
COMMAND="help"
shift
;;
start | stop | status | scan | list | add | remove | auto | restore)
if [[ -z "$COMMAND" || "$COMMAND" == "help" ]]; then
COMMAND="$1"
else
CMD_ARGS+=("$1")
fi
shift
;;
*)
echo "Unknown option: $1"
if [[ -z "$COMMAND" ]]; then
echo "Unknown option or command: $1" >&2
usage >&2
exit 1
fi
CMD_ARGS+=("$1")
shift
;;
esac
done
@ -272,77 +753,35 @@ parse_args() {
parse_args "$@"
if [[ -z "${COMMAND:-}" ]]; then
echo "Usage: $0 [-d|--device DEVICE] [-t|--timed MINUTES] {start|stop|status|scan|list}"
if [[ -z "$COMMAND" ]]; then
usage >&2
exit 1
fi
case "$COMMAND" in
start)
if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
nmcli connection up "$WIFI_5G"
fi
start_bluetooth || exit 1
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_start_all DEVICES[@]
else
bt_start_all BT_AUTO_DEVICES[@]
fi
if [[ "$TIMED_MINUTES" -gt 0 ]]; then
echo "Will disconnect after $TIMED_MINUTES minute(s)..."
sleep $((TIMED_MINUTES * 60))
echo "Time elapsed. Disconnecting..."
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_stop_all DEVICES[@]
else
bt_stop_all BT_AUTO_DEVICES[@]
fi
echo "Stopping Bluetooth service and cutting radio power..."
bluetoothctl power off > /dev/null 2>&1
systemctl_bt stop
systemctl_bt mask
rfkill_bt block
if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
nmcli connection up "$WIFI_24G"
fi
if [[ "$COMMAND" == "help" ]]; then
usage
exit 0
fi
;;
stop)
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_stop_all DEVICES[@]
else
bt_stop_all BT_AUTO_DEVICES[@]
if ! [[ "$TIMED_MINUTES" =~ ^[0-9]+$ ]]; then
echo "ERROR: --timed expects a number of minutes." >&2
exit 1
fi
echo "Stopping Bluetooth service and cutting radio power..."
bluetoothctl power off > /dev/null 2>&1
systemctl_bt stop
systemctl_bt mask
rfkill_bt block
if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
nmcli connection up "$WIFI_24G"
fi
case "$COMMAND" in
start | stop | status)
DEVICES+=("${CMD_ARGS[@]}")
;;
esac
status)
bluetoothctl show | grep -E "Name|Powered|Discoverable"
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_status_all DEVICES[@]
else
bt_status_all BT_AUTO_DEVICES[@]
fi
;;
config_load
scan | list)
# Keeping it brief in output, your previous implementations for scan/list stay intact here
echo "Command $COMMAND executed."
;;
case "$COMMAND" in
start) cmd_start ;;
stop) cmd_stop ;;
restore) cmd_restore ;;
status) cmd_status ;;
scan) cmd_scan ;;
list) cmd_list ;;
add) cmd_add ;;
remove) cmd_remove ;;
auto) cmd_auto ;;
esac

Loading…
Cancel
Save