Skip to content

Commit

Permalink
2025/1/30 14:55
Browse files Browse the repository at this point in the history
  • Loading branch information
iKineticate committed Jan 30, 2025
1 parent 3dbd1bd commit def6bdc
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 50 deletions.
33 changes: 33 additions & 0 deletions README-en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Function

- [ ] Setting:设备电量作为托盘图标(字体或电池图标)
- [x] Setting:更新时间
- [ ] Setting:Auto start
- [x] Setting-notice:Mute notice
- [x] Setting-notice:Low battery notice
- [x] Setting-notice:Notification when reconnecting the device
- [x] Setting-notice:Notification when disconnecting the device
- [x] Setting-notice:Notification when adding a new device
- [x] Setting-notice:Notification when moving a new device

## Known Issues & Suggested Solutions

### 1. Currently, BlueGauge successfully retrieves battery levels from low-energy Bluetooth devices and Plug-and-Play (PnP) devices. However, we are unable to fetch the battery status from devices like AirPods and Xbox controllers, which operate on proprietary communication protocols.

**Solution:**

Welcome contributions from developers who can help us extend support for these devices.


### 2. The character length of tray tooltip is currently limited. When the tooltip text exceeds this limit, it gets truncated, which can result in incomplete device names being displayed. This can cause confusion for users, especially when multiple devices are connected.

**Solution:**

1. Limit Device Name Length: Implement a character limit for device names that ensures they fit within the available space of the tray notification. This may require shortening longer names to prevent truncation.

2. Hide Disconnected Devices: Consider not displaying disconnected devices in the tray notifications. This approach would reduce clutter and ensure that only relevant information is shown, thereby preventing text overflow.


### 3.When BlueGauge updates Bluetooth Information related to Bluetooth devices and sends notifications, if the tray menu is active (open), it can lead to the tray menu freezing. Currently considering a bug in the tray-icon library.

- Temporary Fix: Press `Ctrl + Shift + Esc` to open the `Task Manager`. Search for `BlueGauge.exe`. Select the process and click `End Task` to stop it.
55 changes: 37 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,41 @@ A lightweight tray tool for easily checking the battery level of your Bluetooth

![image](https://raw.githubusercontent.com/iKineticate/BlueGauge/main/screenshots/app.png)

<h3 align="center"> 简体中文 | <a href='./README-en.md'>English</a></h3>

- [x] 左键单击托盘显示通知
- [x] 支持非低功耗蓝牙设备(PnP设备)
- [ ] 左键点击托盘显示通知
- [ ] 菜单:自定义更新时间
- [ ] 菜单:添加开机启动
- [ ] 菜单:更新按钮
- [ ] 托盘图标替换为指定蓝牙设备的电量(数字或电池图标)
- [ ] 低电量通知(可选通知阈值)
- [ ] 定时通知指定已连接设备的电量
- [ ] 通知设置:可选静音及其它声音
- [ ] 通知设置:可选是否展示进度条


# 问题:
1. 托盘提示的长度受到限制
2. 托盘提示的行数受到限制
3. 使用PnP获取电量时,CPU使用率过高(≈12%)
4. 当更新托盘时,右键菜单会消失
## 功能

- [ ] 设置:设备电量作为托盘图标(字体或电池图标)
- [x] 设置:更新时间
- [ ] 设置:开机启动
- [x] 设置-通知:静音通知
- [x] 设置-通知:低电量通知
- [x] 设置-通知:重新连接设备通知
- [x] 设置-通知:断开连接设备通知
- [x] 设置-通知:新添加设备通知
- [x] 设置-通知:被移除设备通知

## 已知问题与建议

### 1. 无法获取某些设备电量信息

目前,BlueGauge 可检索蓝牙低功耗(BLE)设备和即插即用(PnP)设备的电量,但对于像 **AirPods****Xbox 控制器** 等使用专有通信协议的设备,可能无法获取电量信息。

**解决方案:**
欢迎有能力的开发者贡献代码,帮助扩展对这些设备的支持。

### 2. 托盘提示文本被截断

托盘提示的字符长度有限,当设备名称过长时,提示文本会被截断,导致无法完整显示设备名称。尤其在连接多个设备时,设备名称可能不完整。

**建议的解决办法:**

1. **设置设备名称长度限制**:对设备名称的字符长度进行限制,确保其在托盘通知区域内完整显示。

2. **隐藏未连接的设备**:对于未连接的设备,可以考虑不在托盘通知中显示,从而减少杂乱,避免文本溢出。

### 3. 当蓝牙设备的状态更新时,已显示的托盘菜单可能冻结

当设备重新或断开连接时,处于显示状态下的托盘菜单可能会冻结,导致用户无法操作菜单。目前考虑是tray-icon库的BUG。

- **临时解决方案**:按 `Ctrl + Shift + Esc` 打开 `任务管理器`,找到 `BlueGauge.exe` 进程,选择该进程并点击 `结束任务`
32 changes: 17 additions & 15 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use glob::glob;
pub struct Config {
pub update_interval: u64,
pub show_disconnected_devices: bool,
pub truncate_bluetooth_name: bool,
pub battery_prefix_name: bool,
pub icon: Option<ShowIcon>,
pub notify_mute: bool,
pub notify_low_battery: Option<u8>,
Expand Down Expand Up @@ -43,7 +45,9 @@ fn create_new_ini(ini_path: PathBuf) -> Result<(Config, PathBuf)> {
ini.with_section(Some("Settings"))
.set("update_interval", "30") // 默认30(单位秒)
.set("icon", "none") // Value: none、logo、ttf、battery_png(若为图标exe同一目录中存放*.png任一数量的照片,*的范围为0~100,要求每组照片宽高一致)
.set("show_disconnected_devices", "false");
.set("show_disconnected_devices", "false")
.set("truncate_bluetooth_name", "false")
.set("battery_prefix_name", "false");

ini.with_section(Some("Notifications"))
.set("notify_low_battery", "none") // Value:none、number(0~100,单位百分比)
Expand All @@ -59,6 +63,8 @@ fn create_new_ini(ini_path: PathBuf) -> Result<(Config, PathBuf)> {
update_interval: 30,
icon: None,
show_disconnected_devices: false,
truncate_bluetooth_name: false,
battery_prefix_name: false,
notify_low_battery: None,
notify_reconnection: false,
notify_disconnection: false,
Expand Down Expand Up @@ -87,6 +93,12 @@ fn read_ini(exe_dir: &Path, ini_path: PathBuf) -> Result<(Config, PathBuf)> {
let show_disconnected_devices = setting_section.get("show_disconnected_devices")
.map_or(false, |v| v.trim().to_lowercase() == "true");

let truncate_bluetooth_name = setting_section.get("truncate_bluetooth_name")
.map_or(false, |v| v.trim().to_lowercase() == "true");

let battery_prefix_name = setting_section.get("battery_prefix_name")
.map_or(false, |v| v.trim().to_lowercase() == "true");

let icon = setting_section.get("icon").map(|v| {
match v.trim().to_lowercase().as_str() {
"logo" => {
Expand Down Expand Up @@ -147,6 +159,8 @@ fn read_ini(exe_dir: &Path, ini_path: PathBuf) -> Result<(Config, PathBuf)> {
update_interval,
icon,
show_disconnected_devices,
truncate_bluetooth_name,
battery_prefix_name,
notify_low_battery,
notify_reconnection,
notify_disconnection,
Expand All @@ -158,26 +172,14 @@ fn read_ini(exe_dir: &Path, ini_path: PathBuf) -> Result<(Config, PathBuf)> {
Ok((config, ini_path))
}

pub fn write_ini_update_interval(ini_path: &Path, value: u64) {
let mut ini = Ini::load_from_file(ini_path).expect("Failed to load config.ini in BlueGauge.exe directory");
ini.set_to(Some("Settings"), "update_interval".to_owned(), value.to_string());
ini.write_to_file(ini_path).expect("Failed to write INI file");
}

pub fn write_ini_notifications(ini_path: &Path, key: &str, value: String) {
let mut ini = Ini::load_from_file(ini_path).expect("Failed to load config.ini in BlueGauge.exe directory");
ini.set_to(Some("Notifications"), key.to_owned(), value);
ini.write_to_file(ini_path).expect("Failed to write INI file");
}

pub fn write_ini_notify_low_battery(ini_path: &Path, value: u8) {
let mut ini = Ini::load_from_file(ini_path).expect("Failed to load config.ini in BlueGauge.exe directory");
ini.set_to(Some("Notifications"), "notify_low_battery".to_owned(), value.to_string());
ini.write_to_file(ini_path).expect("Failed to write INI file");
}

pub fn write_ini_show_disconnected(ini_path: &Path, value: String) {
pub fn write_ini_settings(ini_path: &Path, key: &str, value: String) {
let mut ini = Ini::load_from_file(ini_path).expect("Failed to load config.ini in BlueGauge.exe directory");
ini.set_to(Some("Settings"), "show_disconnected_devices".to_owned(), value);
ini.set_to(Some("Settings"), key.to_owned(), value);
ini.write_to_file(ini_path).expect("Failed to write INI file");
}
105 changes: 88 additions & 17 deletions src/systray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async fn loop_systray() -> Result<()> {

std::thread::sleep(std::time::Duration::from_secs(update_interval));

if let Ok(mut updated_in_advance) = updated_in_advance.lock() {
if let Ok(mut updated_in_advance) = updated_in_advance.try_lock() {
if std::mem::replace(&mut *updated_in_advance, false) {
continue;
}
Expand Down Expand Up @@ -197,18 +197,19 @@ async fn loop_systray() -> Result<()> {
// 如菜单ID可以格式化为u64,则菜单事件对应的是更新频率的设置
if let Ok(update_interval) = menu_id.trim().parse::<u64>() {
config.update_interval = update_interval;
write_ini_update_interval(&ini_path, update_interval);
write_ini_settings(&ini_path, "update_interval",update_interval.to_string());
if let Ok(mut update_menu_event) = update_menu_event.lock() {
if let Err(err) = proxy_menu.send_event(TrayEvent::ForwardUpdate) {
eprintln!("{err}")
} else {
*update_menu_event = true;
}
}
// 如菜单ID可以格式化为f64,则菜单事件对应的是设备低电量的设置
} else if let Ok(low_battery) = menu_id.trim().parse::<f64>() {
let low_battery = (low_battery * 100.0).floor().clamp(0.0, 99.0) as u8;
config.notify_low_battery = if low_battery == 0 { None } else { Some(low_battery) };
write_ini_notify_low_battery(&ini_path, low_battery);
write_ini_settings(&ini_path, "notify_low_battery",low_battery.to_string());
if let Ok(mut update_menu_event) = update_menu_event.lock() {
if let Err(err) = proxy_menu.send_event(TrayEvent::ForwardUpdate) {
eprintln!("{err}");
Expand Down Expand Up @@ -240,7 +241,41 @@ async fn loop_systray() -> Result<()> {
},
"show_disconnected_devices" => {
config.show_disconnected_devices = !config.show_disconnected_devices;
write_ini_show_disconnected(&ini_path, config.show_disconnected_devices.to_string());
write_ini_settings(
&ini_path,
"show_disconnected_devices",
config.show_disconnected_devices.to_string()
);
if let Ok(mut update_menu_event) = update_menu_event.lock() {
if let Err(err) = proxy_menu.send_event(TrayEvent::ForwardUpdate) {
eprintln!("{err}")
} else {
*update_menu_event = true;
}
}
},
"truncate_bluetooth_name" => {
config.truncate_bluetooth_name = !config.truncate_bluetooth_name;
write_ini_settings(
&ini_path,
"truncate_bluetooth_name",
config.truncate_bluetooth_name.to_string()
);
if let Ok(mut update_menu_event) = update_menu_event.lock() {
if let Err(err) = proxy_menu.send_event(TrayEvent::ForwardUpdate) {
eprintln!("{err}")
} else {
*update_menu_event = true;
}
}
},
"battery_prefix_name" => {
config.battery_prefix_name = !config.battery_prefix_name;
write_ini_settings(
&ini_path,
"battery_prefix_name",
config.battery_prefix_name.to_string()
);
if let Ok(mut update_menu_event) = update_menu_event.lock() {
if let Err(err) = proxy_menu.send_event(TrayEvent::ForwardUpdate) {
eprintln!("{err}")
Expand Down Expand Up @@ -429,19 +464,32 @@ async fn get_bluetooth_tray_info(config: Arc<Mutex<Config>>) -> Result<(Vec<Stri
.await
.map_err(|e| anyhow!("Failed to get bluetooth devices info - {e}"))?;
let show_disconnected_devices = config.lock().map_or(false, |c| c.show_disconnected_devices);
let (tooltip, menu_devices) = convert_tray_info(&bluetooth_devices_info, show_disconnected_devices);
let truncate_bluetooth_name = config.lock().map_or(false, |c| c.truncate_bluetooth_name);
let battery_prefix_name = config.lock().map_or(false, |c| c.battery_prefix_name);
let (tooltip, menu_devices) = convert_tray_info(
&bluetooth_devices_info,
show_disconnected_devices,
truncate_bluetooth_name,
battery_prefix_name,
);
Ok((tooltip, menu_devices, bluetooth_devices_info))
}

fn convert_tray_info(
bluetooth_devices_info: &HashSet<BluetoothInfo>,
show_disconnected_devices: bool,
truncate_bluetooth_name: bool,
battery_prefix_name: bool,
) -> (Vec<String>, Vec<String>) {
bluetooth_devices_info.iter().fold((Vec::new(), Vec::new()), |mut acc, blue_info| {
let name = truncate_with_ellipsis(&blue_info.name, 10);
let name = truncate_with_ellipsis(truncate_bluetooth_name, &blue_info.name, 10);
let battery = blue_info.battery;
let status_icon = if blue_info.status { "🟢" } else { "🔴" }; // { "[●]" } else { "[−]" }
let info = format!("{status_icon}{battery:3}% - {name}");
let status_icon = if blue_info.status { "🟢" } else { "🔴" };
let info = if battery_prefix_name {
format!("{status_icon}{battery:3}% - {name}")
} else {
format!("{status_icon}{name} - {battery:3}%")
};
match blue_info.status {
true => {
acc.0.insert(0, info);
Expand All @@ -459,8 +507,8 @@ fn convert_tray_info(
})
}

fn truncate_with_ellipsis(s: &str, max_chars: usize) -> String {
if s.chars().count() > max_chars {
fn truncate_with_ellipsis(truncate_bluetooth_name: bool, s: &str, max_chars: usize) -> String {
if truncate_bluetooth_name && s.chars().count() > max_chars {
let mut result = s.chars().take(max_chars).collect::<String>();
result.push_str("...");
result
Expand All @@ -478,12 +526,28 @@ fn create_tray_menu(menu_devices: &Vec<String>, config: &Config) -> Result<Menu>

let menu_show_disconnected_devices = CheckMenuItem::with_id(
"show_disconnected_devices",
"Show Disconnected",
"Show Disconnected Devices",
true,
config.show_disconnected_devices,
None
);

let truncate_bluetooth_name = CheckMenuItem::with_id(
"truncate_bluetooth_name",
"Truncate Device Name",
true,
config.truncate_bluetooth_name,
None
);

let battery_prefix_name = CheckMenuItem::with_id(
"battery_prefix_name",
"Battery Prefix Name",
true,
config.battery_prefix_name,
None
);

let update_items = &[
&CheckMenuItem::with_id("15", "15s", true, config.update_interval == 15, None) as &dyn IsMenuItem,
&CheckMenuItem::with_id("30", "30s", true, config.update_interval == 30, None) as &dyn IsMenuItem,
Expand All @@ -510,6 +574,7 @@ fn create_tray_menu(menu_devices: &Vec<String>, config: &Config) -> Result<Menu>
&CheckMenuItem::with_id("0.25", "25%", true, low_battery.map_or(false, |v| v == 25), None) as &dyn IsMenuItem,
];
let notify_low_battery = Submenu::with_items("Low Battery", true, low_battery_items)?;

let notify_items = &[
&CheckMenuItem::with_id("notify_mute", "Notify Silently", true, config.notify_mute, None) as &dyn IsMenuItem,
&notify_low_battery as &dyn IsMenuItem,
Expand All @@ -518,7 +583,6 @@ fn create_tray_menu(menu_devices: &Vec<String>, config: &Config) -> Result<Menu>
&CheckMenuItem::with_id("notify_added_devices", "New Devices", true, config.notify_added_devices, None) as &dyn IsMenuItem,
&CheckMenuItem::with_id("notify_remove_devices", "Remove Devices", true, config.notify_remove_devices, None) as &dyn IsMenuItem,
];

let menu_notify = Submenu::with_items("Notifications", true, notify_items)?;

let menu_about = PredefinedMenuItem::about(
Expand All @@ -531,16 +595,23 @@ fn create_tray_menu(menu_devices: &Vec<String>, config: &Config) -> Result<Menu>
..Default::default()
}));

let settings_items = &[
&menu_show_disconnected_devices as &dyn IsMenuItem,
&truncate_bluetooth_name as &dyn IsMenuItem,
&battery_prefix_name as &dyn IsMenuItem,
&menu_update as &dyn IsMenuItem,
&menu_notify as &dyn IsMenuItem,
];

let menu_setting = Submenu::with_items("Settings", true, settings_items)?;

for text in menu_devices {
let item = CheckMenuItem::with_id(text, text, true, false, None);
tray_menu.append(&item).map_err(|_| anyhow!("Failed to append 'Devices' to Tray Menu"))?;
}

tray_menu.append(&menu_separator).context("Failed to apped 'Separator' to Tray Menu")?;
tray_menu.append(&menu_show_disconnected_devices).context("Failed to apped 'Separator' to Tray Menu")?;
tray_menu.append(&menu_separator).context("Failed to apped 'Separator' to Tray Menu")?;
tray_menu.append(&menu_update).context("Failed to apped 'Update Interval' to Tray Menu")?;
tray_menu.append(&menu_separator).context("Failed to apped 'Separator' to Tray Menu")?;
tray_menu.append(&menu_notify).context("Failed to apped 'Update Interval' to Tray Menu")?;
tray_menu.append(&menu_setting).context("Failed to apped 'Update Interval' to Tray Menu")?;
tray_menu.append(&menu_separator).context("Failed to apped 'Separator' to Tray Menu")?;
tray_menu.append(&menu_about).context("Failed to apped 'About' to Tray Menu")?;
tray_menu.append(&menu_separator).context("Failed to apped 'Separator' to Tray Menu")?;
Expand Down

0 comments on commit def6bdc

Please sign in to comment.