mirror of
https://github.com/xzeldon/razer-battery-report.git
synced 2025-07-17 00:06:15 +03:00
Compare commits
12 Commits
2c8e4da552
...
v0.2.4
Author | SHA1 | Date | |
---|---|---|---|
f38b046b54
|
|||
66e6a5bbd5
|
|||
a418a73d3e
|
|||
21f631f68c
|
|||
f5349a56a5
|
|||
2c0e62016b
|
|||
65429b01f5
|
|||
ce2cd46055
|
|||
8577920504
|
|||
fc7dd809fe
|
|||
805ba16c2c
|
|||
d9f92c97f4
|
58
.github/workflows/release.yml
vendored
Normal file
58
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
name: Build and Publish Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
paths-ignore:
|
||||||
|
- "**.md"
|
||||||
|
- .github/workflows/build-and-release.yml
|
||||||
|
- .gitignore
|
||||||
|
- LICENSE
|
||||||
|
- img/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
name: Build and Publish Release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v4.1.0
|
||||||
|
|
||||||
|
- name: Setup workflow cache
|
||||||
|
uses: actions/cache@v4.1.0
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
target/
|
||||||
|
key: windows-cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Setup Rust stable toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Upload workflow artifact
|
||||||
|
uses: actions/upload-artifact@v4.1.0
|
||||||
|
with:
|
||||||
|
name: razer-battery-report
|
||||||
|
path: ./target/x86_64-pc-windows-msvc/release/razer-battery-report.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Publish Release
|
||||||
|
uses: softprops/action-gh-release@v2.1.0
|
||||||
|
with:
|
||||||
|
files: ./target/x86_64-pc-windows-msvc/release/razer-battery-report.exe
|
||||||
|
draft: true
|
||||||
|
fail_on_unmatched_files: true
|
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler"
|
name = "adler"
|
||||||
@ -1813,11 +1813,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "razer-battery-report"
|
name = "razer-battery-report"
|
||||||
version = "0.2.2"
|
version = "0.2.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hidapi",
|
"hidapi",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
"parking_lot",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"tao",
|
"tao",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "razer-battery-report"
|
name = "razer-battery-report"
|
||||||
version = "0.2.2"
|
version = "0.2.4"
|
||||||
authors = ["xzeldon <contact@zeldon.ru>"]
|
authors = ["xzeldon <contact@zeldon.ru>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Razer Battery Level Tray Indicator"
|
description = "Razer Battery Level Tray Indicator"
|
||||||
@ -11,6 +11,13 @@ lto = "fat"
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
# Faster builds, slower executables
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 0
|
||||||
|
lto = false
|
||||||
|
incremental = true
|
||||||
|
codegen-units = 16
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Communicate with HID devices
|
# Communicate with HID devices
|
||||||
hidapi = "2.6.3"
|
hidapi = "2.6.3"
|
||||||
@ -28,3 +35,6 @@ image = "0.25.2"
|
|||||||
|
|
||||||
# Windows API
|
# Windows API
|
||||||
winapi = { version = "0.3.9", features = ["winuser", "wincon", "consoleapi"] }
|
winapi = { version = "0.3.9", features = ["winuser", "wincon", "consoleapi"] }
|
||||||
|
|
||||||
|
# Efficient synchronization primitives (e.g. Mutex, RwLock and etc.)
|
||||||
|
parking_lot = "0.12"
|
||||||
|
14
README.md
14
README.md
@ -10,15 +10,17 @@
|
|||||||
|
|
||||||
Show your wireless Razer devices battery levels in your system tray.
|
Show your wireless Razer devices battery levels in your system tray.
|
||||||
|
|
||||||
> This is a work in progress and currently support only **Razer DeathAdder V3 Pro**.
|
> This is a work in progress and currently support only **Razer DeathAdder V3 Pro** and **Razer DeathAdder V3 HyperSpeed**.
|
||||||
|
|
||||||
> Currently, this works only on **Windows**, should work on **Linux** if you _add udev rule to get access to usb devices_ (see [here](https://github.com/libusb/hidapi/blob/master/udev/69-hid.rules)) and remove/`cfg(windows)` some platform-specific code. But I haven't tested yet.
|
> Currently, this works only on **Windows**.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Downloading a Prebuilt Binary
|
### Installation
|
||||||
|
|
||||||
> _Todo_
|
1. Download `razer-battery-report.exe` from [latest release](https://github.com/xzeldon/razer-battery-report/releases/latest)
|
||||||
|
2. Run `razer-battery-report.exe`
|
||||||
|
3. If you want a start menu shortcut you can make one yourself! Simply right-click `razer-battery-report.exe` and select "Pin to Start". This will automatically create a shortcut in %appdata%\Microsoft\Windows\Start Menu\Programs.
|
||||||
|
|
||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
@ -43,8 +45,8 @@ To build, you must have [Rust](https://www.rust-lang.org/) and
|
|||||||
- [ ] Force update devices button in tray menu
|
- [ ] Force update devices button in tray menu
|
||||||
- [ ] Colored tray icons for different battery levels
|
- [ ] Colored tray icons for different battery levels
|
||||||
- [x] Show log window button in tray menu
|
- [x] Show log window button in tray menu
|
||||||
- [ ] Further reduce CPU usage by using Event Loop Proxy events (more info [here](https://github.com/tauri-apps/tray-icon/issues/83#issuecomment-1697773065))
|
- [x] Further reduce CPU usage by using Event Loop Proxy events (more info [here](https://github.com/tauri-apps/tray-icon/issues/83#issuecomment-1697773065))
|
||||||
- [ ] Prebuilt Binary
|
- [x] Prebuilt Binary
|
||||||
- [ ] Command Line Arguments for update frequency
|
- [ ] Command Line Arguments for update frequency
|
||||||
- [ ] Support for other Razer Devices (I only have DeathAdder V3 Pro, so I won't be able to test it with other devices)
|
- [ ] Support for other Razer Devices (I only have DeathAdder V3 Pro, so I won't be able to test it with other devices)
|
||||||
|
|
||||||
|
58
src/console.rs
Normal file
58
src/console.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{ffi::OsStr, os::windows::ffi::OsStrExt, sync::Arc};
|
||||||
|
use winapi::um::{consoleapi, wincon, winuser};
|
||||||
|
|
||||||
|
pub struct DebugConsole {
|
||||||
|
hwnd: *mut winapi::shared::windef::HWND__,
|
||||||
|
visible: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugConsole {
|
||||||
|
pub fn new(title: &str) -> Self {
|
||||||
|
unsafe {
|
||||||
|
consoleapi::AllocConsole();
|
||||||
|
|
||||||
|
let title: Vec<u16> = OsStr::new(title)
|
||||||
|
.encode_wide()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect();
|
||||||
|
wincon::SetConsoleTitleW(title.as_ptr());
|
||||||
|
|
||||||
|
let hwnd = wincon::GetConsoleWindow();
|
||||||
|
|
||||||
|
if !hwnd.is_null() {
|
||||||
|
let hmenu = winuser::GetSystemMenu(hwnd, 0);
|
||||||
|
if !hmenu.is_null() {
|
||||||
|
winuser::DeleteMenu(hmenu, winuser::SC_CLOSE as u32, winuser::MF_BYCOMMAND);
|
||||||
|
}
|
||||||
|
winuser::ShowWindow(hwnd, winuser::SW_HIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
hwnd,
|
||||||
|
visible: Arc::new(Mutex::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle_visibility(&self) {
|
||||||
|
if !self.hwnd.is_null() {
|
||||||
|
let mut visible = self.visible.lock();
|
||||||
|
*visible = !*visible;
|
||||||
|
unsafe {
|
||||||
|
winuser::ShowWindow(
|
||||||
|
self.hwnd,
|
||||||
|
if *visible {
|
||||||
|
winuser::SW_SHOW
|
||||||
|
} else {
|
||||||
|
winuser::SW_HIDE
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_visible(&self) -> bool {
|
||||||
|
*self.visible.lock()
|
||||||
|
}
|
||||||
|
}
|
@ -28,7 +28,9 @@ impl DeviceInfo {
|
|||||||
pub const fn transaction_id(&self) -> u8 {
|
pub const fn transaction_id(&self) -> u8 {
|
||||||
match self.pid {
|
match self.pid {
|
||||||
pid if pid == RAZER_DEATHADDER_V3_PRO_WIRED.pid
|
pid if pid == RAZER_DEATHADDER_V3_PRO_WIRED.pid
|
||||||
|| pid == RAZER_DEATHADDER_V3_PRO_WIRELESS.pid =>
|
|| pid == RAZER_DEATHADDER_V3_PRO_WIRELESS.pid
|
||||||
|
|| pid == RAZER_DEATHADDER_V3_HYPERSPEED_WIRED.pid
|
||||||
|
|| pid == RAZER_DEATHADDER_V3_HYPERSPEED_WIRELESS.pid =>
|
||||||
{
|
{
|
||||||
0x1F
|
0x1F
|
||||||
}
|
}
|
||||||
@ -38,11 +40,18 @@ impl DeviceInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const RAZER_DEATHADDER_V3_PRO_WIRED: DeviceInfo =
|
pub const RAZER_DEATHADDER_V3_PRO_WIRED: DeviceInfo =
|
||||||
DeviceInfo::new("Razer DeathAdder V3 Pro", 0x00B6, 0, 1, 2);
|
DeviceInfo::new("Razer DeathAdder V3 Pro (Wired)", 0x00B6, 0, 1, 2);
|
||||||
pub const RAZER_DEATHADDER_V3_PRO_WIRELESS: DeviceInfo =
|
pub const RAZER_DEATHADDER_V3_PRO_WIRELESS: DeviceInfo =
|
||||||
DeviceInfo::new("Razer DeathAdder V3 Pro", 0x00B7, 0, 1, 2);
|
DeviceInfo::new("Razer DeathAdder V3 Pro (Wireless)", 0x00B7, 0, 1, 2);
|
||||||
|
|
||||||
pub const RAZER_DEVICE_LIST: [DeviceInfo; 2] = [
|
pub const RAZER_DEATHADDER_V3_HYPERSPEED_WIRED: DeviceInfo =
|
||||||
|
DeviceInfo::new("Razer DeathAdder V3 HyperSpeed (Wired)", 0x00C4, 0, 1, 2);
|
||||||
|
pub const RAZER_DEATHADDER_V3_HYPERSPEED_WIRELESS: DeviceInfo =
|
||||||
|
DeviceInfo::new("Razer DeathAdder V3 HyperSpeed (Wireless)", 0x00C5, 0, 1, 2);
|
||||||
|
|
||||||
|
pub const RAZER_DEVICE_LIST: [DeviceInfo; 4] = [
|
||||||
RAZER_DEATHADDER_V3_PRO_WIRED,
|
RAZER_DEATHADDER_V3_PRO_WIRED,
|
||||||
RAZER_DEATHADDER_V3_PRO_WIRELESS,
|
RAZER_DEATHADDER_V3_PRO_WIRELESS,
|
||||||
|
RAZER_DEATHADDER_V3_HYPERSPEED_WIRED,
|
||||||
|
RAZER_DEATHADDER_V3_HYPERSPEED_WIRELESS,
|
||||||
];
|
];
|
||||||
|
25
src/main.rs
25
src/main.rs
@ -1,35 +1,20 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
|
use console::DebugConsole;
|
||||||
use tray::TrayApp;
|
use tray::TrayApp;
|
||||||
use winapi::um::{self, wincon, winuser};
|
|
||||||
|
|
||||||
|
mod console;
|
||||||
mod controller;
|
mod controller;
|
||||||
mod devices;
|
mod devices;
|
||||||
mod manager;
|
mod manager;
|
||||||
mod tray;
|
mod tray;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
unsafe {
|
let console = DebugConsole::new("Razer Battery Report Debug Console");
|
||||||
// Allocate new console for the process
|
|
||||||
um::consoleapi::AllocConsole();
|
|
||||||
|
|
||||||
// Modify the console window's style to remove the system menu (close, minimize, etc.).
|
|
||||||
winuser::SetWindowLongPtrW(
|
|
||||||
wincon::GetConsoleWindow(),
|
|
||||||
winuser::GWL_STYLE,
|
|
||||||
#[allow(clippy::cast_possible_wrap)]
|
|
||||||
{
|
|
||||||
winuser::GetWindowLongPtrW(wincon::GetConsoleWindow(), winuser::GWL_STYLE)
|
|
||||||
& !winuser::WS_SYSMENU as isize
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hide the console window
|
|
||||||
winuser::ShowWindow(wincon::GetConsoleWindow(), winuser::SW_HIDE);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::env::set_var("RUST_LOG", "trace");
|
std::env::set_var("RUST_LOG", "trace");
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
let checker = TrayApp::new();
|
|
||||||
|
let checker = TrayApp::new(console);
|
||||||
checker.run();
|
checker.run();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use hidapi::HidApi;
|
use hidapi::HidApi;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use parking_lot::Mutex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
|
||||||
use crate::controller::DeviceController;
|
use crate::controller::DeviceController;
|
||||||
@ -21,7 +22,7 @@ impl DeviceManager {
|
|||||||
|
|
||||||
pub fn fetch_devices(&mut self) -> (Vec<u32>, Vec<u32>) {
|
pub fn fetch_devices(&mut self) -> (Vec<u32>, Vec<u32>) {
|
||||||
let old_ids: HashSet<u32> = {
|
let old_ids: HashSet<u32> = {
|
||||||
let controllers = self.device_controllers.lock().unwrap();
|
let controllers = self.device_controllers.lock();
|
||||||
controllers
|
controllers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|controller| controller.pid as u32)
|
.map(|controller| controller.pid as u32)
|
||||||
@ -37,13 +38,13 @@ impl DeviceManager {
|
|||||||
let removed_devices: Vec<u32> = old_ids.difference(&new_ids).cloned().collect();
|
let removed_devices: Vec<u32> = old_ids.difference(&new_ids).cloned().collect();
|
||||||
let connected_devices: Vec<u32> = new_ids.difference(&old_ids).cloned().collect();
|
let connected_devices: Vec<u32> = new_ids.difference(&old_ids).cloned().collect();
|
||||||
|
|
||||||
*self.device_controllers.lock().unwrap() = new_controllers;
|
*self.device_controllers.lock() = new_controllers;
|
||||||
|
|
||||||
(removed_devices, connected_devices)
|
(removed_devices, connected_devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_device_name(&self, id: u32) -> Option<String> {
|
pub fn get_device_name(&self, id: u32) -> Option<String> {
|
||||||
let controllers = self.device_controllers.lock().unwrap();
|
let controllers = self.device_controllers.lock();
|
||||||
controllers
|
controllers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|controller| controller.pid as u32 == id)
|
.find(|controller| controller.pid as u32 == id)
|
||||||
@ -51,7 +52,7 @@ impl DeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_device_battery_level(&self, id: u32) -> Option<i32> {
|
pub fn get_device_battery_level(&self, id: u32) -> Option<i32> {
|
||||||
let controllers = self.device_controllers.lock().unwrap();
|
let controllers = self.device_controllers.lock();
|
||||||
let controller = controllers
|
let controller = controllers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|controller| controller.pid as u32 == id)?;
|
.find(|controller| controller.pid as u32 == id)?;
|
||||||
@ -66,7 +67,7 @@ impl DeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_device_charging(&self, id: u32) -> Option<bool> {
|
pub fn is_device_charging(&self, id: u32) -> Option<bool> {
|
||||||
let controllers = self.device_controllers.lock().unwrap();
|
let controllers = self.device_controllers.lock();
|
||||||
let controller = controllers
|
let controller = controllers
|
||||||
.iter()
|
.iter()
|
||||||
.find(|controller| controller.pid as u32 == id)?;
|
.find(|controller| controller.pid as u32 == id)?;
|
||||||
|
158
src/tray.rs
158
src/tray.rs
@ -1,24 +1,20 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
rc::Rc,
|
sync::Arc,
|
||||||
sync::{mpsc, Arc, Mutex},
|
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, Instant},
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::manager::DeviceManager;
|
use crate::{console::DebugConsole, manager::DeviceManager};
|
||||||
use log::{error, info, trace};
|
use log::{error, info, trace};
|
||||||
use tao::event_loop::EventLoopBuilder;
|
use parking_lot::Mutex;
|
||||||
|
use tao::event_loop::{EventLoopBuilder, EventLoopProxy};
|
||||||
use tray_icon::{
|
use tray_icon::{
|
||||||
menu::{Menu, MenuEvent, MenuItem},
|
menu::{IsMenuItem, Menu, MenuEvent, MenuItem},
|
||||||
TrayIcon, TrayIconBuilder,
|
TrayIcon, TrayIconBuilder,
|
||||||
};
|
};
|
||||||
use winapi::um::{
|
|
||||||
wincon::GetConsoleWindow,
|
|
||||||
winuser::{self, ShowWindow},
|
|
||||||
};
|
|
||||||
|
|
||||||
const BATTERY_UPDATE_INTERVAL: Duration = Duration::from_secs(60);
|
const BATTERY_UPDATE_INTERVAL: Duration = Duration::from_secs(300); // 5 min
|
||||||
const DEVICE_FETCH_INTERVAL: Duration = Duration::from_secs(5);
|
const DEVICE_FETCH_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -44,17 +40,17 @@ impl MemoryDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct TrayInner {
|
pub struct TrayInner {
|
||||||
tray_icon: Rc<Mutex<Option<TrayIcon>>>,
|
tray_icon: Arc<Mutex<Option<TrayIcon>>>,
|
||||||
console_state: Arc<Mutex<bool>>,
|
|
||||||
menu_items: Arc<Mutex<Vec<MenuItem>>>,
|
menu_items: Arc<Mutex<Vec<MenuItem>>>,
|
||||||
|
debug_console: Arc<DebugConsole>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TrayInner {
|
impl TrayInner {
|
||||||
fn new() -> Self {
|
fn new(debug_console: Arc<DebugConsole>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tray_icon: Rc::new(Mutex::new(None)),
|
tray_icon: Arc::new(Mutex::new(None)),
|
||||||
console_state: Arc::new(Mutex::new(false)),
|
|
||||||
menu_items: Arc::new(Mutex::new(Vec::new())),
|
menu_items: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
debug_console,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,18 +60,23 @@ impl TrayInner {
|
|||||||
let show_console_item = MenuItem::new("Show Log Window", true, None);
|
let show_console_item = MenuItem::new("Show Log Window", true, None);
|
||||||
let quit_item = MenuItem::new("Exit", true, None);
|
let quit_item = MenuItem::new("Exit", true, None);
|
||||||
|
|
||||||
let mut menu_items = self.menu_items.lock().unwrap();
|
let mut menu_items = self.menu_items.lock();
|
||||||
menu_items.push(show_console_item);
|
menu_items.push(show_console_item);
|
||||||
menu_items.push(quit_item);
|
menu_items.push(quit_item);
|
||||||
|
|
||||||
|
let item_refs: Vec<&dyn IsMenuItem> = menu_items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item as &dyn IsMenuItem)
|
||||||
|
.collect();
|
||||||
|
|
||||||
tray_menu
|
tray_menu
|
||||||
.append_items(&[&menu_items[0], &menu_items[1]])
|
.append_items(&item_refs)
|
||||||
.unwrap();
|
.expect("Failed to append menu items");
|
||||||
tray_menu
|
tray_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_tray(
|
fn build_tray(
|
||||||
tray_icon: &Rc<Mutex<Option<TrayIcon>>>,
|
tray_icon: &Arc<Mutex<Option<TrayIcon>>>,
|
||||||
tray_menu: &Menu,
|
tray_menu: &Menu,
|
||||||
icon: tray_icon::Icon,
|
icon: tray_icon::Icon,
|
||||||
) {
|
) {
|
||||||
@ -86,7 +87,7 @@ impl TrayInner {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
match tray_builder {
|
match tray_builder {
|
||||||
Ok(tray) => *tray_icon.lock().unwrap() = Some(tray),
|
Ok(tray) => *tray_icon.lock() = Some(tray),
|
||||||
Err(err) => error!("Failed to create tray icon: {}", err),
|
Err(err) => error!("Failed to create tray icon: {}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,26 +99,32 @@ pub struct TrayApp {
|
|||||||
tray_inner: TrayInner,
|
tray_inner: TrayInner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum TrayEvent {
|
||||||
|
DeviceUpdate(Vec<u32>),
|
||||||
|
MenuEvent(MenuEvent),
|
||||||
|
}
|
||||||
|
|
||||||
impl TrayApp {
|
impl TrayApp {
|
||||||
pub fn new() -> Self {
|
pub fn new(debug_console: DebugConsole) -> Self {
|
||||||
Self {
|
Self {
|
||||||
device_manager: Arc::new(Mutex::new(DeviceManager::new())),
|
device_manager: Arc::new(Mutex::new(DeviceManager::new())),
|
||||||
devices: Arc::new(Mutex::new(HashMap::new())),
|
devices: Arc::new(Mutex::new(HashMap::new())),
|
||||||
tray_inner: TrayInner::new(),
|
tray_inner: TrayInner::new(Arc::new(debug_console)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&self) {
|
pub fn run(&self) {
|
||||||
let icon = Self::create_icon();
|
let icon = Self::create_icon();
|
||||||
let event_loop = EventLoopBuilder::new().build();
|
let event_loop = EventLoopBuilder::with_user_event().build();
|
||||||
let tray_menu = self.tray_inner.create_menu();
|
let tray_menu = self.tray_inner.create_menu();
|
||||||
|
|
||||||
let (sender, receiver) = mpsc::channel();
|
let proxy = event_loop.create_proxy();
|
||||||
|
|
||||||
self.spawn_device_fetch_thread(sender.clone());
|
self.spawn_device_fetch_thread(proxy.clone());
|
||||||
self.spawn_battery_check_thread(sender);
|
self.spawn_battery_check_thread(proxy.clone());
|
||||||
|
|
||||||
self.run_event_loop(event_loop, icon, tray_menu, receiver);
|
self.run_event_loop(event_loop, icon, tray_menu, proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_icon() -> tray_icon::Icon {
|
fn create_icon() -> tray_icon::Icon {
|
||||||
@ -131,18 +138,19 @@ impl TrayApp {
|
|||||||
tray_icon::Icon::from_rgba(rgba, width, height).expect("Failed to create icon")
|
tray_icon::Icon::from_rgba(rgba, width, height).expect("Failed to create icon")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_device_fetch_thread(&self, tx: mpsc::Sender<Vec<u32>>) {
|
fn spawn_device_fetch_thread(&self, proxy: EventLoopProxy<TrayEvent>) {
|
||||||
let devices = Arc::clone(&self.devices);
|
let devices = Arc::clone(&self.devices);
|
||||||
let device_manager = Arc::clone(&self.device_manager);
|
let device_manager = Arc::clone(&self.device_manager);
|
||||||
|
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || {
|
||||||
|
let mut last_devices = HashSet::new();
|
||||||
|
loop {
|
||||||
let (removed_devices, connected_devices) = {
|
let (removed_devices, connected_devices) = {
|
||||||
let mut manager = device_manager.lock().unwrap();
|
let mut manager = device_manager.lock();
|
||||||
manager.fetch_devices()
|
manager.fetch_devices()
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
let mut devices = devices.lock();
|
||||||
let mut devices = devices.lock().unwrap();
|
|
||||||
for id in removed_devices {
|
for id in removed_devices {
|
||||||
if let Some(device) = devices.remove(&id) {
|
if let Some(device) = devices.remove(&id) {
|
||||||
info!("Device removed: {}", device.name);
|
info!("Device removed: {}", device.name);
|
||||||
@ -150,53 +158,52 @@ impl TrayApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for &id in &connected_devices {
|
for &id in &connected_devices {
|
||||||
if let std::collections::hash_map::Entry::Vacant(e) = devices.entry(id) {
|
if !devices.contains_key(&id) {
|
||||||
if let Some(name) = device_manager.lock().unwrap().get_device_name(id) {
|
if let Some(name) = device_manager.lock().get_device_name(id) {
|
||||||
e.insert(MemoryDevice::new(name.clone(), id));
|
devices.insert(id, MemoryDevice::new(name.clone(), id));
|
||||||
info!("New device: {}", name);
|
info!("New device: {}", name);
|
||||||
} else {
|
} else {
|
||||||
error!("Failed to get device name for id: {}", id);
|
error!("Failed to get device name for id: {}", id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !connected_devices.is_empty() {
|
let current_devices: HashSet<_> = connected_devices.iter().cloned().collect();
|
||||||
tx.send(connected_devices).unwrap();
|
if current_devices != last_devices {
|
||||||
|
let _ = proxy.send_event(TrayEvent::DeviceUpdate(connected_devices));
|
||||||
|
last_devices = current_devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::sleep(DEVICE_FETCH_INTERVAL);
|
thread::sleep(DEVICE_FETCH_INTERVAL);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_battery_check_thread(&self, tx: mpsc::Sender<Vec<u32>>) {
|
fn spawn_battery_check_thread(&self, proxy: EventLoopProxy<TrayEvent>) {
|
||||||
let devices = Arc::clone(&self.devices);
|
let devices = Arc::clone(&self.devices);
|
||||||
|
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || loop {
|
||||||
let device_ids: Vec<u32> = devices.lock().unwrap().keys().cloned().collect();
|
let device_ids: Vec<u32> = devices.lock().keys().cloned().collect();
|
||||||
tx.send(device_ids).unwrap();
|
let _ = proxy.send_event(TrayEvent::DeviceUpdate(device_ids));
|
||||||
thread::sleep(BATTERY_UPDATE_INTERVAL);
|
thread::sleep(BATTERY_UPDATE_INTERVAL);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_event_loop(
|
fn run_event_loop(
|
||||||
&self,
|
&self,
|
||||||
event_loop: tao::event_loop::EventLoop<()>,
|
event_loop: tao::event_loop::EventLoop<TrayEvent>,
|
||||||
icon: tray_icon::Icon,
|
icon: tray_icon::Icon,
|
||||||
tray_menu: Menu,
|
tray_menu: Menu,
|
||||||
rx: mpsc::Receiver<Vec<u32>>,
|
proxy: EventLoopProxy<TrayEvent>,
|
||||||
) {
|
) {
|
||||||
let devices = Arc::clone(&self.devices);
|
let devices = Arc::clone(&self.devices);
|
||||||
let device_manager = Arc::clone(&self.device_manager);
|
let device_manager = Arc::clone(&self.device_manager);
|
||||||
let tray_icon = Rc::clone(&self.tray_inner.tray_icon);
|
let tray_icon = Arc::clone(&self.tray_inner.tray_icon);
|
||||||
let console_state = Arc::clone(&self.tray_inner.console_state);
|
let debug_console = Arc::clone(&self.tray_inner.debug_console);
|
||||||
let menu_items = Arc::clone(&self.tray_inner.menu_items);
|
let menu_items = Arc::clone(&self.tray_inner.menu_items);
|
||||||
|
|
||||||
let menu_channel = MenuEvent::receiver();
|
let menu_channel = MenuEvent::receiver();
|
||||||
|
|
||||||
let mut last_update = Instant::now();
|
|
||||||
let update_interval = Duration::from_millis(100);
|
|
||||||
|
|
||||||
event_loop.run(move |event, _, control_flow| {
|
event_loop.run(move |event, _, control_flow| {
|
||||||
*control_flow = tao::event_loop::ControlFlow::Wait;
|
*control_flow = tao::event_loop::ControlFlow::Wait;
|
||||||
|
|
||||||
@ -204,44 +211,33 @@ impl TrayApp {
|
|||||||
tao::event::Event::NewEvents(tao::event::StartCause::Init) => {
|
tao::event::Event::NewEvents(tao::event::StartCause::Init) => {
|
||||||
TrayInner::build_tray(&tray_icon, &tray_menu, icon.clone());
|
TrayInner::build_tray(&tray_icon, &tray_menu, icon.clone());
|
||||||
}
|
}
|
||||||
tao::event::Event::MainEventsCleared => {
|
tao::event::Event::UserEvent(TrayEvent::DeviceUpdate(device_ids)) => {
|
||||||
// Check if it's time to process updates
|
|
||||||
if last_update.elapsed() >= update_interval {
|
|
||||||
while let Ok(device_ids) = rx.try_recv() {
|
|
||||||
Self::update(&devices, &device_manager, &device_ids, &tray_icon);
|
Self::update(&devices, &device_manager, &device_ids, &tray_icon);
|
||||||
}
|
}
|
||||||
|
tao::event::Event::UserEvent(TrayEvent::MenuEvent(event)) => {
|
||||||
|
let menu_items = menu_items.lock();
|
||||||
|
|
||||||
while let Ok(event) = menu_channel.try_recv() {
|
if event.id == menu_items[0].id() {
|
||||||
let menu_items = menu_items.lock().unwrap();
|
debug_console.toggle_visibility();
|
||||||
let show_console_item = &menu_items[0];
|
let visible = debug_console.is_visible();
|
||||||
let quit_item = &menu_items[1];
|
menu_items[0].set_text(if visible {
|
||||||
|
"Hide Log Window"
|
||||||
if event.id == show_console_item.id() {
|
|
||||||
let mut visible = console_state.lock().unwrap();
|
|
||||||
*visible = !*visible;
|
|
||||||
|
|
||||||
if *visible {
|
|
||||||
unsafe { ShowWindow(GetConsoleWindow(), winuser::SW_SHOW) };
|
|
||||||
show_console_item.set_text("Hide Log Window");
|
|
||||||
trace!("showing log window");
|
|
||||||
} else {
|
} else {
|
||||||
unsafe { ShowWindow(GetConsoleWindow(), winuser::SW_HIDE) };
|
"Show Log Window"
|
||||||
show_console_item.set_text("Show Log Window");
|
});
|
||||||
trace!("hiding log window");
|
trace!("{} log window", if visible { "showing" } else { "hiding" });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.id == quit_item.id() {
|
if event.id == menu_items[1].id() {
|
||||||
*control_flow = tao::event_loop::ControlFlow::Exit;
|
*control_flow = tao::event_loop::ControlFlow::Exit;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last_update = Instant::now();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(event) = menu_channel.try_recv() {
|
||||||
|
let _ = proxy.send_event(TrayEvent::MenuEvent(event));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,10 +245,10 @@ impl TrayApp {
|
|||||||
devices: &Arc<Mutex<HashMap<u32, MemoryDevice>>>,
|
devices: &Arc<Mutex<HashMap<u32, MemoryDevice>>>,
|
||||||
manager: &Arc<Mutex<DeviceManager>>,
|
manager: &Arc<Mutex<DeviceManager>>,
|
||||||
device_ids: &[u32],
|
device_ids: &[u32],
|
||||||
tray_icon: &Rc<Mutex<Option<TrayIcon>>>,
|
tray_icon: &Arc<Mutex<Option<TrayIcon>>>,
|
||||||
) {
|
) {
|
||||||
let mut devices = devices.lock().unwrap();
|
let mut devices = devices.lock();
|
||||||
let manager = manager.lock().unwrap();
|
let manager = manager.lock();
|
||||||
|
|
||||||
for &id in device_ids {
|
for &id in device_ids {
|
||||||
if let Some(device) = devices.get_mut(&id) {
|
if let Some(device) = devices.get_mut(&id) {
|
||||||
@ -269,7 +265,7 @@ impl TrayApp {
|
|||||||
|
|
||||||
Self::check_notify(device);
|
Self::check_notify(device);
|
||||||
|
|
||||||
if let Some(tray_icon) = tray_icon.lock().unwrap().as_mut() {
|
if let Some(tray_icon) = tray_icon.lock().as_mut() {
|
||||||
let _ = tray_icon
|
let _ = tray_icon
|
||||||
.set_tooltip(Some(format!("{}: {}%", device.name, battery_level)));
|
.set_tooltip(Some(format!("{}: {}%", device.name, battery_level)));
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user