refactored cli, extracted menu to cli, removed deprecated
Release / Build and Release (push) Has been cancelled

This commit is contained in:
2026-04-07 11:42:57 +02:00
parent ffdb689ef9
commit eaeba84092
7 changed files with 773 additions and 158 deletions
+135
View File
@@ -0,0 +1,135 @@
//! Bluetooth interactive menu (client-side).
//!
//! Runs entirely in the client process because it needs to spawn the user's
//! menu command (rofi/dmenu/wofi) — the daemon has no business opening GUI
//! windows. Communicates with the daemon via IPC to fetch device lists and
//! dispatch connect/disconnect/mode commands.
/// Format strings used both when *building* menu items and when *parsing*
/// the user's selection back. Keeping them together prevents drift.
mod fmt {
/// Connected device with a plugin mode: `"<alias>: Mode: <mode> [<mac>]"`.
pub const MODE_INFIX: &str = ": Mode: ";
/// Disconnect action: `"Disconnect <alias> [<mac>]"`.
pub const DISCONNECT_PREFIX: &str = "Disconnect ";
/// Visual separator before paired-but-not-connected devices.
pub const CONNECT_HEADER: &str = "--- Connect Device ---";
}
/// Extract a MAC address enclosed in `[…]` at the end of a string.
fn parse_mac_from_brackets(s: &str) -> Option<&str> {
let start = s.rfind('[')?;
let end = s.rfind(']')?;
if end > start + 1 {
Some(&s[start + 1..end])
} else {
None
}
}
/// Extract a MAC address enclosed in `(…)` at the end of a string.
fn parse_mac_from_parens(s: &str) -> Option<&str> {
let start = s.rfind('(')?;
let end = s.rfind(')')?;
if end > start + 1 {
Some(&s[start + 1..end])
} else {
None
}
}
/// Parse a mode selection line: `"<alias>: Mode: <mode> [<mac>]"`.
/// Returns `(mode, mac)`.
fn parse_mode_selection(s: &str) -> Option<(&str, &str)> {
let mac = parse_mac_from_brackets(s)?;
let mode_start = s.find(fmt::MODE_INFIX)?;
let mode_begin = mode_start + fmt::MODE_INFIX.len();
let bracket_start = s.rfind('[')?;
if bracket_start > mode_begin {
let mode = s[mode_begin..bracket_start].trim_end();
Some((mode, mac))
} else {
None
}
}
/// Run the interactive Bluetooth device menu.
///
/// Fetches connected/paired devices from the daemon, presents them in the
/// user's configured menu command, and dispatches the selected action back
/// to the daemon.
pub fn run_bt_menu() {
let config = crate::config::load_config(None);
let mut items = Vec::new();
let mut connected: Vec<(String, String)> = Vec::new();
let mut paired: Vec<(String, String)> = Vec::new();
// Fetch the device list from the daemon.
if let Ok(json_str) = crate::ipc::request_data("bt", &["menu_data"])
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str)
&& let Some(text) = val.get("text").and_then(|t| t.as_str())
{
for line in text.lines() {
if let Some(rest) = line.strip_prefix("CONNECTED:")
&& let Some((alias, mac)) = rest.split_once('|')
{
connected.push((alias.to_string(), mac.to_string()));
} else if let Some(rest) = line.strip_prefix("PAIRED:")
&& let Some((alias, mac)) = rest.split_once('|')
{
paired.push((alias.to_string(), mac.to_string()));
}
}
}
// Build menu items for connected devices (modes + disconnect).
for (alias, mac) in &connected {
if let Ok(json_str) = crate::ipc::request_data("bt", &["get_modes", mac])
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str)
&& let Some(modes_str) = val.get("text").and_then(|t| t.as_str())
&& !modes_str.is_empty()
{
for mode in modes_str.lines() {
items.push(format!("{}{}{} [{}]", alias, fmt::MODE_INFIX, mode, mac));
}
}
items.push(format!("{}{} [{}]", fmt::DISCONNECT_PREFIX, alias, mac));
}
// Paired-but-not-connected devices go below a separator.
if !paired.is_empty() {
items.push(fmt::CONNECT_HEADER.to_string());
for (alias, mac) in &paired {
items.push(format!("{} ({})", alias, mac));
}
}
if items.is_empty() {
tracing::info!("No Bluetooth options found.");
return;
}
let Ok(selected) = crate::utils::show_menu("BT Menu: ", &items, &config.general.menu_command)
else {
return;
};
if let Some((mode, mac)) = parse_mode_selection(&selected) {
crate::output::print_waybar_response(crate::ipc::request_data(
"bt",
&["set_mode", mode, mac],
));
} else if selected.starts_with(fmt::DISCONNECT_PREFIX) {
if let Some(mac) = parse_mac_from_brackets(&selected) {
crate::output::print_waybar_response(crate::ipc::request_data(
"bt",
&["disconnect", mac],
));
}
} else if selected == fmt::CONNECT_HEADER {
// Section header — no action.
} else if let Some(mac) = parse_mac_from_parens(&selected) {
crate::output::print_waybar_response(crate::ipc::request_data("bt", &["connect", mac]));
}
}