Compare commits

..

No commits in common. 'd16897f5428ddd163f530535b815a862bc8a627f' and '287d8453a75643f742080dbe8e781ff5bf83bd2f' have entirely different histories.

  1. 13
      .config/btswitch/devices.conf
  2. 34
      .config/nvim/lazy-lock.json
  3. 691
      .local/bin/btswitch
  4. 34
      .local/bin/cat-md

13
.config/btswitch/devices.conf

@ -1,13 +0,0 @@
# 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

34
.config/nvim/lazy-lock.json

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

691
.local/bin/btswitch

@ -7,37 +7,22 @@ set -u
WIFI_5G="cintra-5g" WIFI_5G="cintra-5g"
WIFI_24G="cintra-5g" WIFI_24G="cintra-5g"
BT_CONFIG_DIR="$HOME/.config/btswitch" declare -A BT_DEVICES
BT_CONFIG_FILE="$BT_CONFIG_DIR/devices.conf" 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_SERVICE="bluetooth" BT_SERVICE="bluetooth"
BT_RETRIES=6 BT_RETRIES=6
BT_DELAY=3 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 # Sentinel file to track that the sudoers rule was installed
BT_SUDOERS_SENTINEL="$BT_CONFIG_DIR/.sudoers_rule_installed" BT_SUDOERS_SENTINEL="$HOME/.config/btswitch/.sudoers_rule_installed"
declare -A BT_DEVICES
BT_AUTO_DEVICES=()
DEVICES=()
CMD_ARGS=()
COMMAND=""
TIMED_MINUTES=0
HARD_MODE=0
# ------------------------------------------------- # -------------------------------------------------
# HELPERS # HELPERS
@ -47,37 +32,26 @@ wifi_switch_needed() {
[[ -n "$1" && -n "$2" && "$1" != "$2" ]] [[ -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() { 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 if [[ -f "$BT_SUDOERS_SENTINEL" ]] && grep -q "^SUDOERS_RULE_VERSION=2" "$BT_SUDOERS_SENTINEL"; then
return 0 return 0
fi fi
cat >&2 << EOF cat >&2 << EOF
NOTE: 'stop --hard' (full system-wide shutdown) and 'restore' after it NOTE: To completely disable Bluetooth and prevent auto-reconnects, this
need rights to control the bluetooth service and rfkill. You will be script needs to use systemctl and rfkill. You will be prompted for your
prompted for your password ONCE to install a passwordless sudoers rule: password ONCE to update your passwordless sudoers rule:
/etc/sudoers.d/49-btswitch /etc/sudoers.d/49-btswitch
Regular start/stop/scan/list do NOT need sudo while Bluetooth is
already running.
EOF EOF
read -r -p "Install the sudoers rule now? [y/N] " answer read -r -p "Install the updated sudoers rule now? [y/N] " answer
case "$answer" in case "$answer" in
y | Y | yes | YES) ;; y | Y | yes | YES) ;;
*) *)
echo "Skipping. 'stop --hard' and 'restore' will not work without it." >&2 echo "Skipping. The script cannot control bluetooth completely without it." >&2
return 1 return 1
;; ;;
esac esac
@ -110,12 +84,12 @@ SUDOERS
systemctl_bt() { systemctl_bt() {
ensure_sudoers_rule || { ensure_sudoers_rule || {
echo "ERROR: Cannot control $BT_SERVICE.service without the sudoers rule." >&2 echo "ERROR: Cannot control bluetooth.service without the sudoers rule." >&2
exit 1 exit 1
} }
if ! sudo /usr/bin/systemctl "$@" "$BT_SERVICE"; then if ! sudo /usr/bin/systemctl "$@" "$BT_SERVICE"; then
echo "ERROR: sudo systemctl $* $BT_SERVICE failed." >&2 echo "ERROR: sudo systemctl $* $BT_SERVICE failed." >&2
return 1 exit 1
fi fi
} }
@ -124,20 +98,8 @@ rfkill_bt() {
sudo /usr/bin/rfkill "$1" bluetooth > /dev/null 2>&1 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() { bt_info() {
bt_service_active || return 1 bluetoothctl info "$1" 2> /dev/null
timeout 5 bluetoothctl info "$1" 2> /dev/null
} }
bt_is_paired() { bt_is_paired() {
@ -165,153 +127,30 @@ bt_resolve_device() {
fi 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
}
# ------------------------------------------------- # -------------------------------------------------
# DEVICE CONFIG FILE # BLUETOOTH CONTROL
# ------------------------------------------------- # -------------------------------------------------
config_seed() { start_bluetooth() {
mkdir -p "$BT_CONFIG_DIR" echo "Unblocking hardware radio..."
{ rfkill_bt unblock
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..."
# BLUETOOTH CONTROL systemctl_bt unmask
# ------------------------------------------------- systemctl_bt start
# Makes Bluetooth usable again (after a hard stop or if it was off). echo "Waiting for bluetoothd to become ready..."
# 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 for i in {1..10}; do
timeout 5 bluetoothctl power on > /dev/null 2>&1 if bluetoothctl show | grep -q "Powered: yes"; then
sleep 1 bluetoothctl pairable on > /dev/null 2>&1
if timeout 5 bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then
return 0 return 0
fi fi
bluetoothctl power on > /dev/null 2>&1
sleep 1
done done
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) echo "ERROR: Bluetooth adapter not found or could not be powered on."
bt_hard_off() { return 1
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() { bt_pair_device() {
@ -319,7 +158,7 @@ bt_pair_device() {
if bt_is_paired "$mac"; then if bt_is_paired "$mac"; then
return 0 return 0
fi fi
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 echo "✖ Device $mac is not paired. Please pair it manually first."
return 1 return 1
} }
@ -328,14 +167,11 @@ bt_ensure_connected() {
echo "----------------------------------------" echo "----------------------------------------"
echo "Checking device: $mac" echo "Checking device: $mac"
timeout 5 bluetoothctl unblock "$mac" > /dev/null 2>&1
if ! bt_is_trusted "$mac"; then if ! bt_is_trusted "$mac"; then
echo "→ Trusting $mac" echo "→ Trusting $mac"
timeout 5 bluetoothctl trust "$mac" > /dev/null 2>&1 bluetoothctl trust "$mac" > /dev/null
fi fi
local i
for ((i = 1; i <= BT_RETRIES; i++)); do for ((i = 1; i <= BT_RETRIES; i++)); do
if bt_is_connected "$mac"; then if bt_is_connected "$mac"; then
echo "✔ Connected: $mac" echo "✔ Connected: $mac"
@ -347,7 +183,7 @@ bt_ensure_connected() {
bt_pair_device "$mac" || return 1 bt_pair_device "$mac" || return 1
fi fi
timeout $((BT_DELAY * 4)) bluetoothctl connect "$mac" > /dev/null 2>&1 bluetoothctl connect "$mac" > /dev/null 2>&1
sleep "$BT_DELAY" sleep "$BT_DELAY"
if bt_is_connected "$mac"; then if bt_is_connected "$mac"; then
@ -360,63 +196,39 @@ bt_ensure_connected() {
return 1 return 1
} }
# Disconnects and blocks the device so it cannot auto-reconnect,
# while leaving Bluetooth running for everything else.
bt_safe_disconnect() { bt_safe_disconnect() {
local mac="$1" local mac="$1"
if ! bt_is_connected "$mac"; then
return 0
fi
echo "Disconnecting $mac..."
timeout 5 bluetoothctl disconnect "$mac" > /dev/null 2>&1
sleep 1
if bt_is_connected "$mac"; then if bt_is_connected "$mac"; then
echo "⚠ $mac is still connected." >&2 echo "Disconnecting $mac..."
return 0 bluetoothctl disconnect "$mac" > /dev/null 2>&1
fi fi
echo "Blocking $mac to prevent auto-reconnect ('btswitch start' unblocks it automatically)."
timeout 5 bluetoothctl block "$mac" > /dev/null 2>&1
} }
bt_start_all() { bt_stop_all() {
local dev mac failed=0 local devices=("${!1}")
while read -r dev; do for dev in "${devices[@]}"; do
[[ -n "$dev" ]] || continue local mac
mac=$(bt_resolve_device "$dev") mac=$(bt_resolve_device "$dev")
if [[ -z "$mac" ]]; then [[ -n "$mac" ]] && bt_safe_disconnect "$mac"
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 done
continue
fi
bt_ensure_connected "$mac" || failed=1
done < <(target_devices)
return "$failed"
} }
bt_stop_all() { bt_start_all() {
local dev mac local devices=("${!1}")
while read -r dev; do for dev in "${devices[@]}"; do
[[ -n "$dev" ]] || continue local mac
mac=$(bt_resolve_device "$dev") mac=$(bt_resolve_device "$dev")
if [[ -z "$mac" ]]; then [[ -n "$mac" ]] && bt_ensure_connected "$mac"
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 done
continue
fi
bt_safe_disconnect "$mac"
done < <(target_devices)
} }
bt_status_one() { bt_status_all() {
local dev="$1" mac local devices=("${!1}")
for dev in "${devices[@]}"; do
local mac
mac=$(bt_resolve_device "$dev") mac=$(bt_resolve_device "$dev")
if [[ -z "$mac" ]]; then if [[ -n "$mac" ]]; then
echo "⚠ Unknown device '$dev' (see 'btswitch list')" >&2 echo "--- $dev ($mac) ---"
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 if bt_is_connected "$mac"; then
echo "Status: CONNECTED" echo "Status: CONNECTED"
elif bt_is_paired "$mac"; then elif bt_is_paired "$mac"; then
@ -424,293 +236,17 @@ bt_status_one() {
else else
echo "Status: Not Paired / Unknown" echo "Status: Not Paired / Unknown"
fi 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 fi
BT_AUTO_DEVICES+=("$name")
;;
off)
local filtered=() dev
for dev in "${BT_AUTO_DEVICES[@]}"; do
[[ "$dev" != "$name" ]] && filtered+=("$dev")
done 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 # MAIN
# ------------------------------------------------- # -------------------------------------------------
DEVICES=()
TIMED_MINUTES=0
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -722,30 +258,13 @@ parse_args() {
TIMED_MINUTES="$2" TIMED_MINUTES="$2"
shift 2 shift 2
;; ;;
--hard) start | stop | status | scan | list)
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" COMMAND="$1"
else
CMD_ARGS+=("$1")
fi
shift shift
;; ;;
*) *)
if [[ -z "$COMMAND" ]]; then echo "Unknown option: $1"
echo "Unknown option or command: $1" >&2
usage >&2
exit 1 exit 1
fi
CMD_ARGS+=("$1")
shift
;; ;;
esac esac
done done
@ -753,35 +272,77 @@ parse_args() {
parse_args "$@" parse_args "$@"
if [[ -z "$COMMAND" ]]; then if [[ -z "${COMMAND:-}" ]]; then
usage >&2 echo "Usage: $0 [-d|--device DEVICE] [-t|--timed MINUTES] {start|stop|status|scan|list}"
exit 1 exit 1
fi fi
if [[ "$COMMAND" == "help" ]]; then
usage case "$COMMAND" in
exit 0 start)
if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
nmcli connection up "$WIFI_5G"
fi fi
if ! [[ "$TIMED_MINUTES" =~ ^[0-9]+$ ]]; then
echo "ERROR: --timed expects a number of minutes." >&2 start_bluetooth || exit 1
exit 1
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_start_all DEVICES[@]
else
bt_start_all BT_AUTO_DEVICES[@]
fi fi
case "$COMMAND" in if [[ "$TIMED_MINUTES" -gt 0 ]]; then
start | stop | status) echo "Will disconnect after $TIMED_MINUTES minute(s)..."
DEVICES+=("${CMD_ARGS[@]}") 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
fi
;; ;;
esac
config_load stop)
if [[ ${#DEVICES[@]} -gt 0 ]]; then
bt_stop_all DEVICES[@]
else
bt_stop_all BT_AUTO_DEVICES[@]
fi
case "$COMMAND" in echo "Stopping Bluetooth service and cutting radio power..."
start) cmd_start ;; bluetoothctl power off > /dev/null 2>&1
stop) cmd_stop ;; systemctl_bt stop
restore) cmd_restore ;; systemctl_bt mask
status) cmd_status ;; rfkill_bt block
scan) cmd_scan ;;
list) cmd_list ;; if wifi_switch_needed "$WIFI_5G" "$WIFI_24G"; then
add) cmd_add ;; nmcli connection up "$WIFI_24G"
remove) cmd_remove ;; fi
auto) cmd_auto ;; ;;
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
;;
scan | list)
# Keeping it brief in output, your previous implementations for scan/list stay intact here
echo "Command $COMMAND executed."
;;
esac esac

34
.local/bin/cat-md

@ -4,28 +4,16 @@ set -euo pipefail
# --- Defaults --- # --- Defaults ---
COPY=true COPY=true
CLIP_CMD=() CLIP_CMD=""
# --- Detect clipboard tool --- # --- Detect clipboard tool ---
detect_clipboard() { detect_clipboard() {
# Pick the right tool for the session:
# wl-copy on Wayland, xclip/xsel on X11. Fall back to whatever exists.
if [[ "${XDG_SESSION_TYPE:-}" == "wayland" || -n "${WAYLAND_DISPLAY:-}" ]]; then
if command -v wl-copy &> /dev/null; then
CLIP_CMD=(wl-copy)
elif command -v xclip &> /dev/null; then
CLIP_CMD=(xclip -selection clipboard)
elif command -v xsel &> /dev/null; then
CLIP_CMD=(xsel --clipboard --input)
fi
else
if command -v xclip &> /dev/null; then if command -v xclip &> /dev/null; then
CLIP_CMD=(xclip -selection clipboard) CLIP_CMD="xclip -selection clipboard"
elif command -v xsel &> /dev/null; then elif command -v xsel &> /dev/null; then
CLIP_CMD=(xsel --clipboard --input) CLIP_CMD="xsel --clipboard --input"
elif command -v wl-copy &> /dev/null; then else
CLIP_CMD=(wl-copy) CLIP_CMD=""
fi
fi fi
} }
@ -53,11 +41,10 @@ Examples:
cat-md file1.txt file2.txt cat-md file1.txt file2.txt
cat-md src/ cat-md src/
cat-md -n file.txt # no clipboard cat-md -n file.txt # no clipboard
cat-md -n file.txt | less # pipe manually cat-md file.txt | wl-copy # Wayland clipboard (manual override)
Requirements (one of): Requirements (one of):
wl-copy — Wayland clipboard (from wl-clipboard) xclip — X11 clipboard (preferred)
xclip — X11 clipboard (preferred on X11)
xsel — alternative X11 clipboard tool xsel — alternative X11 clipboard tool
EOF EOF
exit 0 exit 0
@ -97,8 +84,8 @@ fi
detect_clipboard detect_clipboard
if $COPY && [[ ${#CLIP_CMD[@]} -eq 0 ]]; then if $COPY && [[ -z "$CLIP_CMD" ]]; then
echo "Warning: no clipboard tool found (tried wl-copy, xclip, xsel). Output will go to stdout only." >&2 echo "Warning: neither xclip nor xsel found. Output will go to stdout only." >&2
COPY=false COPY=false
fi fi
@ -123,8 +110,9 @@ generate() {
} }
if $COPY; then if $COPY; then
generate "${ARGS[@]}" | "${CLIP_CMD[@]}" generate "${ARGS[@]}" | $CLIP_CMD
echo "Copied to clipboard." >&2 echo "Copied to clipboard." >&2
else else
generate "${ARGS[@]}" generate "${ARGS[@]}"
fi fi

Loading…
Cancel
Save