├── .cargo └── config.toml ├── infoband.png ├── .gitignore ├── src ├── macros.rs ├── metrics │ ├── memory.rs │ ├── cpu.rs │ ├── disk.rs │ └── network.rs ├── opt.rs ├── window │ ├── awake.rs │ ├── microphone.rs │ ├── timers.rs │ ├── position │ │ └── listener.rs │ ├── proc.rs │ ├── microphone │ │ └── listener.rs │ ├── position.rs │ ├── messages.rs │ ├── paint.rs │ └── state.rs ├── stats.rs ├── metrics.rs ├── constants.rs ├── window.rs ├── utils.rs ├── main.rs └── perf.rs ├── README.md ├── LICENSE ├── wix ├── License.rtf └── main.wxs ├── .github └── workflows │ └── ci.yml ├── Cargo.toml └── Cargo.lock /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = ["-Ctarget-feature=+crt-static"] 3 | -------------------------------------------------------------------------------- /infoband.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikdesjardins/infoband/master/infoband.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | use std::mem::ManuallyDrop; 2 | 3 | #[macro_export] 4 | macro_rules! defer { 5 | ($f:stmt $(;)?) => { 6 | let _x = $crate::macros::Defer(::std::mem::ManuallyDrop::new( 7 | #[allow(redundant_semicolons)] 8 | || { 9 | $f; 10 | }, 11 | )); 12 | }; 13 | } 14 | 15 | #[doc(hidden)] 16 | pub struct Defer(pub ManuallyDrop); 17 | 18 | impl Drop for Defer { 19 | fn drop(&mut self) { 20 | let f = unsafe { ManuallyDrop::take(&mut self.0) }; 21 | f(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/metrics/memory.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | use windows::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORYSTATUSEX}; 3 | use windows::core::Result; 4 | 5 | #[derive(Default)] 6 | pub struct State; 7 | 8 | impl State { 9 | pub fn fetch_percent(&self) -> Result { 10 | let mut mem_status = MEMORYSTATUSEX { 11 | dwLength: mem::size_of::() as u32, 12 | ..Default::default() 13 | }; 14 | // SAFETY: `mem_status` is a valid `MEMORYSTATUSEX` 15 | unsafe { GlobalMemoryStatusEx(&mut mem_status)? }; 16 | 17 | let used = mem_status.ullTotalPhys - mem_status.ullAvailPhys; 18 | let total = mem_status.ullTotalPhys; 19 | let percent = (used * 100) as f64 / total as f64; 20 | 21 | Ok(percent) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # infoband 2 | 3 | Windows "DeskBand" displaying cpu/mem/disk/network info. 4 | 5 | ![](./infoband.png) 6 | 7 | ## Configuration 8 | 9 | On first startup, `infoband` will generate a config file at `%localappdata%\infoband\infoband.json`. 10 | 11 | `infoband` does not apply config changes in real time, but it does kill the previous instance on startup. So my usual workflow for tweaking configuration is to repeatedly save the configuration and run `infoband` to see the result. 12 | 13 | To mute and unmute your mic with a hotkey, populate the `mic_hotkey` section with the desired [Virtual Key Code](https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) and modifiers. 14 | 15 | ```json 16 | { 17 | "mic_hotkey": { 18 | "virtual_key_code": 67, 19 | "win": true, 20 | "ctrl": true, 21 | "shift": true, 22 | "alt": true 23 | } 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 erikdesjardins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /wix/License.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fnil\fcharset0 Courier New;}} 2 | {\*\generator Riched20 10.0.15063}\viewkind4\uc1 3 | \pard\sa180\fs24\lang9 Copyright (c) 2023 Erik Desjardins\par 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\par 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\par 6 | \f1 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\f0\par 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/opt.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{DEFAULT_KEEP_AWAKE_WHILE_UNLOCKED, DEFAULT_MIC_HOTKEY}; 2 | use argh::FromArgs; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Computer info "deskband". 6 | #[derive(FromArgs)] 7 | pub struct Cli { 8 | /// logging verbosity (-v debug -v -v trace) 9 | #[argh(switch, short = 'v')] 10 | pub verbose: u8, 11 | 12 | /// whether to make the window more visible and interactible for debugging 13 | #[argh(switch)] 14 | pub debug_paint: bool, 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | #[serde(default)] 19 | pub struct ConfigFile { 20 | pub mic_hotkey: Option, 21 | #[serde(default)] 22 | pub keep_awake_while_unlocked: bool, 23 | } 24 | 25 | impl Default for ConfigFile { 26 | fn default() -> Self { 27 | Self { 28 | mic_hotkey: DEFAULT_MIC_HOTKEY, 29 | keep_awake_while_unlocked: DEFAULT_KEEP_AWAKE_WHILE_UNLOCKED, 30 | } 31 | } 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | pub struct MicrophoneHotkey { 36 | pub virtual_key_code: u16, 37 | #[serde(default)] 38 | pub win: bool, 39 | #[serde(default)] 40 | pub ctrl: bool, 41 | #[serde(default)] 42 | pub shift: bool, 43 | #[serde(default)] 44 | pub alt: bool, 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v*.*.* 9 | pull_request: 10 | 11 | jobs: 12 | fmt: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: rustup toolchain install stable --profile minimal 17 | - run: rustup component add rustfmt 18 | 19 | - run: cargo fmt --all -- --check 20 | 21 | clippy: 22 | runs-on: windows-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - run: rustup toolchain install stable --profile minimal 26 | - run: rustup component add clippy 27 | 28 | - run: | 29 | $env:RUSTFLAGS="-D warnings" 30 | cargo clippy 31 | 32 | test: 33 | runs-on: windows-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - run: rustup toolchain install stable --profile minimal 37 | 38 | - run: cargo test 39 | 40 | build: 41 | runs-on: windows-latest 42 | permissions: 43 | contents: write 44 | steps: 45 | - uses: actions/checkout@v3 46 | - run: rustup toolchain install stable --profile minimal 47 | 48 | - run: cargo build --release 49 | - run: dir target/release/infoband.exe 50 | 51 | - run: cargo install cargo-wix --debug 52 | - run: cargo wix --nocapture 53 | 54 | - uses: softprops/action-gh-release@v1 55 | if: startsWith(github.ref, 'refs/tags/') 56 | with: 57 | files: | 58 | target/release/infoband.exe 59 | target/wix/infoband-*.msi 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "infoband" 3 | version = "1.7.0" 4 | authors = ["Erik Desjardins "] 5 | description = "Windows \"DeskBand\" displaying cpu/mem/disk/network info." 6 | repository = "https://github.com/erikdesjardins/infoband" 7 | license = "MIT" 8 | edition = "2024" 9 | 10 | [build-dependencies] 11 | embed-manifest = "1.4" 12 | 13 | [dependencies] 14 | argh = "0.1" 15 | log4rs = { version = "1.0", default-features = false, features = ["console_appender", "file_appender"] } 16 | log = { version = "0.4", features = ["release_max_level_info"] } 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | windows = { version = "0.62", features = [ 20 | "Win32_Foundation", 21 | "Win32_Graphics_Gdi", 22 | "Win32_Media_Audio", 23 | "Win32_Media_Audio_Endpoints", 24 | "Win32_NetworkManagement_IpHelper", 25 | "Win32_System_Com", 26 | "Win32_System_Com_StructuredStorage", 27 | "Win32_System_DataExchange", 28 | "Win32_System_LibraryLoader", 29 | "Win32_System_Ole", 30 | "Win32_System_Performance", 31 | "Win32_System_Power", 32 | "Win32_System_ProcessStatus", 33 | "Win32_System_RemoteDesktop", 34 | "Win32_System_SystemInformation", 35 | "Win32_System_Threading", 36 | "Win32_System_Variant", 37 | "Win32_UI_Accessibility", 38 | "Win32_UI_Controls", 39 | "Win32_UI_HiDpi", 40 | "Win32_UI_Input_KeyboardAndMouse", 41 | "Win32_UI_WindowsAndMessaging", 42 | ] } 43 | windows-core = { version = "0.62", default-features = false } # Needed only for #[implement(ComInterface)]. 44 | 45 | [profile.release] 46 | panic = "abort" 47 | lto = true 48 | codegen-units = 1 49 | -------------------------------------------------------------------------------- /src/window/awake.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use windows::Win32::System::Power::{ 3 | ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, EXECUTION_STATE, 4 | SetThreadExecutionState, 5 | }; 6 | use windows::core::{Error, Result}; 7 | 8 | pub struct Awake { 9 | currently_kept_awake: Cell>, 10 | } 11 | 12 | impl Drop for Awake { 13 | fn drop(&mut self) { 14 | _ = self.keep_awake_fallible(false); 15 | } 16 | } 17 | 18 | impl Awake { 19 | pub fn new() -> Self { 20 | Self { 21 | currently_kept_awake: Cell::new(None), 22 | } 23 | } 24 | 25 | pub fn enable(&self) { 26 | self.currently_kept_awake.set(Some(false)); 27 | } 28 | 29 | pub fn keep_awake(&self, awake: bool) { 30 | if let Err(e) = self.keep_awake_fallible(awake) { 31 | log::error!("Failed to set keep awake state to {awake}: {e}"); 32 | } 33 | } 34 | 35 | fn keep_awake_fallible(&self, awake: bool) -> Result<()> { 36 | let Some(current_state) = self.currently_kept_awake.get() else { 37 | // Disabled, do nothing. 38 | return Ok(()); 39 | }; 40 | if current_state == awake { 41 | return Ok(()); 42 | } 43 | 44 | let new_state = if awake { 45 | ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED 46 | } else { 47 | ES_CONTINUOUS 48 | }; 49 | 50 | // SAFETY: No preconditions. 51 | let res = unsafe { SetThreadExecutionState(new_state) }; 52 | if res == EXECUTION_STATE(0) { 53 | return Err(Error::from_thread()); 54 | } 55 | 56 | self.currently_kept_awake.set(Some(awake)); 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/metrics/cpu.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use windows::Win32::Foundation::FILETIME; 3 | use windows::Win32::System::Threading::GetSystemTimes; 4 | use windows::core::Result; 5 | 6 | #[derive(Default)] 7 | pub struct State { 8 | prev_times: Cell>, 9 | } 10 | 11 | impl State { 12 | pub fn fetch_percent(&self) -> Result { 13 | let mut idle = FILETIME::default(); 14 | let mut kernel_plus_idle = FILETIME::default(); 15 | let mut user = FILETIME::default(); 16 | // SAFETY: all pointers are to valid `FILETIME`s 17 | unsafe { 18 | GetSystemTimes( 19 | Some(&mut idle), 20 | Some(&mut kernel_plus_idle), 21 | Some(&mut user), 22 | )? 23 | }; 24 | 25 | let to_100ns_intervals = |filetime: FILETIME| { 26 | (u64::from(filetime.dwHighDateTime) << 32) | u64::from(filetime.dwLowDateTime) 27 | }; 28 | 29 | let idle = to_100ns_intervals(idle); 30 | let kernel_plus_idle = to_100ns_intervals(kernel_plus_idle); 31 | let user = to_100ns_intervals(user); 32 | 33 | // On first sample, just store the current times and return zero. 34 | let percent = match self.prev_times.get() { 35 | Some((prev_idle, prev_kernel_plus_idle, prev_user)) => { 36 | let idle_delta = idle.wrapping_sub(prev_idle); 37 | let kernel_plus_idle_delta = kernel_plus_idle.wrapping_sub(prev_kernel_plus_idle); 38 | let user_delta = user.wrapping_sub(prev_user); 39 | 40 | let time_delta = kernel_plus_idle_delta + user_delta; 41 | let active_delta = time_delta - idle_delta; 42 | 43 | (active_delta * 100) as f64 / (time_delta as f64) 44 | } 45 | None => 0.0, 46 | }; 47 | 48 | self.prev_times.set(Some((idle, kernel_plus_idle, user))); 49 | 50 | Ok(percent) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/window/microphone.rs: -------------------------------------------------------------------------------- 1 | use listener::ListenerManager; 2 | use std::cell::{Cell, RefCell}; 3 | use std::ptr; 4 | use windows::Win32::Foundation::HWND; 5 | use windows::core::Result; 6 | 7 | mod listener; 8 | 9 | pub struct Microphone { 10 | listener: RefCell, 11 | is_muted: Cell, 12 | } 13 | 14 | impl Microphone { 15 | pub fn new(window: HWND) -> Result { 16 | Ok(Self { 17 | listener: RefCell::new(ListenerManager::new(window)?), 18 | // Assume muted in initial state, to avoid showing the microphone warning banner on startup. 19 | is_muted: Cell::new(true), 20 | }) 21 | } 22 | 23 | pub fn is_muted(&self) -> bool { 24 | self.is_muted.get() 25 | } 26 | 27 | pub fn refresh_devices(&self) { 28 | if let Err(e) = self.refresh_devices_fallible() { 29 | log::error!("Refreshing active microphones failed: {e}"); 30 | } 31 | } 32 | 33 | fn refresh_devices_fallible(&self) -> Result<()> { 34 | self.listener.borrow_mut().refresh_endpoints()?; 35 | 36 | Ok(()) 37 | } 38 | 39 | pub fn update_muted_state(&self) { 40 | if let Err(e) = self.update_muted_state_fallible() { 41 | log::error!("Updating muted state failed: {e}"); 42 | } 43 | } 44 | 45 | fn update_muted_state_fallible(&self) -> Result<()> { 46 | let mut all_muted = true; 47 | 48 | for endpoint in self.listener.borrow().endpoints() { 49 | let mute = unsafe { endpoint.GetMute()? }; 50 | if !mute.as_bool() { 51 | all_muted = false; 52 | break; 53 | } 54 | } 55 | 56 | self.is_muted.set(all_muted); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub fn set_mute(&self, mute: bool) { 62 | if let Err(e) = self.set_mute_fallible(mute) { 63 | log::error!("Setting muted state failed: {e}"); 64 | } 65 | } 66 | 67 | fn set_mute_fallible(&self, mute: bool) -> Result<()> { 68 | for endpoint in self.listener.borrow().endpoints() { 69 | unsafe { endpoint.SetMute(mute, ptr::null())? }; 70 | } 71 | 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::cell::Cell; 3 | use std::ops::{Add, Mul, Sub}; 4 | 5 | pub struct CircularBuffer 6 | where 7 | T: Default + Copy, 8 | { 9 | samples: [Cell; N], 10 | next_index: Cell, 11 | len: Cell, 12 | } 13 | 14 | impl Default for CircularBuffer 15 | where 16 | T: Default + Copy, 17 | { 18 | fn default() -> Self { 19 | Self { 20 | samples: array::from_fn(|_| Cell::new(T::default())), 21 | next_index: Cell::new(0), 22 | len: Cell::new(0), 23 | } 24 | } 25 | } 26 | 27 | impl CircularBuffer 28 | where 29 | T: Default + Copy, 30 | { 31 | pub fn push(&self, sample: T) { 32 | let index = self.next_index.get(); 33 | self.samples[index].set(sample); 34 | self.next_index.set((index + 1) % N); 35 | self.len.set((self.len.get() + 1).min(N)); 36 | } 37 | 38 | pub fn exponential_moving_average(&self, alpha: f64) -> T 39 | where 40 | T: From + Add + Sub + Mul, 41 | { 42 | assert!(alpha > 0.0 && alpha <= 1.0); 43 | let mut result = T::default(); 44 | let mut weight = 1.0; 45 | let mut index = self.next_index.get(); 46 | for _ in 0..self.len.get() { 47 | index = (index + N - 1) % N; 48 | let sample = self.samples[index].get(); 49 | result = result + T::from(weight) * (sample - result); 50 | weight *= alpha; 51 | } 52 | result 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | 60 | #[test] 61 | fn test_circular_buffer() { 62 | let buffer = CircularBuffer::::default(); 63 | assert_eq!(buffer.exponential_moving_average(0.5), 0.0); 64 | buffer.push(1.0); 65 | assert_eq!(buffer.exponential_moving_average(0.5), 1.0); 66 | buffer.push(2.0); 67 | assert_eq!(buffer.exponential_moving_average(0.5), 1.5); 68 | buffer.push(3.0); 69 | assert_eq!(buffer.exponential_moving_average(0.5), 2.125); 70 | buffer.push(4.0); 71 | assert_eq!(buffer.exponential_moving_average(0.5), 3.125); 72 | buffer.push(5.0); 73 | assert_eq!(buffer.exponential_moving_average(0.5), 4.125); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/window/timers.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ 2 | FETCH_AND_REDRAW_TIMER_COALESCE, FETCH_TIMER_MS, IDT_FETCH_AND_REDRAW_TIMER, 3 | IDT_MIC_STATE_TIMER, IDT_TRAY_POSITION_TIMER, IDT_Z_ORDER_TIMER, MIC_STATE_TIMER_COALESCE, 4 | MIC_STATE_TIMER_MS, TRAY_POSITION_TIMER_COALESCE, TRAY_POSITION_TIMER_MS, 5 | Z_ORDER_TIMER_COALESCE, Z_ORDER_TIMER_MS, 6 | }; 7 | use windows::Win32::Foundation::HWND; 8 | use windows::Win32::UI::WindowsAndMessaging::{KillTimer, SetCoalescableTimer}; 9 | use windows::core::{Error, Result}; 10 | 11 | pub struct Timers { 12 | pub fetch_and_redraw: 13 | Timer<{ IDT_FETCH_AND_REDRAW_TIMER.0 }, FETCH_TIMER_MS, FETCH_AND_REDRAW_TIMER_COALESCE>, 14 | pub tray_position: 15 | Timer<{ IDT_TRAY_POSITION_TIMER.0 }, TRAY_POSITION_TIMER_MS, TRAY_POSITION_TIMER_COALESCE>, 16 | pub z_order: Timer<{ IDT_Z_ORDER_TIMER.0 }, Z_ORDER_TIMER_MS, Z_ORDER_TIMER_COALESCE>, 17 | pub mic_state: Timer<{ IDT_MIC_STATE_TIMER.0 }, MIC_STATE_TIMER_MS, MIC_STATE_TIMER_COALESCE>, 18 | } 19 | 20 | impl Timers { 21 | pub fn new() -> Self { 22 | Self { 23 | fetch_and_redraw: Timer::new(), 24 | tray_position: Timer::new(), 25 | z_order: Timer::new(), 26 | mic_state: Timer::new(), 27 | } 28 | } 29 | } 30 | 31 | pub struct Timer { 32 | _priv: (), 33 | } 34 | 35 | impl Timer { 36 | fn new() -> Self { 37 | Self { _priv: () } 38 | } 39 | 40 | /// Schedule the timer. 41 | /// 42 | /// If the timer is already running, this will overwrite it. 43 | pub fn reschedule(&self, window: HWND) { 44 | if let Err(e) = self.reschedule_fallible(window) { 45 | log::error!("Rescheduling timer with id {ID} failed: {e}"); 46 | } 47 | } 48 | 49 | fn reschedule_fallible(&self, window: HWND) -> Result<()> { 50 | // Note: this timer will be destroyed when the window is destroyed. 51 | // (And in fact we can't destroy it manually, since the window handle will be invalid at that point.) 52 | match unsafe { SetCoalescableTimer(Some(window), ID, INTERVAL, None, COALESCE) } { 53 | 0 => Err(Error::from_thread()), 54 | _ => Ok(()), 55 | } 56 | } 57 | 58 | /// Kill the timer. 59 | pub fn kill(&self, window: HWND) { 60 | if let Err(e) = self.kill_fallible(window) { 61 | log::error!("Killing timer with id {ID} failed: {e}"); 62 | } 63 | } 64 | 65 | pub fn kill_fallible(&self, window: HWND) -> Result<()> { 66 | unsafe { KillTimer(Some(window), ID) } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/metrics/disk.rs: -------------------------------------------------------------------------------- 1 | use crate::perf::{PerfQueries, SingleCounter}; 2 | use std::cell::Cell; 3 | use std::time::Duration; 4 | use windows::core::{GUID, Result}; 5 | 6 | // Should be the counterset ID of "FileSystem Disk Activity", aka {F596750D-B109-4247-A62F-DEA47A46E505}. 7 | // This is a counterset of type PERF_COUNTERSET_MULTI_AGGREGATE. 8 | // If this gets changed at some point, we'll need to use PerfEnumerateCounterSet + PerfQueryCounterSetRegistrationInfo 9 | // to find the ID dynamically, as described in: 10 | // https://learn.microsoft.com/en-ca/windows/win32/perfctrs/using-the-perflib-functions-to-consume-counter-data 11 | const FILESYSTEM_DISK_ACTIVITY_COUNTERSET: GUID = 12 | GUID::from_u128(0xF596750DB1094247A62FDEA47A46E505); 13 | 14 | // "FileSystem Bytes Read" counter ID 15 | const FILESYSTEM_BYTES_READ_COUNTER: u32 = 0; 16 | // "FileSystem Bytes Written" counter ID 17 | const FILESYSTEM_BYTES_WRITTEN_COUNTER: u32 = 1; 18 | 19 | // Always filter to the "default" instance. 20 | // From introspection, it seems that there are only two instances: "default" and "_Total", which always have the same value. 21 | const FILESYSTEM_INSTANCE_NAME: &[u8; 6] = b"_Total"; 22 | 23 | pub struct State { 24 | queries: PerfQueries, 25 | prev_bytes_read: Cell, 26 | prev_bytes_written: Cell, 27 | } 28 | 29 | impl State { 30 | pub fn new() -> Result { 31 | Ok(Self { 32 | queries: PerfQueries::new_filtered_to_single_counter( 33 | FILESYSTEM_DISK_ACTIVITY_COUNTERSET, 34 | &[ 35 | FILESYSTEM_BYTES_READ_COUNTER, 36 | FILESYSTEM_BYTES_WRITTEN_COUNTER, 37 | ], 38 | FILESYSTEM_INSTANCE_NAME, 39 | )?, 40 | prev_bytes_read: Default::default(), 41 | prev_bytes_written: Default::default(), 42 | }) 43 | } 44 | 45 | pub fn fetch_mbyte(&self, time_delta: Option) -> Result { 46 | let [bytes_read, bytes_written] = self.queries.query_data()?; 47 | 48 | // On first sample, just store the current byte count and return zero. 49 | let mbyte = match time_delta { 50 | Some(time_delta) => { 51 | let bytes_read_delta = bytes_read.wrapping_sub(self.prev_bytes_read.get()); 52 | let bytes_written_delta = bytes_written.wrapping_sub(self.prev_bytes_written.get()); 53 | 54 | let total_byte_delta = bytes_read_delta + bytes_written_delta; 55 | 56 | total_byte_delta as f64 / (1024 * 1024) as f64 / time_delta.as_secs_f64() 57 | } 58 | None => 0.0, 59 | }; 60 | 61 | self.prev_bytes_read.set(bytes_read); 62 | self.prev_bytes_written.set(bytes_written); 63 | 64 | Ok(mbyte) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/window/position/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::UM_QUEUE_TRAY_POSITION_CHECK; 2 | use windows::Win32::Foundation::{HWND, LPARAM}; 3 | use windows::Win32::System::Com::SAFEARRAY; 4 | use windows::Win32::UI::Accessibility::{ 5 | IUIAutomation, IUIAutomationElement, IUIAutomationStructureChangedEventHandler, 6 | IUIAutomationStructureChangedEventHandler_Impl, StructureChangeType, TreeScope_Subtree, 7 | }; 8 | use windows::Win32::UI::WindowsAndMessaging::{PostMessageW, WM_USER}; 9 | use windows::core::{Ref, Result}; 10 | use windows_core::implement; 11 | 12 | pub struct TrayListenerManager { 13 | automation: IUIAutomation, 14 | listener: IUIAutomationStructureChangedEventHandler, 15 | registered_element: IUIAutomationElement, 16 | } 17 | 18 | impl Drop for TrayListenerManager { 19 | fn drop(&mut self) { 20 | if let Err(e) = unsafe { 21 | self.automation 22 | .RemoveStructureChangedEventHandler(&self.registered_element, &self.listener) 23 | } { 24 | log::warn!("Unregistering tray listener failed: {e}"); 25 | } 26 | } 27 | } 28 | 29 | impl TrayListenerManager { 30 | pub fn new( 31 | window: HWND, 32 | automation: IUIAutomation, 33 | element: IUIAutomationElement, 34 | ) -> Result { 35 | let listener = IUIAutomationStructureChangedEventHandler::from(TrayListener { window }); 36 | 37 | register_listener(&automation, &element, &listener)?; 38 | 39 | let registered_element = element; 40 | 41 | Ok(Self { 42 | automation, 43 | listener, 44 | registered_element, 45 | }) 46 | } 47 | 48 | pub fn element(&self) -> &IUIAutomationElement { 49 | &self.registered_element 50 | } 51 | 52 | pub fn refresh_element(&mut self, element: IUIAutomationElement) -> Result<()> { 53 | // Unregister old element 54 | if let Err(e) = unsafe { 55 | self.automation 56 | .RemoveStructureChangedEventHandler(&self.registered_element, &self.listener) 57 | } { 58 | log::warn!("Unregistering old tray listener failed: {e}"); 59 | } 60 | 61 | // Register new element 62 | register_listener(&self.automation, &element, &self.listener)?; 63 | 64 | self.registered_element = element; 65 | 66 | Ok(()) 67 | } 68 | } 69 | 70 | fn register_listener( 71 | automation: &IUIAutomation, 72 | element: &IUIAutomationElement, 73 | listener: &IUIAutomationStructureChangedEventHandler, 74 | ) -> Result<()> { 75 | unsafe { 76 | automation.AddStructureChangedEventHandler(element, TreeScope_Subtree, None, listener) 77 | } 78 | } 79 | 80 | #[implement(IUIAutomationStructureChangedEventHandler)] 81 | struct TrayListener { 82 | window: HWND, 83 | } 84 | 85 | impl IUIAutomationStructureChangedEventHandler_Impl for TrayListener_Impl { 86 | fn HandleStructureChangedEvent( 87 | &self, 88 | _: Ref<'_, IUIAutomationElement>, 89 | _: StructureChangeType, 90 | _: *const SAFEARRAY, 91 | ) -> Result<()> { 92 | // WARNING: this may be called from another thread, so we can only do thread-safe operations here. 93 | 94 | // Send a message to the main thread to enqueue a tray position check. 95 | unsafe { 96 | PostMessageW( 97 | Some(self.window), 98 | WM_USER, 99 | UM_QUEUE_TRAY_POSITION_CHECK, 100 | LPARAM(0), 101 | ) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{EXPONENTIAL_DECAY_ALPHA, SAMPLE_COUNT}; 2 | use crate::stats::CircularBuffer; 3 | use std::cell::Cell; 4 | use std::time::Instant; 5 | use windows::core::Result; 6 | 7 | mod cpu; 8 | mod disk; 9 | mod memory; 10 | mod network; 11 | 12 | pub struct Metrics { 13 | /// Timestamp of the last time metrics were fetched. 14 | prev_time: Cell>, 15 | 16 | cpu: cpu::State, 17 | /// Samples of CPU usage as a percentage of total CPU time. 18 | cpu_percent: CircularBuffer, 19 | 20 | memory: memory::State, 21 | /// Samples of memory usage as a percentage of total memory. 22 | memory_percent: CircularBuffer, 23 | 24 | disk: disk::State, 25 | /// Samples of disk bandwidth in megabytes per second. 26 | disk_mbyte: CircularBuffer, 27 | 28 | /// Count of network bytes transferred at the time of the previous fetch. 29 | network: network::State, 30 | /// Samples of network bandwidth in megabits per second. 31 | network_mbit: CircularBuffer, 32 | 33 | /// Number of times that metrics have been fetched (wrapping). 34 | fetch_count: Cell, 35 | } 36 | 37 | impl Metrics { 38 | pub fn new() -> Result { 39 | Ok(Self { 40 | prev_time: Default::default(), 41 | cpu: Default::default(), 42 | cpu_percent: Default::default(), 43 | memory: Default::default(), 44 | memory_percent: Default::default(), 45 | disk: disk::State::new()?, 46 | disk_mbyte: Default::default(), 47 | network: Default::default(), 48 | network_mbit: Default::default(), 49 | fetch_count: Default::default(), 50 | }) 51 | } 52 | 53 | #[inline(never)] 54 | pub fn fetch(&self) -> usize { 55 | let time = Instant::now(); 56 | let prev_time = self.prev_time.replace(Some(time)); 57 | let time_delta = prev_time.map(|prev_time| time - prev_time); 58 | 59 | match self.cpu.fetch_percent() { 60 | Ok(cpu) => { 61 | log::trace!("Fetched CPU: {cpu:.3}"); 62 | self.cpu_percent.push(cpu); 63 | } 64 | Err(e) => log::error!("Failed to fetch CPU: {e}"), 65 | } 66 | 67 | match self.memory.fetch_percent() { 68 | Ok(memory) => { 69 | log::trace!("Fetched memory: {memory:.3}"); 70 | self.memory_percent.push(memory); 71 | } 72 | Err(e) => log::error!("Failed to fetch memory: {e}"), 73 | } 74 | 75 | match self.disk.fetch_mbyte(time_delta) { 76 | Ok(disk) => { 77 | log::trace!("Fetched disk: {disk:.3}"); 78 | self.disk_mbyte.push(disk); 79 | } 80 | Err(e) => log::error!("Failed to fetch disk: {e}"), 81 | } 82 | 83 | match self.network.fetch_mbit(time_delta) { 84 | Ok(network) => { 85 | log::trace!("Fetched network: {network:.3}"); 86 | self.network_mbit.push(network); 87 | } 88 | Err(e) => log::error!("Failed to fetch network: {e}"), 89 | } 90 | 91 | let new_fetch_count = self.fetch_count.get().wrapping_add(1); 92 | self.fetch_count.set(new_fetch_count); 93 | new_fetch_count 94 | } 95 | 96 | pub fn avg_cpu_percent(&self) -> f64 { 97 | self.cpu_percent 98 | .exponential_moving_average(EXPONENTIAL_DECAY_ALPHA) 99 | } 100 | 101 | pub fn avg_memory_percent(&self) -> f64 { 102 | self.memory_percent 103 | .exponential_moving_average(EXPONENTIAL_DECAY_ALPHA) 104 | } 105 | 106 | pub fn avg_disk_mbyte(&self) -> f64 { 107 | self.disk_mbyte 108 | .exponential_moving_average(EXPONENTIAL_DECAY_ALPHA) 109 | } 110 | 111 | pub fn avg_network_mbit(&self) -> f64 { 112 | self.network_mbit 113 | .exponential_moving_average(EXPONENTIAL_DECAY_ALPHA) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/window/proc.rs: -------------------------------------------------------------------------------- 1 | use std::ptr::NonNull; 2 | use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; 3 | use windows::Win32::UI::WindowsAndMessaging::{ 4 | DefWindowProcW, GWLP_USERDATA, GetWindowLongPtrW, SetWindowLongPtrW, WM_NCCREATE, WM_NCDESTROY, 5 | }; 6 | use windows::core::Result; 7 | 8 | // This does not require Sync or Send. It appears that window procedures are very thread-local. 9 | // e.g. https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage 10 | // > If the specified window was created by the calling thread, the window procedure is called immediately 11 | // > as a subroutine. 12 | // > If the specified window was created by a different thread, the system switches to that thread and 13 | // > calls the appropriate window procedure. 14 | // That function is of course only one way to send messages to a window, 15 | // but it's part of a general pattern (e.g. message loops are also thread local). 16 | pub trait ProcHandler: Sized { 17 | fn new(window: HWND) -> Result; 18 | 19 | /// Handle a window message. 20 | /// 21 | /// If this returns `None`, the message will be passed to `DefWindowProcW`. 22 | fn handle(&self, window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) 23 | -> Option; 24 | } 25 | 26 | pub unsafe extern "system" fn window_proc( 27 | window: HWND, 28 | message: u32, 29 | wparam: WPARAM, 30 | lparam: LPARAM, 31 | ) -> LRESULT { 32 | let state = match message { 33 | // On create, set to default state 34 | WM_NCCREATE => { 35 | #[cold] 36 | #[inline(never)] 37 | fn create_state(window: HWND) -> Result> { 38 | let state = H::new(window)?; 39 | Ok(Box::new(state)) 40 | } 41 | 42 | let state = match create_state::(window) { 43 | Ok(state) => state, 44 | Err(e) => { 45 | log::error!("Failed to create window state: {e}"); 46 | // Returning false terminates window creation 47 | return LRESULT(0); 48 | } 49 | }; 50 | 51 | // SAFETY: handle is valid as we created it 52 | unsafe { SetWindowLongPtrW(window, GWLP_USERDATA, Box::into_raw(state) as isize) }; 53 | // SAFETY: propagates same safety requirements as caller 54 | return unsafe { DefWindowProcW(window, message, wparam, lparam) }; 55 | } 56 | // On destroy, drop state 57 | WM_NCDESTROY => { 58 | // SAFETY: setting state to 0 is always safe; type will be valid since we set it 59 | let state = unsafe { SetWindowLongPtrW(window, GWLP_USERDATA, 0) as *mut H }; 60 | if !state.is_null() { 61 | // SAFETY: state is either valid (as we set GWLP_USERDATA when constructing the window), or null 62 | unsafe { drop(Box::from_raw(state)) }; 63 | } 64 | 65 | // SAFETY: propagates same safety requirements as caller 66 | return unsafe { DefWindowProcW(window, message, wparam, lparam) }; 67 | } 68 | // For all other messages, get the state and handle as normal... 69 | _ => { 70 | // SAFETY: state is valid or null 71 | let state = unsafe { GetWindowLongPtrW(window, GWLP_USERDATA) as *mut H }; 72 | let state = NonNull::new(state); 73 | // SAFETY: state is either valid (as we set GWLP_USERDATA when constructing the window), or null 74 | unsafe { state.map(|s| s.as_ref()) } 75 | } 76 | }; 77 | 78 | let Some(state) = state else { 79 | log::warn!( 80 | "Window proc invoked with no state set (message=0x{:08x} wparam=0x{:08x} lparam=0x{:012x})", 81 | message, 82 | wparam.0, 83 | lparam.0 84 | ); 85 | // SAFETY: propagates same safety requirements as caller 86 | return unsafe { DefWindowProcW(window, message, wparam, lparam) }; 87 | }; 88 | 89 | let Some(result) = state.handle(window, message, wparam, lparam) else { 90 | // SAFETY: propagates same safety requirements as caller 91 | return unsafe { DefWindowProcW(window, message, wparam, lparam) }; 92 | }; 93 | 94 | result 95 | } 96 | -------------------------------------------------------------------------------- /src/metrics/network.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::mem; 4 | use std::ptr::addr_of_mut; 5 | use std::time::Duration; 6 | use windows::Win32::Foundation::WIN32_ERROR; 7 | use windows::Win32::NetworkManagement::IpHelper::{GetIfTable, MIB_IFROW, MIB_IFTABLE}; 8 | use windows::core::Result; 9 | 10 | #[derive(Default)] 11 | pub struct State { 12 | prev_byte_counts: RefCell>, 13 | } 14 | 15 | impl State { 16 | pub fn fetch_mbit(&self, time_delta: Option) -> Result { 17 | /// Identical to MIB_IFTABLE but with more rows. 18 | #[repr(C)] 19 | struct BIG_MIB_IFTABLE { 20 | dw_num_entries: u32, 21 | table: [MIB_IFROW; 128], 22 | } 23 | const { 24 | assert!(mem::align_of::() == mem::align_of::()); 25 | assert!( 26 | mem::offset_of!(BIG_MIB_IFTABLE, dw_num_entries) 27 | == mem::offset_of!(MIB_IFTABLE, dwNumEntries) 28 | ); 29 | assert!(mem::offset_of!(BIG_MIB_IFTABLE, table) == mem::offset_of!(MIB_IFTABLE, table)); 30 | } 31 | 32 | // SAFETY: MIB_IFTABLE can be safely zero-initialized 33 | let mut interfaces: BIG_MIB_IFTABLE = unsafe { mem::zeroed() }; 34 | let mut size_of_interfaces = mem::size_of_val(&interfaces).try_into().unwrap(); 35 | 36 | // SAFETY: BIG_MIB_IFTABLE is layout-compatible with MIB_IFTABLE, but with a larger table 37 | unsafe { 38 | WIN32_ERROR(GetIfTable( 39 | Some(addr_of_mut!(interfaces).cast::()), 40 | &mut size_of_interfaces, 41 | false, 42 | )) 43 | .ok()? 44 | }; 45 | 46 | let interfaces = &mut interfaces.table[..interfaces.dw_num_entries as usize]; 47 | 48 | // Windows has many internal copies of the same interface, which results in double-counting. 49 | // 50 | // For example: 51 | // status=INTERNAL_IF_OPER_STATUS(5) type=6 addr=[4, 217, 245, 51, 50, 182, 0, 0] bytes=2288317722 - \DEVICE\TCPIP_{438B8BC2-XXXX-XXXX-XXXX-XXXXXXXXXXXX} Realtek PCIe 2.5GbE Family Controller-WFP Native MAC Layer LightWeight Filter-0000 52 | // status=INTERNAL_IF_OPER_STATUS(5) type=6 addr=[4, 217, 245, 51, 50, 182, 0, 0] bytes=2288317722 - \DEVICE\TCPIP_{8C3238C4-XXXX-XXXX-XXXX-XXXXXXXXXXXX} Realtek PCIe 2.5GbE Family Controller-Npcap Packet Driver (NPCAP)-0000 53 | // status=INTERNAL_IF_OPER_STATUS(5) type=6 addr=[4, 217, 245, 51, 50, 182, 0, 0] bytes=2288317722 - \DEVICE\TCPIP_{438B8BC4-XXXX-XXXX-XXXX-XXXXXXXXXXXX} Realtek PCIe 2.5GbE Family Controller-QoS Packet Scheduler-0000 54 | // status=INTERNAL_IF_OPER_STATUS(5) type=6 addr=[4, 217, 245, 51, 50, 182, 0, 0] bytes=2288317722 - \DEVICE\TCPIP_{438B8BC7-XXXX-XXXX-XXXX-XXXXXXXXXXXX} Realtek PCIe 2.5GbE Family Controller-WFP 802.3 MAC Layer LightWeight Filter-0000 55 | // 56 | // To avoid this, deduplicate interfaces by address. 57 | 58 | interfaces.sort_unstable_by_key(|if_row| if_row.bPhysAddr); 59 | 60 | let mut prev_byte_counts = self.prev_byte_counts.borrow_mut(); 61 | let mut total_byte_delta = 0; 62 | 63 | let mut last_addr = 0; 64 | for if_row in interfaces { 65 | let addr = u64::from_ne_bytes(if_row.bPhysAddr); 66 | if addr == last_addr { 67 | // Duplicate entry, ignore. 68 | continue; 69 | } 70 | last_addr = addr; 71 | 72 | let in_bytes = if_row.dwInOctets; 73 | let out_bytes = if_row.dwOutOctets; 74 | 75 | // Compute delta if this interface has been seen before; otherwise just store the current counts 76 | if let Some((prev_in_bytes, prev_out_bytes)) = 77 | prev_byte_counts.insert(addr, (in_bytes, out_bytes)) 78 | { 79 | let in_byte_delta = in_bytes.wrapping_sub(prev_in_bytes); 80 | let out_byte_delta = out_bytes.wrapping_sub(prev_out_bytes); 81 | 82 | total_byte_delta += u64::from(in_byte_delta) + u64::from(out_byte_delta); 83 | } 84 | } 85 | 86 | // On first sample, just return zero. 87 | let mbit = match time_delta { 88 | Some(time_delta) => { 89 | let bits_per_byte = 8; 90 | let bits = total_byte_delta * bits_per_byte; 91 | (bits as f64) / 1_000_000.0 / time_delta.as_secs_f64() 92 | } 93 | None => 0.0, 94 | }; 95 | 96 | Ok(mbit) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/window/microphone/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::UM_QUEUE_MIC_STATE_CHECK; 2 | use crate::defer; 3 | use windows::Win32::Foundation::{HWND, LPARAM}; 4 | use windows::Win32::Media::Audio::Endpoints::{ 5 | IAudioEndpointVolume, IAudioEndpointVolumeCallback, IAudioEndpointVolumeCallback_Impl, 6 | }; 7 | use windows::Win32::Media::Audio::{ 8 | DEVICE_STATE_ACTIVE, IMMDeviceEnumerator, MMDeviceEnumerator, eCapture, 9 | }; 10 | use windows::Win32::System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, CoTaskMemFree}; 11 | use windows::Win32::UI::WindowsAndMessaging::{PostMessageW, WM_USER}; 12 | use windows::core::Result; 13 | use windows_core::{HSTRING, implement}; 14 | 15 | pub struct ListenerManager { 16 | dev_enumerator: IMMDeviceEnumerator, 17 | listener: IAudioEndpointVolumeCallback, 18 | // List of all endpoints and their corresponding IDs. 19 | // 20 | // Invariant: All items in the list must have been registered with the `listener` via `RegisterControlChangeNotify`.` 21 | registered_endpoints: Vec<(HSTRING, IAudioEndpointVolume)>, 22 | } 23 | 24 | impl Drop for ListenerManager { 25 | fn drop(&mut self) { 26 | for (id, endpoint) in &self.registered_endpoints { 27 | if let Err(e) = unsafe { endpoint.UnregisterControlChangeNotify(&self.listener) } { 28 | log::warn!("Unregistering listener failed for mic {id}: {e}"); 29 | } 30 | } 31 | } 32 | } 33 | 34 | impl ListenerManager { 35 | pub fn new(window: HWND) -> Result { 36 | let dev_enumerator = 37 | unsafe { CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_INPROC_SERVER)? }; 38 | 39 | Ok(Self { 40 | dev_enumerator, 41 | listener: IAudioEndpointVolumeCallback::from(MicrophoneListener { window }), 42 | registered_endpoints: Vec::new(), 43 | }) 44 | } 45 | 46 | pub fn endpoints(&self) -> impl Iterator { 47 | self.registered_endpoints 48 | .iter() 49 | .map(|(_, endpoint)| endpoint) 50 | } 51 | 52 | pub fn refresh_endpoints(&mut self) -> Result<()> { 53 | let endpoints = unsafe { 54 | self.dev_enumerator 55 | .EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE)? 56 | }; 57 | 58 | let count = unsafe { endpoints.GetCount()? }; 59 | 60 | for i in 0..count { 61 | // Get the device... 62 | let device = unsafe { endpoints.Item(i)? }; 63 | 64 | // ...and its ID. 65 | let id = unsafe { device.GetId()? }; 66 | defer! { 67 | unsafe { CoTaskMemFree(Some(id.as_ptr().cast())) }; 68 | } 69 | assert!(!id.is_null()); 70 | let id_bytes = unsafe { id.as_wide() }; 71 | 72 | // Ignore devices we've already registered. 73 | if self 74 | .registered_endpoints 75 | .iter() 76 | .any(|(i, _)| &**i == id_bytes) 77 | { 78 | continue; 79 | } 80 | 81 | let id = unsafe { id.to_hstring() }; 82 | 83 | // If not already registered, get the endpoint... 84 | let endpoint = 85 | unsafe { device.Activate::(CLSCTX_INPROC_SERVER, None)? }; 86 | 87 | // ...and register the listener. 88 | unsafe { endpoint.RegisterControlChangeNotify(Some(&self.listener))? }; 89 | 90 | log::debug!("Registered listener for mic {id}"); 91 | 92 | // Invariant: we just successfully registered the listener above. 93 | self.registered_endpoints.push((id, endpoint)); 94 | } 95 | 96 | Ok(()) 97 | } 98 | } 99 | 100 | #[implement(IAudioEndpointVolumeCallback)] 101 | struct MicrophoneListener { 102 | window: HWND, 103 | } 104 | 105 | impl IAudioEndpointVolumeCallback_Impl for MicrophoneListener_Impl { 106 | fn OnNotify( 107 | &self, 108 | _: *mut windows::Win32::Media::Audio::AUDIO_VOLUME_NOTIFICATION_DATA, 109 | ) -> Result<()> { 110 | // WARNING: this may be called from another thread, so we can only do thread-safe operations here. 111 | 112 | // Send a message to the main thread to enqueue a mic state check. 113 | unsafe { 114 | PostMessageW( 115 | Some(self.window), 116 | WM_USER, 117 | UM_QUEUE_MIC_STATE_CHECK, 118 | LPARAM(0), 119 | ) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::opt::MicrophoneHotkey; 2 | use crate::utils::Unscaled; 3 | use windows::Win32::Foundation::{COLORREF, WPARAM}; 4 | use windows::Win32::UI::Input::KeyboardAndMouse::VK_OEM_2; 5 | use windows::Win32::UI::WindowsAndMessaging::{self, TIMERV_DEFAULT_COALESCING}; 6 | 7 | // Startup parameters 8 | pub const EXISTING_PROCESS_SHUTDOWN_MS: u32 = 1000; 9 | 10 | // Sizing and positioning 11 | // 12 | // Replicating the exact positioning that Windows uses is difficult. 13 | // The constants (far) below result in +/- 1 pixel differences at 100%, 150% (perfect), and 200%, when positioning based on the midpoint of the text. 14 | // Positioning based on the top of the text doesn't seem to work, nor does the bottom. 15 | // I suspect windows is doing something with font metrics and positioning based on the baseline of the text. 16 | // 17 | // As follows are the raw (non-DPI-scaled) offsets to the TOP (not midpoint) of the RENDERING RECT that result in perfect alignment: 18 | // @ 200% 19 | // ^ ^ ^ 20 | // | | 14px 21 | // | 46px | 22 | // | | first 23 | // 96px | 24 | // | second 25 | // v 26 | // @ 150% 27 | // ^ ^ ^ 28 | // | | 10px 29 | // | 34px | 30 | // | | first 31 | // 72px | 32 | // | second 33 | // v 34 | // @ 100% 35 | // ^ ^ ^ 36 | // | | 8px ---> font size 1px too big, so 9px with 1px smaller font 37 | // | 24px ---|-----> same, would be 25px with 1px smaller font 38 | // | | first 39 | // 48px | 40 | // | second 41 | // v 42 | // 43 | // As follows are the raw (non-DPI-scaled) offsets to the top and bottom PIXELS (NOT rendering rect) of Windows' text: 44 | // @ 200% 45 | // ^ ^ ^ 46 | // | | 24px 47 | // | 56px | 48 | // | | first ---> font size 17px 49 | // 96px | | 50 | // | second | 51 | // | | 55px 52 | // | 23px | 53 | // v v v 54 | // @ 150% 55 | // ^ ^ ^ 56 | // | | 17px 57 | // | 41px | 58 | // | | first ---> font size 13px 59 | // 72px | | 60 | // | second | 61 | // | | 42px 62 | // | 18px | 63 | // v v v 64 | // @ 100% 65 | // ^ ^ ^ 66 | // | | 12px 67 | // | 28px | 68 | // | | first ---> font size 8px 69 | // 48px | | 70 | // | second | 71 | // | | 28px 72 | // | 12px | 73 | // v v v 74 | pub const FIRST_LINE_MIDPOINT_OFFSET_FROM_TOP: Unscaled = Unscaled::new(15); 75 | pub const SECOND_LINE_MIDPOINT_OFFSET_FROM_TOP: Unscaled = Unscaled::new(31); 76 | pub const LABEL_WIDTH: Unscaled = Unscaled::new(32); 77 | pub const RIGHT_COLUMN_WIDTH: Unscaled = Unscaled::new(*LABEL_WIDTH.as_inner() + 28); 78 | // Microphone warning will be placed in the horizontal center of the display 79 | pub const MICROPHONE_WARNING_WIDTH: Unscaled = Unscaled::new(78); // ~ 48 * 1.618 (golden ratio) 80 | 81 | // Colors 82 | pub const DEBUG_BACKGROUND_COLOR: COLORREF = COLORREF(0x00_77_77); // yellow 83 | pub const MICROPHONE_WARNING_COLOR: COLORREF = COLORREF(0x00_00_99); // red 84 | 85 | // File names 86 | pub const LOG_FILE_NAME: &str = "infoband.log"; 87 | pub const CONFIG_FILE_NAME: &str = "infoband.json"; 88 | pub const PID_FILE_NAME: &str = "infoband.pid"; 89 | 90 | // Configuration 91 | pub const DEFAULT_MIC_HOTKEY: Option = if cfg!(debug_assertions) { 92 | // Enable by default when debugging so it's easier to test 93 | Some(MicrophoneHotkey { 94 | // Slash / question mark 95 | virtual_key_code: VK_OEM_2.0, 96 | win: true, 97 | ctrl: false, 98 | shift: false, 99 | alt: false, 100 | }) 101 | } else { 102 | None 103 | }; 104 | // Enable by default when debugging so it's easier to test 105 | pub const DEFAULT_KEEP_AWAKE_WHILE_UNLOCKED: bool = cfg!(debug_assertions); 106 | 107 | // User messages 108 | pub const UM_ENABLE_KEEP_AWAKE: WPARAM = WPARAM(1); 109 | pub const UM_ENABLE_DEBUG_PAINT: WPARAM = WPARAM(2); 110 | pub const UM_INITIAL_METRICS: WPARAM = WPARAM(3); 111 | pub const UM_INITIAL_MIC_STATE: WPARAM = WPARAM(4); 112 | pub const UM_INITIAL_RENDER: WPARAM = WPARAM(5); 113 | pub const UM_QUEUE_TRAY_POSITION_CHECK: WPARAM = WPARAM(6); 114 | pub const UM_QUEUE_MIC_STATE_CHECK: WPARAM = WPARAM(7); 115 | 116 | // Timer ids 117 | pub const IDT_FETCH_AND_REDRAW_TIMER: WPARAM = WPARAM(1); 118 | pub const IDT_TRAY_POSITION_TIMER: WPARAM = WPARAM(2); 119 | pub const IDT_Z_ORDER_TIMER: WPARAM = WPARAM(3); 120 | pub const IDT_MIC_STATE_TIMER: WPARAM = WPARAM(4); 121 | 122 | // Timer intervals 123 | pub const FETCH_TIMER_MS: u32 = 1000; 124 | pub const REDRAW_EVERY_N_FETCHES: usize = 5; 125 | pub const TRAY_POSITION_TIMER_MS: u32 = 10; 126 | pub const Z_ORDER_TIMER_MS: u32 = 50; 127 | pub const MIC_STATE_TIMER_MS: u32 = 10; 128 | 129 | // Timer coalescing delays 130 | pub const FETCH_AND_REDRAW_TIMER_COALESCE: u32 = 1000; 131 | pub const TRAY_POSITION_TIMER_COALESCE: u32 = TIMERV_DEFAULT_COALESCING; // usually something short like 32ms 132 | pub const Z_ORDER_TIMER_COALESCE: u32 = TIMERV_DEFAULT_COALESCING; // usually something short like 32ms 133 | pub const MIC_STATE_TIMER_COALESCE: u32 = TIMERV_DEFAULT_COALESCING; // usually something short like 32ms 134 | 135 | // Metrics 136 | pub const SAMPLE_COUNT: usize = 8; 137 | pub const EXPONENTIAL_DECAY_ALPHA: f64 = 0.631; // 0.631^5 = 0.1, so 90% of the weight is for the last 5 samples 138 | 139 | // Shell hook messages 140 | pub const HSHELL_WINDOWACTIVATED: WPARAM = WPARAM(0x4); 141 | pub const HSHELL_RUDEAPPACTIVATED: WPARAM = WPARAM(0x8004); 142 | 143 | // WTS session change messages 144 | pub const WTS_SESSION_LOGON: WPARAM = WPARAM(WindowsAndMessaging::WTS_SESSION_LOGON as _); 145 | pub const WTS_SESSION_LOGOFF: WPARAM = WPARAM(WindowsAndMessaging::WTS_SESSION_LOGOFF as _); 146 | pub const WTS_SESSION_LOCK: WPARAM = WPARAM(WindowsAndMessaging::WTS_SESSION_LOCK as _); 147 | pub const WTS_SESSION_UNLOCK: WPARAM = WPARAM(WindowsAndMessaging::WTS_SESSION_UNLOCK as _); 148 | 149 | // Hotkey ids 150 | pub const HOTKEY_MIC_MUTE: WPARAM = WPARAM(1); 151 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ 2 | HOTKEY_MIC_MUTE, UM_ENABLE_DEBUG_PAINT, UM_ENABLE_KEEP_AWAKE, UM_INITIAL_METRICS, 3 | UM_INITIAL_MIC_STATE, UM_INITIAL_RENDER, 4 | }; 5 | use crate::defer; 6 | use crate::opt::MicrophoneHotkey; 7 | use crate::window::proc::window_proc; 8 | use windows::Win32::Foundation::{HINSTANCE, LPARAM}; 9 | use windows::Win32::System::Com::{ 10 | COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, CoInitializeEx, CoUninitialize, 11 | }; 12 | use windows::Win32::System::LibraryLoader::GetModuleHandleW; 13 | use windows::Win32::System::RemoteDesktop::{ 14 | NOTIFY_FOR_THIS_SESSION, WTSRegisterSessionNotification, 15 | }; 16 | use windows::Win32::UI::Input::KeyboardAndMouse::{ 17 | MOD_ALT, MOD_CONTROL, MOD_NOREPEAT, MOD_SHIFT, MOD_WIN, RegisterHotKey, 18 | }; 19 | use windows::Win32::UI::WindowsAndMessaging::{ 20 | CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DispatchMessageW, GetMessageW, 21 | IDC_ARROW, LoadCursorW, MSG, PostMessageW, RegisterClassW, RegisterShellHookWindow, SW_SHOWNA, 22 | ShowWindow, WM_USER, WNDCLASSW, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_LAYERED, 23 | WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_POPUP, 24 | }; 25 | use windows::core::{Error, HRESULT, Result, w}; 26 | 27 | mod awake; 28 | mod messages; 29 | mod microphone; 30 | mod paint; 31 | mod position; 32 | mod proc; 33 | mod state; 34 | mod timers; 35 | 36 | /// Create the toplevel window, start timers for updating it, and pump the windows message loop. 37 | pub fn create_and_run_message_loop( 38 | mic_hotkey: Option, 39 | keep_awake_while_unlocked: bool, 40 | debug_paint: bool, 41 | ) -> Result<()> { 42 | // Initialize COM, to be used by the microphone management code. 43 | // Ideally, we would put this in the microphone state code, but the docs suggest that: 44 | // > CoUninitialize should be called on application shutdown, as the last call made to the COM library 45 | // > after the application hides its main windows and falls through its main message loop. 46 | // https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize 47 | // ...so it's not clear that uninitializing when the state is dropped is correct. 48 | // Thus, since we have to uninitialize in this function anyways, we also initialize here for consistency. 49 | unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE).ok()? }; 50 | defer! { 51 | unsafe { CoUninitialize() }; 52 | }; 53 | 54 | // SAFETY: no safety requirements when passing null 55 | let instance = HINSTANCE::from(unsafe { GetModuleHandleW(None)? }); 56 | 57 | // SAFETY: using predefined system cursor, so instance handle is unused; IDC_ARROW is guaranteed to exist 58 | let cursor = unsafe { LoadCursorW(None, IDC_ARROW)? }; 59 | 60 | let class = w!("infobandwindow"); 61 | 62 | let wc = WNDCLASSW { 63 | style: CS_HREDRAW | CS_VREDRAW, 64 | hCursor: cursor, 65 | hInstance: instance, 66 | lpszClassName: class, 67 | lpfnWndProc: Some(window_proc::), 68 | ..Default::default() 69 | }; 70 | 71 | // SAFETY: all necessary attributes of WNDCLASSW are initialized 72 | let atom = unsafe { RegisterClassW(&wc) }; 73 | assert!(atom != 0); 74 | 75 | // Note: this window will be destroyed by the default handler for WM_CLOSE. 76 | let window = unsafe { 77 | CreateWindowExW( 78 | // Layered window allows transparency: 79 | // https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#layered-windows 80 | // Transparent window allows clicks to pass through everywhere. 81 | // (Layered windows allow clicks to pass through in transparent areas only.) 82 | // Tool window hides it from the taskbar. 83 | WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW, 84 | class, 85 | None, 86 | // Popup window removes borders and title bar. 87 | // Clipping probably not necessary since we don't have child windows. 88 | WS_POPUP | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, 89 | // Leave all positions defaulted. 90 | // Layered windows aren't displayed until UpdateLayeredWindow is called, so we'll set it then. 91 | CW_USEDEFAULT, 92 | CW_USEDEFAULT, 93 | CW_USEDEFAULT, 94 | CW_USEDEFAULT, 95 | None, 96 | None, 97 | Some(instance), 98 | None, 99 | )? 100 | }; 101 | 102 | // Register window to receive shell hook messages. We use these to follow the z-order state of the taskbar. 103 | unsafe { RegisterShellHookWindow(window).ok()? }; 104 | 105 | // Register window to receive session notifictions. We use these to stop drawing when the session is locked. 106 | // The main reason we do this is to avoid weird situations where buffered paint gets stuck in a failed state. 107 | // This seems to happen when we attempt to draw when the monitor that our window is on is turned off, e.g. if when waking from sleep, 108 | // a different monitor in a multi-monitor setup wakes up first. 109 | unsafe { WTSRegisterSessionNotification(window, NOTIFY_FOR_THIS_SESSION)? }; 110 | 111 | // Register hotkey for mic muting. 112 | if let Some(mic_hotkey) = &mic_hotkey { 113 | let modifiers = { 114 | // Always forbid repeat, and add other modifiers as necessary. 115 | let mut modifiers = MOD_NOREPEAT; 116 | if mic_hotkey.win { 117 | modifiers |= MOD_WIN; 118 | } 119 | if mic_hotkey.shift { 120 | modifiers |= MOD_SHIFT; 121 | } 122 | if mic_hotkey.ctrl { 123 | modifiers |= MOD_CONTROL; 124 | } 125 | if mic_hotkey.alt { 126 | modifiers |= MOD_ALT; 127 | } 128 | modifiers 129 | }; 130 | unsafe { 131 | RegisterHotKey( 132 | Some(window), 133 | HOTKEY_MIC_MUTE.0.try_into().unwrap(), 134 | modifiers, 135 | u32::from(mic_hotkey.virtual_key_code), 136 | )? 137 | }; 138 | } 139 | 140 | // Enqueue a message to tell the window to stay awake 141 | if keep_awake_while_unlocked { 142 | unsafe { PostMessageW(Some(window), WM_USER, UM_ENABLE_KEEP_AWAKE, LPARAM(0))? }; 143 | } 144 | 145 | // Enqueue a message to tell the window about debug settings 146 | if debug_paint { 147 | unsafe { PostMessageW(Some(window), WM_USER, UM_ENABLE_DEBUG_PAINT, LPARAM(0))? }; 148 | } 149 | 150 | // Enqueue a message for initial metrics fetch 151 | unsafe { PostMessageW(Some(window), WM_USER, UM_INITIAL_METRICS, LPARAM(0))? }; 152 | 153 | // Enqueue a message for initial mic state update 154 | if mic_hotkey.is_some() { 155 | unsafe { PostMessageW(Some(window), WM_USER, UM_INITIAL_MIC_STATE, LPARAM(0))? }; 156 | } 157 | 158 | // Enqueue a message for initial render 159 | unsafe { PostMessageW(Some(window), WM_USER, UM_INITIAL_RENDER, LPARAM(0))? }; 160 | 161 | // Show window (without activating/focusing it) after setting it up. 162 | // Note that layered windows still don't render until you call UpdateLayeredWindow. 163 | _ = unsafe { ShowWindow(window, SW_SHOWNA) }; 164 | 165 | // Run message loop (will block) 166 | run_message_loop()?; 167 | 168 | Ok(()) 169 | } 170 | 171 | #[inline(never)] 172 | pub fn run_message_loop() -> Result<()> { 173 | let mut msg = MSG::default(); 174 | // SAFETY: msg pointer is valid 175 | while unsafe { GetMessageW(&mut msg, None, 0, 0) }.as_bool() { 176 | // SAFETY: msg pointer is valid 177 | unsafe { DispatchMessageW(&msg) }; 178 | } 179 | 180 | // Apparently, wParam is the exit code 181 | let exit_code = msg.wParam.0 as i32; 182 | if exit_code == 0 { 183 | Ok(()) 184 | } else { 185 | Err(Error::from_hresult(HRESULT(exit_code))) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use windows::Win32::Foundation::{POINT, RECT, SIZE}; 5 | 6 | pub trait RectExt { 7 | fn from_size(size: SIZE) -> Self; 8 | 9 | fn top_left_corner(self) -> POINT; 10 | fn size(self) -> SIZE; 11 | 12 | fn with_right_edge_at(self, x: i32) -> Self; 13 | fn with_left_edge_at(self, x: i32) -> Self; 14 | fn with_horizontal_midpoint_at(self, y: i32) -> Self; 15 | fn with_vertical_midpoint_at(self, y: i32) -> Self; 16 | 17 | fn width(&self) -> i32; 18 | fn height(&self) -> i32; 19 | } 20 | 21 | impl RectExt for RECT { 22 | fn from_size(size: SIZE) -> Self { 23 | Self { 24 | top: 0, 25 | left: 0, 26 | right: size.cx, 27 | bottom: size.cy, 28 | } 29 | } 30 | 31 | fn top_left_corner(self) -> POINT { 32 | POINT { 33 | x: self.left, 34 | y: self.top, 35 | } 36 | } 37 | 38 | fn size(self) -> SIZE { 39 | SIZE { 40 | cx: self.width(), 41 | cy: self.height(), 42 | } 43 | } 44 | 45 | fn with_left_edge_at(self, x: i32) -> Self { 46 | Self { 47 | left: x, 48 | right: x + self.width(), 49 | ..self 50 | } 51 | } 52 | 53 | fn with_right_edge_at(self, x: i32) -> Self { 54 | Self { 55 | left: x - self.width(), 56 | right: x, 57 | ..self 58 | } 59 | } 60 | 61 | fn with_horizontal_midpoint_at(self, x: i32) -> Self { 62 | let extra = self.width() % 2; 63 | let half_width = self.width() / 2; 64 | Self { 65 | left: x - half_width, 66 | right: x + half_width + extra, 67 | ..self 68 | } 69 | } 70 | 71 | fn with_vertical_midpoint_at(self, y: i32) -> Self { 72 | let extra = self.height() % 2; 73 | let half_height = self.height() / 2; 74 | Self { 75 | top: y - half_height, 76 | bottom: y + half_height + extra, 77 | ..self 78 | } 79 | } 80 | 81 | fn width(&self) -> i32 { 82 | self.right - self.left 83 | } 84 | 85 | fn height(&self) -> i32 { 86 | self.bottom - self.top 87 | } 88 | } 89 | 90 | #[derive(Copy, Clone)] 91 | pub struct ScalingFactor(u32); 92 | 93 | impl ScalingFactor { 94 | pub const ONE: Self = Self(u16::MAX as u32 + 1); 95 | 96 | /// Construct a scaling factor from a fraction. 97 | #[track_caller] 98 | pub fn from_ratio(num: u32, denom: u32) -> Self { 99 | let factor = (u64::from(Self::ONE.0) * u64::from(num)) / u64::from(denom); 100 | match factor.try_into() { 101 | Ok(f) => Self(f), 102 | Err(e) => panic!("Scaling factor {num} / {denom} is too large: {e}"), 103 | } 104 | } 105 | } 106 | 107 | /// Fixed point scaling. 108 | pub trait ScaleBy { 109 | fn scale_by(self, by: ScalingFactor) -> Self; 110 | } 111 | 112 | macro_rules! impl_scaleby { 113 | ($this:ty, via: $intermediate:ty) => { 114 | impl ScaleBy for $this { 115 | fn scale_by(self, by: ScalingFactor) -> Self { 116 | ((<$intermediate>::from(self) * <$intermediate>::from(by.0)) 117 | / <$intermediate>::from(ScalingFactor::ONE.0)) as $this 118 | } 119 | } 120 | }; 121 | } 122 | 123 | impl_scaleby!(i16, via: i64); 124 | impl_scaleby!(i32, via: i64); 125 | impl_scaleby!(u16, via: u64); 126 | impl_scaleby!(u32, via: u64); 127 | 128 | impl ScaleBy for RECT { 129 | fn scale_by(self, by: ScalingFactor) -> Self { 130 | Self { 131 | left: self.left.scale_by(by), 132 | top: self.top.scale_by(by), 133 | right: self.right.scale_by(by), 134 | bottom: self.bottom.scale_by(by), 135 | } 136 | } 137 | } 138 | 139 | impl ScaleBy for SIZE { 140 | fn scale_by(self, by: ScalingFactor) -> Self { 141 | Self { 142 | cx: self.cx.scale_by(by), 143 | cy: self.cy.scale_by(by), 144 | } 145 | } 146 | } 147 | 148 | /// Represents an unscaled constant value. 149 | /// To prevent misuse, the inner value is not vailable unless you call `scale_by` or `into_inner`. 150 | #[derive(Copy, Clone, Serialize, Deserialize)] 151 | #[serde(transparent)] 152 | pub struct Unscaled(T) 153 | where 154 | T: ScaleBy; 155 | 156 | impl Unscaled 157 | where 158 | T: ScaleBy, 159 | { 160 | pub const fn new(value: T) -> Self { 161 | Self(value) 162 | } 163 | 164 | pub fn scale_by(self, by: ScalingFactor) -> T { 165 | self.0.scale_by(by) 166 | } 167 | 168 | pub const fn as_inner(&self) -> &T { 169 | &self.0 170 | } 171 | } 172 | 173 | impl Display for Unscaled { 174 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 175 | self.0.fmt(f) 176 | } 177 | } 178 | 179 | #[cfg(test)] 180 | mod tests { 181 | use super::*; 182 | 183 | #[test] 184 | fn with_right_edge_at() { 185 | let before = RECT { 186 | left: 1, 187 | top: 2, 188 | right: 11, 189 | bottom: 12, 190 | }; 191 | let after = before.with_right_edge_at(20); 192 | assert_eq!(after.right, 20); 193 | assert_eq!(after.top, before.top); 194 | assert_eq!(after.size(), before.size()); 195 | } 196 | 197 | #[test] 198 | fn with_horizontal_midpoint_at() { 199 | let before = RECT { 200 | left: 1, 201 | top: 2, 202 | right: 11, 203 | bottom: 12, 204 | }; 205 | let after = before.with_horizontal_midpoint_at(20); 206 | assert_eq!(after.left, 15); 207 | assert_eq!(after.top, before.top); 208 | assert_eq!(after.size(), before.size()); 209 | } 210 | 211 | #[test] 212 | fn with_horizontal_midpoint_at_odd_width() { 213 | let before = RECT { 214 | left: 1, 215 | top: 2, 216 | right: 10, 217 | bottom: 10, 218 | }; 219 | let after = before.with_horizontal_midpoint_at(20); 220 | assert_eq!(after.left, 16); 221 | assert_eq!(after.top, before.top); 222 | assert_eq!(after.size(), before.size()); 223 | } 224 | 225 | #[test] 226 | fn with_vertical_midpoint_at() { 227 | let before = RECT { 228 | left: 1, 229 | top: 2, 230 | right: 11, 231 | bottom: 12, 232 | }; 233 | let after = before.with_vertical_midpoint_at(20); 234 | assert_eq!(after.top, 15); 235 | assert_eq!(after.left, before.left); 236 | assert_eq!(after.size(), before.size()); 237 | } 238 | 239 | #[test] 240 | fn with_vertical_midpoint_at_odd_height() { 241 | let before = RECT { 242 | left: 2, 243 | top: 1, 244 | right: 10, 245 | bottom: 10, 246 | }; 247 | let after = before.with_vertical_midpoint_at(20); 248 | assert_eq!(after.top, 16); 249 | assert_eq!(after.left, before.left); 250 | assert_eq!(after.size(), before.size()); 251 | } 252 | 253 | #[test] 254 | fn scaling_by_zero() { 255 | assert_eq!(0.scale_by(ScalingFactor::from_ratio(0, 1)), 0); 256 | assert_eq!(1.scale_by(ScalingFactor::from_ratio(0, 123)), 0); 257 | assert_eq!(u16::MAX.scale_by(ScalingFactor::from_ratio(0, 123)), 0); 258 | assert_eq!(u32::MAX.scale_by(ScalingFactor::from_ratio(0, 123)), 0); 259 | } 260 | 261 | #[test] 262 | fn scaling_by_one() { 263 | assert_eq!(0.scale_by(ScalingFactor::ONE), 0); 264 | assert_eq!(1.scale_by(ScalingFactor::ONE), 1); 265 | assert_eq!(u16::MAX.scale_by(ScalingFactor::ONE), u16::MAX); 266 | assert_eq!( 267 | u32::MAX.scale_by(ScalingFactor::from_ratio(123, 123)), 268 | u32::MAX 269 | ); 270 | assert_eq!(123.scale_by(ScalingFactor::from_ratio(123, 123)), 123); 271 | } 272 | 273 | #[test] 274 | fn scaling_by_ten() { 275 | assert_eq!(0.scale_by(ScalingFactor::from_ratio(10, 1)), 0); 276 | assert_eq!(1.scale_by(ScalingFactor::from_ratio(10, 1)), 10); 277 | assert_eq!( 278 | u32::from(u16::MAX).scale_by(ScalingFactor::from_ratio(10, 1)), 279 | 655350 280 | ); 281 | assert_eq!(123.scale_by(ScalingFactor::from_ratio(100, 10)), 1230); 282 | } 283 | 284 | #[test] 285 | fn scaling_by_one_point_five() { 286 | assert_eq!(0.scale_by(ScalingFactor::from_ratio(144, 96)), 0); 287 | assert_eq!(1.scale_by(ScalingFactor::from_ratio(144, 96)), 1); 288 | assert_eq!(2.scale_by(ScalingFactor::from_ratio(144, 96)), 3); 289 | assert_eq!(100.scale_by(ScalingFactor::from_ratio(144, 96)), 150); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | non_snake_case, 3 | non_camel_case_types, 4 | unstable_name_collisions, 5 | clippy::collapsible_else_if, 6 | clippy::if_same_then_else, 7 | clippy::let_unit_value, 8 | clippy::manual_non_exhaustive 9 | )] 10 | #![deny(unsafe_op_in_unsafe_fn)] 11 | // Prevent the automatic console window you get on startup. 12 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 13 | 14 | use crate::constants::{CONFIG_FILE_NAME, LOG_FILE_NAME, PID_FILE_NAME}; 15 | use constants::EXISTING_PROCESS_SHUTDOWN_MS; 16 | use log::LevelFilter; 17 | use log4rs::Config; 18 | use log4rs::append::console::{ConsoleAppender, Target}; 19 | use log4rs::append::file::FileAppender; 20 | use log4rs::config::{Appender, Root}; 21 | use log4rs::encode::pattern::PatternEncoder; 22 | use std::env; 23 | use std::fs::{self, File}; 24 | use std::io; 25 | use std::path::{Path, PathBuf}; 26 | use windows::Win32::Foundation::{CloseHandle, WAIT_OBJECT_0, WAIT_TIMEOUT}; 27 | use windows::Win32::System::ProcessStatus::GetModuleFileNameExW; 28 | use windows::Win32::System::Threading::{ 29 | GetCurrentProcessId, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, 30 | PROCESS_TERMINATE, TerminateProcess, WaitForSingleObject, 31 | }; 32 | use windows::core::{Error, Result, w}; 33 | 34 | mod macros; 35 | 36 | mod constants; 37 | mod metrics; 38 | mod opt; 39 | mod perf; 40 | mod stats; 41 | mod utils; 42 | mod window; 43 | 44 | fn main() -> Result<()> { 45 | let opt::Cli { 46 | verbose, 47 | debug_paint, 48 | } = argh::from_env(); 49 | 50 | // Init logging as early as possible. 51 | let config = if cfg!(debug_assertions) { 52 | // In debug builds, don't create log/config files 53 | init_logging(None, verbose); 54 | Default::default() 55 | } else { 56 | // In release (installed) builds, create log/config files in local appdata. 57 | let path = make_local_appdata_folder(); 58 | init_logging(Some(&path.join(LOG_FILE_NAME)), verbose); 59 | kill_and_write_pid_file(&path.join(PID_FILE_NAME)); 60 | load_config_file(&path.join(CONFIG_FILE_NAME)) 61 | }; 62 | 63 | let opt::ConfigFile { 64 | mic_hotkey, 65 | keep_awake_while_unlocked, 66 | } = config; 67 | 68 | log::info!("Started up infoband {}", env!("CARGO_PKG_VERSION")); 69 | 70 | if let Err(e) = 71 | window::create_and_run_message_loop(mic_hotkey, keep_awake_while_unlocked, debug_paint) 72 | { 73 | log::error!("Failed to create and run message loop: {e}"); 74 | return Err(e); 75 | } 76 | 77 | Ok(()) 78 | } 79 | 80 | fn make_local_appdata_folder() -> PathBuf { 81 | let Some(local_appdata) = env::var_os("LOCALAPPDATA") else { 82 | panic!("Failed to get LOCALAPPDATA environment variable."); 83 | }; 84 | 85 | let mut path = PathBuf::from(local_appdata); 86 | path.push("infoband"); 87 | 88 | if let Err(e) = fs::create_dir_all(&path) { 89 | panic!( 90 | "Failed to create local appdata folder `{}`: {e}", 91 | path.display() 92 | ); 93 | } 94 | 95 | path 96 | } 97 | 98 | fn init_logging(path: Option<&Path>, verbose: u8) { 99 | log4rs::init_config( 100 | Config::builder() 101 | .appender(Appender::builder().build("default", { 102 | let encoder = Box::new(PatternEncoder::new("[{date(%Y-%m-%d %H:%M:%S%.3f)} {highlight({level}):5} {target}] {highlight({message})}{n}")); 103 | if let Some(path) = path { 104 | Box::new(FileAppender::builder().encoder(encoder).build(path).unwrap()) 105 | } else { 106 | Box::new( 107 | ConsoleAppender::builder() 108 | .encoder(encoder) 109 | .target(Target::Stderr) 110 | .build(), 111 | ) 112 | } 113 | })) 114 | .build(Root::builder().appender("default").build(match verbose { 115 | 0 => LevelFilter::Info, 116 | 1 => LevelFilter::Debug, 117 | _ => LevelFilter::Trace, 118 | })) 119 | .unwrap(), 120 | ) 121 | .unwrap(); 122 | } 123 | 124 | fn kill_and_write_pid_file(path: &Path) { 125 | fn kill_existing_process(path: &Path) { 126 | let pid = match fs::read_to_string(path) { 127 | Ok(pid) => pid, 128 | Err(e) if e.kind() == io::ErrorKind::NotFound => return, 129 | Err(e) => return log::warn!("Failed to read pid file `{}`: {e}", path.display()), 130 | }; 131 | 132 | let pid = match pid.parse::() { 133 | Ok(pid) => pid, 134 | Err(e) => return log::warn!("Failed to parse pid file `{}`: {e}", path.display()), 135 | }; 136 | 137 | let process = match unsafe { 138 | OpenProcess( 139 | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE | PROCESS_SYNCHRONIZE, 140 | false, 141 | pid, 142 | ) 143 | } { 144 | Ok(process) => process, 145 | // This happens normally when the process has already exited. 146 | Err(e) => return log::debug!("Failed to open process {pid}: {e}"), 147 | }; 148 | defer! { 149 | if let Err(e) = unsafe { CloseHandle(process) } { 150 | log::warn!("Failed to close process {pid}: {e}"); 151 | } 152 | } 153 | 154 | let mut name = [0; 4096]; 155 | let len = match unsafe { GetModuleFileNameExW(Some(process), None, &mut name) } { 156 | 0 => { 157 | return log::warn!( 158 | "Failed to get process name for pid {pid}: {}", 159 | Error::from_thread() 160 | ); 161 | } 162 | len => len, 163 | }; 164 | let name = &name[..len as usize]; 165 | 166 | let infoband = w!("infoband.exe"); 167 | if !name.ends_with(unsafe { infoband.as_wide() }) { 168 | return log::debug!( 169 | "Not killing process {pid} (`{}`)", 170 | String::from_utf16_lossy(name) 171 | ); 172 | } 173 | 174 | match unsafe { TerminateProcess(process, 0) } { 175 | Ok(()) => log::info!("Started termination of existing instance with pid {pid}"), 176 | Err(e) => return log::warn!("Failed to terminate process {pid}: {e}"), 177 | } 178 | 179 | match unsafe { WaitForSingleObject(process, EXISTING_PROCESS_SHUTDOWN_MS) } { 180 | WAIT_OBJECT_0 => { 181 | log::info!("Completed termination of existing instance with pid {pid}") 182 | } 183 | WAIT_TIMEOUT => log::warn!( 184 | "Existing instance with pid {pid} did not exit after {EXISTING_PROCESS_SHUTDOWN_MS}ms" 185 | ), 186 | _ => log::warn!( 187 | "Failed to wait for existing instance with pid {pid} to exit: {}", 188 | Error::from_thread() 189 | ), 190 | } 191 | } 192 | 193 | kill_existing_process(path); 194 | 195 | // SAFETY: not unsafe 196 | let current_pid = unsafe { GetCurrentProcessId() }; 197 | 198 | match fs::write(path, current_pid.to_string()) { 199 | Ok(()) => log::debug!("Wrote pid {current_pid} to file `{}`", path.display()), 200 | Err(e) => log::warn!("Failed to write pid file `{}`: {e}", path.display()), 201 | } 202 | } 203 | 204 | fn load_config_file(path: &Path) -> opt::ConfigFile { 205 | let default_config = opt::ConfigFile::default(); 206 | 207 | match File::open(path) { 208 | Ok(file) => match serde_json::from_reader(file) { 209 | Ok(config) => { 210 | log::info!("Loaded config from file `{}`", path.display()); 211 | config 212 | } 213 | Err(e) => { 214 | log::error!("Failed to parse config file `{}`: {e}", path.display()); 215 | default_config 216 | } 217 | }, 218 | Err(e) if e.kind() == io::ErrorKind::NotFound => { 219 | log::info!("Config file `{}` not found, creating", path.display()); 220 | match File::create(path) { 221 | Ok(file) => { 222 | if let Err(e) = serde_json::to_writer_pretty(file, &default_config) { 223 | log::warn!("Failed to write config file `{}`: {e}", path.display()); 224 | if let Err(e) = fs::remove_file(path) { 225 | log::warn!("...and failed to delete the empty file: {e}"); 226 | } 227 | } 228 | } 229 | Err(e) => log::warn!("Failed to create config file `{}`: {e}", path.display()), 230 | } 231 | default_config 232 | } 233 | Err(e) => { 234 | log::error!("Failed to load config file: {e}"); 235 | default_config 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/perf.rs: -------------------------------------------------------------------------------- 1 | use std::array; 2 | use std::marker::PhantomData; 3 | use std::mem; 4 | use std::ptr::addr_of_mut; 5 | use windows::Win32::Foundation::{HANDLE, WIN32_ERROR}; 6 | use windows::Win32::System::Performance::{ 7 | PERF_COUNTER_DATA, PERF_COUNTER_HEADER, PERF_COUNTER_IDENTIFIER, PERF_DATA_HEADER, 8 | PERF_SINGLE_COUNTER, PERF_WILDCARD_COUNTER, PerfAddCounters, PerfCloseQueryHandle, 9 | PerfCounterDataType, PerfOpenQueryHandle, PerfQueryCounterData, PerfQueryCounterInfo, 10 | }; 11 | use windows::core::{GUID, Result}; 12 | 13 | /// Represents the type of data that will be fetched from a performance counter, 14 | /// which impacts the memory layout of the blocks that will be generated by PerfQueryCounterData. 15 | /// Corresponds to the PerfCounterDataType enum. 16 | /// 17 | /// Workaround for lack of const generic enum variants. 18 | pub trait PerfCounterType { 19 | const TYPE: PerfCounterDataType; 20 | } 21 | 22 | /// Represents PERF_SINGLE_COUNTER, filtered to a single counter. 23 | pub struct SingleCounter; 24 | 25 | impl PerfCounterType for SingleCounter { 26 | const TYPE: PerfCounterDataType = PERF_SINGLE_COUNTER; 27 | } 28 | 29 | /// Represents an open performance query handle. 30 | /// Can be repeatedly queried to get perf data. 31 | pub struct PerfQueries 32 | where 33 | Type: PerfCounterType, 34 | CounterValue: Copy + Default, 35 | { 36 | /// The handle to the performance query. 37 | // SAFETY: must not be modified or dropped until this struct is dropped. 38 | handle: HANDLE, 39 | /// Indexes of the counters in query results (since for some reason this is not guaranteed) 40 | counter_indexes: [u32; COUNTERS], 41 | /// The structore of data that will be returned by this perf query (e.g. single or multi value). 42 | _type: PhantomData, 43 | /// The type of data that will be fetched from this handle. 44 | /// Usually u64 or u32. 45 | _value: PhantomData, 46 | } 47 | 48 | impl Drop for PerfQueries 49 | where 50 | Type: PerfCounterType, 51 | CounterValue: Copy + Default, 52 | { 53 | fn drop(&mut self) { 54 | // SAFETY: handle is valid and hasn't been closed due to our safety invariant. 55 | if let Err(e) = unsafe { WIN32_ERROR(PerfCloseQueryHandle(self.handle)).ok() } { 56 | log::error!("Failed to close PerfQueryHandle: {e}"); 57 | } 58 | } 59 | } 60 | 61 | impl PerfQueries 62 | where 63 | CounterValue: Copy + Default, 64 | { 65 | /// Query the given counterset, for the given counter ids, filtered to the given instance name filter. 66 | /// 67 | /// Since we expect there to be a single instance, the instance name filter cannot be `b"*"` or `b""`. 68 | pub fn new_filtered_to_single_counter( 69 | counterset: GUID, 70 | counter_ids: &[u32; COUNTERS], 71 | instance_name_filter: &[u8; N], 72 | ) -> Result { 73 | assert!(!matches!(instance_name_filter.as_slice(), b"" | b"*")); 74 | 75 | let instance_name_filter = instance_name_filter.map(|c| { 76 | let mut one_char = [0; 1]; 77 | let c = char::from_u32(u32::from(c)) 78 | .unwrap_or_else(|| panic!("Filter string must be valid UTF-8")); 79 | c.encode_utf16(&mut one_char); 80 | one_char[0] 81 | }); 82 | 83 | // Create handle to hold counters which we will repeatedly query. 84 | let handle = { 85 | let mut handle = HANDLE::default(); 86 | // SAFETY: handle is a valid pointer to PerfQueryHandle 87 | unsafe { WIN32_ERROR(PerfOpenQueryHandle(None, &mut handle)).ok()? }; 88 | handle 89 | }; 90 | 91 | // Create instance right after handle so the handle will be dropped if we error. 92 | let mut queries = PerfQueries { 93 | handle, 94 | counter_indexes: [0; COUNTERS], // will be filled in later 95 | _type: PhantomData, 96 | _value: PhantomData, 97 | }; 98 | 99 | // Add counters to the query handle. 100 | 101 | #[repr(C)] 102 | #[repr(align(8))] 103 | struct PERF_COUNTER_IDENTIFIER_WITH_NAME { 104 | identifier: PERF_COUNTER_IDENTIFIER, 105 | name_filter: [u16; N], 106 | null: u16, 107 | } 108 | 109 | let mut counters = counter_ids.map(|counter_id| PERF_COUNTER_IDENTIFIER_WITH_NAME { 110 | identifier: PERF_COUNTER_IDENTIFIER { 111 | CounterSetGuid: counterset, 112 | Size: mem::size_of::>() 113 | .try_into() 114 | .unwrap(), 115 | CounterId: counter_id, 116 | // Note that, per https://learn.microsoft.com/en-us/windows/win32/api/perflib/ns-perflib-perf_instance_header#remarks, 117 | // each instance is identified by _both_ its instance id and name combined... 118 | // In practice, I do see duplicate instance IDs frequently, but I don't see duplicate names, 119 | // so we use the wildcard instance ID here and only filter on the name (below). 120 | InstanceId: PERF_WILDCARD_COUNTER, 121 | ..Default::default() 122 | }, 123 | name_filter: instance_name_filter, 124 | null: 0, 125 | }); 126 | let counters_size = mem::size_of_val(&counters).try_into().unwrap(); 127 | 128 | // SAFETY: handle is valid, counters matches the defined layout for PERF_COUNTER_IDENTIFIER blocks. 129 | // https://learn.microsoft.com/en-us/windows/win32/api/perflib/ns-perflib-perf_counter_identifier 130 | unsafe { 131 | WIN32_ERROR(PerfAddCounters( 132 | handle, 133 | counters.as_mut_ptr().cast::(), 134 | counters_size, 135 | )) 136 | .ok()? 137 | }; 138 | 139 | // Consume status from adding each identifier. 140 | for counter in &counters { 141 | WIN32_ERROR(counter.identifier.Status).ok()?; 142 | } 143 | 144 | // Populate query indexes for the counters. 145 | // (For some reason data is not always returned in an order matching the order queries were added.) 146 | 147 | unsafe { 148 | WIN32_ERROR(PerfQueryCounterInfo( 149 | handle, 150 | Some(counters.as_mut_ptr().cast::()), 151 | counters_size, 152 | &mut 0, 153 | )) 154 | .ok()? 155 | }; 156 | 157 | queries.counter_indexes = counters.map(|counter| counter.identifier.Index); 158 | 159 | Ok(queries) 160 | } 161 | 162 | /// Query data from perf counters. 163 | pub fn query_data(&self) -> Result<[CounterValue; COUNTERS]> { 164 | // Get data from perf counters. 165 | // https://learn.microsoft.com/en-us/windows/win32/api/perflib/nf-perflib-perfquerycounterdata 166 | 167 | // Technically, I infer you are supposed to call PerfQueryCounterData first to determine how big of a buffer to allocate, 168 | // then call it again with the buffer. But since we are only querying for a specific fixed result, 169 | // make a struct that's exactly the right size, in the hope that it will generate that layout. 170 | // Just in case, we also check that the fields to ensure they match our expected layout. 171 | 172 | #[repr(C)] 173 | #[repr(align(8))] 174 | struct PerfDataResults { 175 | header: PERF_DATA_HEADER, 176 | counters: [PerfDataCounter; COUNTERS], 177 | } 178 | 179 | #[derive(Default)] 180 | #[repr(C)] 181 | struct PerfDataCounter { 182 | header: PERF_COUNTER_HEADER, 183 | data_prefix: PERF_COUNTER_DATA, 184 | value: CounterValue, 185 | } 186 | 187 | let mut results = PerfDataResults:: { 188 | header: Default::default(), 189 | counters: array::from_fn(|_| Default::default()), 190 | }; 191 | let results_size = mem::size_of_val(&results).try_into().unwrap(); 192 | 193 | // SAFETY: `handle` is valid; `results` pointer is valid for `writes` for `results_size` bytes 194 | unsafe { 195 | WIN32_ERROR(PerfQueryCounterData( 196 | self.handle, 197 | Some(addr_of_mut!(results).cast::()), 198 | results_size, 199 | &mut 0, 200 | )) 201 | .ok()? 202 | }; 203 | 204 | assert_eq!( 205 | results.header.dwNumCounters, COUNTERS as u32, 206 | "must have the correct number of counters" 207 | ); 208 | 209 | for (i, counter) in results.counters.iter().enumerate() { 210 | // Consume status from counter fetch 211 | WIN32_ERROR(counter.header.dwStatus).ok()?; 212 | 213 | assert_eq!( 214 | counter.header.dwType, 215 | SingleCounter::TYPE, 216 | "counter {i} must have correct type" 217 | ); 218 | assert_eq!( 219 | counter.data_prefix.dwDataSize, 220 | mem::size_of::() as u32, 221 | "data size must be valid" 222 | ); 223 | } 224 | 225 | let values = self 226 | .counter_indexes 227 | .map(|i| results.counters[i as usize].value); 228 | 229 | Ok(values) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /wix/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 81 | 82 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 99 | 100 | 105 | 106 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 138 | 139 | 140 | 141 | 142 | 143 | 152 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 195 | 1 196 | 1 197 | 198 | WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed 203 | 204 | 205 | 209 | 210 | 211 | 214 | 215 | 216 | 217 | 223 | 224 | 232 | 233 | 234 | 235 | 243 | 244 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /src/window/position.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::MICROPHONE_WARNING_WIDTH; 2 | use crate::utils::ScalingFactor; 3 | use crate::window::position::listener::TrayListenerManager; 4 | use std::cell::{Cell, RefCell}; 5 | use std::mem; 6 | use windows::Win32::Foundation::{ 7 | ERROR_EMPTY, ERROR_INVALID_WINDOW_HANDLE, ERROR_SUCCESS, HWND, RECT, 8 | }; 9 | use windows::Win32::Graphics::Gdi::{ 10 | GetMonitorInfoW, MONITOR_DEFAULTTOPRIMARY, MONITORINFO, MonitorFromWindow, 11 | }; 12 | use windows::Win32::System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance}; 13 | use windows::Win32::System::Variant::VARIANT; 14 | use windows::Win32::UI::Accessibility::{ 15 | CUIAutomation, IUIAutomation, IUIAutomationElement, ROLE_SYSTEM_PANE, ROLE_SYSTEM_PUSHBUTTON, 16 | TreeScope_Children, TreeScope_Descendants, UIA_LegacyIAccessibleRolePropertyId, 17 | }; 18 | use windows::Win32::UI::HiDpi::GetDpiForWindow; 19 | use windows::Win32::UI::WindowsAndMessaging::{ 20 | FindWindowW, GWL_EXSTYLE, GetWindowLongW, HWND_BOTTOM, HWND_TOPMOST, SWP_NOMOVE, 21 | SWP_NOSENDCHANGING, SWP_NOSIZE, SetWindowPos, USER_DEFAULT_SCREEN_DPI, WINDOW_EX_STYLE, 22 | WS_EX_TOPMOST, 23 | }; 24 | use windows::core::{Error, HRESULT, Result, w}; 25 | 26 | mod listener; 27 | 28 | /// Manages the position and z-order of the window. 29 | /// 30 | /// In order to properly handle fullscreen windows, we need to match the z-order of the Windows taskbar. 31 | /// Naturally, there is no proper API for this, and the logic that the taskbar itself uses is hacky and has bugs. 32 | /// Thankfully, we can mostly avoid this, by listening to the same messages that the taskbar does, 33 | /// but not actually performing the same logic, just checking whether the taskbar put itself on top or not and doing the same thing. 34 | /// 35 | /// Big thanks to RudeWindowFixer, which contains a reverse engineered description of the taskbar's logic: 36 | /// https://github.com/dechamps/RudeWindowFixer 37 | pub struct Position { 38 | automation: IUIAutomation, 39 | /// The shell window, displaying the Windows taskbar. 40 | shell: Cell, 41 | /// System tray area. 42 | tray: RefCell, 43 | /// DPI scaling factor of the window. 44 | dpi: Cell, 45 | /// Size and position of the taskbar on the primary monitor. 46 | taskbar: Cell, 47 | /// Left edge of the system tray area. 48 | tray_left_edge: Cell, 49 | /// Size and position of the window. 50 | rect: Cell, 51 | /// Whether our window is currently topmost. 52 | currently_topmost: Cell>, 53 | } 54 | 55 | impl Position { 56 | pub fn new(window: HWND) -> Result { 57 | let automation: IUIAutomation = 58 | unsafe { CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER)? }; 59 | 60 | let (shell, tray) = get_shell_window_and_system_tray(&automation)?; 61 | 62 | let tray = TrayListenerManager::new(window, automation.clone(), tray)?; 63 | 64 | let dpi = unsafe { GetDpiForWindow(shell) }; 65 | let dpi = ScalingFactor::from_ratio(dpi, USER_DEFAULT_SCREEN_DPI); 66 | 67 | // Window starts out not displayed, with size and position zero. 68 | let empty = RECT { 69 | top: 0, 70 | bottom: 0, 71 | left: 0, 72 | right: 0, 73 | }; 74 | 75 | Ok(Self { 76 | automation, 77 | shell: Cell::new(shell), 78 | tray: RefCell::new(tray), 79 | dpi: Cell::new(dpi), 80 | taskbar: Cell::new(empty), 81 | tray_left_edge: Cell::new(0), 82 | rect: Cell::new(empty), 83 | // Initial state is "unknown". 84 | // (We always need to call it at least once, since it might not be all the way on the top or bottom.) 85 | currently_topmost: Cell::new(None), 86 | }) 87 | } 88 | 89 | pub fn get(&self) -> (ScalingFactor, RECT) { 90 | (self.dpi.get(), self.rect.get()) 91 | } 92 | 93 | pub fn set_dpi(&self, dpi_raw: u32) { 94 | let dpi = ScalingFactor::from_ratio(dpi_raw, USER_DEFAULT_SCREEN_DPI); 95 | self.dpi.set(dpi); 96 | } 97 | 98 | pub fn update_taskbar_position(&self) { 99 | match self.get_taskbar_position() { 100 | Ok(rect) => { 101 | self.taskbar.set(rect); 102 | } 103 | Err(e) => { 104 | log::error!("Update taskbar position failed, preserving old position: {e}"); 105 | } 106 | } 107 | } 108 | 109 | fn get_taskbar_position(&self) -> Result { 110 | let monitor = unsafe { MonitorFromWindow(self.shell.get(), MONITOR_DEFAULTTOPRIMARY) }; 111 | 112 | // Get size of primary monitor 113 | let monitor_info = { 114 | let mut monitor_info = MONITORINFO { 115 | cbSize: mem::size_of::() as u32, 116 | ..Default::default() 117 | }; 118 | // SAFETY: lpmi is valid pointer to MONITORINFO 119 | unsafe { GetMonitorInfoW(monitor, &mut monitor_info).ok()? }; 120 | monitor_info 121 | }; 122 | 123 | // Taskbar is below the work area to the edges of the monitor 124 | Ok(RECT { 125 | top: monitor_info.rcWork.bottom, 126 | bottom: monitor_info.rcMonitor.bottom, 127 | left: monitor_info.rcMonitor.left, 128 | right: monitor_info.rcMonitor.right, 129 | }) 130 | } 131 | 132 | pub fn update_tray_position(&self) { 133 | match self.get_left_edge_of_tray() { 134 | Ok(left_edge) => { 135 | self.tray_left_edge.set(left_edge); 136 | } 137 | Err(e) => { 138 | log::error!("Update tray left edge failed, preserving old position: {e}"); 139 | } 140 | } 141 | } 142 | 143 | fn get_left_edge_of_tray(&self) -> Result { 144 | let first_tray_button = 145 | get_first_tray_button(&self.automation, self.tray.borrow().element())?; 146 | 147 | let rect = unsafe { first_tray_button.CurrentBoundingRectangle()? }; 148 | 149 | Ok(rect.left) 150 | } 151 | 152 | #[must_use = "Window position must be applied after recomputing"] 153 | pub fn recompute(&self) -> (ScalingFactor, RECT) { 154 | match self.recompute_fallible() { 155 | Ok(rect) => { 156 | self.rect.set(rect); 157 | } 158 | Err(e) => { 159 | log::error!("Update window position failed, preserving old position: {e}"); 160 | } 161 | } 162 | 163 | (self.dpi.get(), self.rect.get()) 164 | } 165 | 166 | fn recompute_fallible(&self) -> Result { 167 | let dpi = self.dpi.get(); 168 | let taskbar = self.taskbar.get(); 169 | let tray_left_edge = self.tray_left_edge.get(); 170 | 171 | let midpoint = |a, b| a + (b - a) / 2; 172 | 173 | // Height is always the size of the taskbar 174 | let top = taskbar.top; 175 | let bottom = taskbar.bottom; 176 | // Right edge is adjacent to right edge of system tray 177 | let right = tray_left_edge; 178 | // Left edge positioned at the horizontal center of the display, with enough room for the mic warning 179 | let left = 180 | midpoint(taskbar.left, taskbar.right) - MICROPHONE_WARNING_WIDTH.scale_by(dpi) / 2; 181 | 182 | if top == bottom || left == right { 183 | return Err(Error::new(ERROR_EMPTY.into(), "Draw rectange is empty")); 184 | } 185 | 186 | Ok(RECT { 187 | top, 188 | bottom, 189 | left, 190 | right, 191 | }) 192 | } 193 | 194 | /// Set our window's z-order to match the taskbar's. 195 | pub fn update_z_order(&self, window: HWND) { 196 | if let Err(e) = self.update_z_order_fallible(window) { 197 | log::error!("Z-order update failed: {e}"); 198 | } 199 | } 200 | 201 | fn update_z_order_fallible(&self, window: HWND) -> Result<()> { 202 | let is_shell_topmost = self.is_shell_topmost()?; 203 | 204 | self.set_z_order_to(window, is_shell_topmost)?; 205 | 206 | Ok(()) 207 | } 208 | 209 | fn is_shell_topmost(&self) -> Result { 210 | match is_window_topmost(self.shell.get()) { 211 | Ok(is_topmost) => Ok(is_topmost), 212 | Err(e) if e.code() == HRESULT::from(ERROR_INVALID_WINDOW_HANDLE) => { 213 | log::warn!("Shell window handle is invalid (explorer crashed?); refetching"); 214 | let (shell, tray) = get_shell_window_and_system_tray(&self.automation)?; 215 | self.shell.set(shell); 216 | self.tray.borrow_mut().refresh_element(tray)?; 217 | 218 | is_window_topmost(self.shell.get()) 219 | } 220 | Err(e) => Err(e), 221 | } 222 | } 223 | 224 | fn set_z_order_to(&self, window: HWND, topmost: bool) -> Result<()> { 225 | log::debug!("Setting z-order to topmost={topmost}"); 226 | 227 | unsafe { 228 | SetWindowPos( 229 | window, 230 | Some(if topmost { HWND_TOPMOST } else { HWND_BOTTOM }), 231 | 0, 232 | 0, 233 | 0, 234 | 0, 235 | SWP_NOMOVE | SWP_NOSIZE | SWP_NOSENDCHANGING, 236 | )? 237 | }; 238 | 239 | self.currently_topmost.set(Some(topmost)); 240 | 241 | Ok(()) 242 | } 243 | } 244 | 245 | fn get_shell_window_and_system_tray( 246 | automation: &IUIAutomation, 247 | ) -> Result<(HWND, IUIAutomationElement)> { 248 | let shell = unsafe { FindWindowW(w!("Shell_TrayWnd"), None)? }; 249 | 250 | let role_is_pane = unsafe { 251 | automation.CreatePropertyCondition( 252 | UIA_LegacyIAccessibleRolePropertyId, 253 | &VARIANT::from(ROLE_SYSTEM_PANE as i32), 254 | )? 255 | }; 256 | 257 | let tray = match unsafe { 258 | automation 259 | .ElementFromHandle(shell)? 260 | .FindFirst(TreeScope_Descendants, &role_is_pane) 261 | } { 262 | Ok(tray) => tray, 263 | Err(e) if e.code() == HRESULT::from(ERROR_SUCCESS) => { 264 | return Err(Error::new( 265 | ERROR_EMPTY.into(), 266 | "System tray not found in shell window", 267 | )); 268 | } 269 | Err(e) => return Err(e), 270 | }; 271 | 272 | Ok((shell, tray)) 273 | } 274 | 275 | fn get_first_tray_button( 276 | automation: &IUIAutomation, 277 | tray: &IUIAutomationElement, 278 | ) -> Result { 279 | let role_is_pushbutton = unsafe { 280 | automation.CreatePropertyCondition( 281 | UIA_LegacyIAccessibleRolePropertyId, 282 | &VARIANT::from(ROLE_SYSTEM_PUSHBUTTON as i32), 283 | )? 284 | }; 285 | 286 | match unsafe { tray.FindFirst(TreeScope_Children, &role_is_pushbutton) } { 287 | Ok(first_tray_button) => Ok(first_tray_button), 288 | Err(e) if e.code() == HRESULT::from(ERROR_SUCCESS) => Err(Error::new( 289 | ERROR_EMPTY.into(), 290 | "No tray buttons found in system tray", 291 | )), 292 | Err(e) => Err(e), 293 | } 294 | } 295 | 296 | fn is_window_topmost(handle: HWND) -> Result { 297 | let style = { 298 | let res = unsafe { GetWindowLongW(handle, GWL_EXSTYLE) }; 299 | if res == 0 { 300 | return Err(Error::from_thread()); 301 | } 302 | WINDOW_EX_STYLE(res as u32) 303 | }; 304 | 305 | Ok(style.contains(WS_EX_TOPMOST)) 306 | } 307 | -------------------------------------------------------------------------------- /src/window/messages.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | use std::sync::{Mutex, OnceLock, PoisonError}; 4 | use windows::Win32::System::DataExchange::GetClipboardFormatNameW; 5 | use windows::Win32::UI::WindowsAndMessaging::WM_USER; 6 | 7 | static MESSAGES: OnceLock> = OnceLock::new(); 8 | 9 | fn messages() -> &'static HashMap { 10 | MESSAGES.get_or_init( 11 | #[cold] 12 | #[inline(never)] 13 | || { 14 | use windows::Win32::UI::WindowsAndMessaging::*; 15 | 16 | let mut map = HashMap::new(); 17 | 18 | macro_rules! add { 19 | ($($msg:ident),* $(,)?) => { 20 | $( 21 | map.insert($msg, stringify!($msg)); 22 | )* 23 | }; 24 | } 25 | 26 | add!( 27 | WM_ACTIVATE, 28 | WM_ACTIVATEAPP, 29 | WM_AFXFIRST, 30 | WM_AFXLAST, 31 | WM_APP, 32 | WM_APPCOMMAND, 33 | WM_ASKCBFORMATNAME, 34 | WM_CANCELJOURNAL, 35 | WM_CANCELMODE, 36 | WM_CAPTURECHANGED, 37 | WM_CHANGECBCHAIN, 38 | WM_CHANGEUISTATE, 39 | WM_CHAR, 40 | WM_CHARTOITEM, 41 | WM_CHILDACTIVATE, 42 | WM_CLEAR, 43 | WM_CLIPBOARDUPDATE, 44 | WM_CLOSE, 45 | WM_COMMAND, 46 | WM_COMMNOTIFY, 47 | WM_COMPACTING, 48 | WM_COMPAREITEM, 49 | WM_CONTEXTMENU, 50 | WM_COPY, 51 | WM_COPYDATA, 52 | WM_CREATE, 53 | WM_CTLCOLORBTN, 54 | WM_CTLCOLORDLG, 55 | WM_CTLCOLOREDIT, 56 | WM_CTLCOLORLISTBOX, 57 | WM_CTLCOLORMSGBOX, 58 | WM_CTLCOLORSCROLLBAR, 59 | WM_CTLCOLORSTATIC, 60 | WM_CUT, 61 | WM_DEADCHAR, 62 | WM_DELETEITEM, 63 | WM_DESTROY, 64 | WM_DESTROYCLIPBOARD, 65 | WM_DEVICECHANGE, 66 | WM_DEVMODECHANGE, 67 | WM_DISPLAYCHANGE, 68 | WM_DPICHANGED, 69 | WM_DPICHANGED_AFTERPARENT, 70 | WM_DPICHANGED_BEFOREPARENT, 71 | WM_DRAWCLIPBOARD, 72 | WM_DRAWITEM, 73 | WM_DROPFILES, 74 | WM_DWMCOLORIZATIONCOLORCHANGED, 75 | WM_DWMCOMPOSITIONCHANGED, 76 | WM_DWMNCRENDERINGCHANGED, 77 | WM_DWMSENDICONICLIVEPREVIEWBITMAP, 78 | WM_DWMSENDICONICTHUMBNAIL, 79 | WM_DWMWINDOWMAXIMIZEDCHANGE, 80 | WM_ENABLE, 81 | WM_ENDSESSION, 82 | WM_ENTERIDLE, 83 | WM_ENTERMENULOOP, 84 | WM_ENTERSIZEMOVE, 85 | WM_ERASEBKGND, 86 | WM_EXITMENULOOP, 87 | WM_EXITSIZEMOVE, 88 | WM_FONTCHANGE, 89 | WM_GESTURE, 90 | WM_GESTURENOTIFY, 91 | WM_GETDLGCODE, 92 | WM_GETDPISCALEDSIZE, 93 | WM_GETFONT, 94 | WM_GETHOTKEY, 95 | WM_GETICON, 96 | WM_GETMINMAXINFO, 97 | WM_GETOBJECT, 98 | WM_GETTEXT, 99 | WM_GETTEXTLENGTH, 100 | WM_GETTITLEBARINFOEX, 101 | WM_HANDHELDFIRST, 102 | WM_HANDHELDLAST, 103 | WM_HELP, 104 | WM_HOTKEY, 105 | WM_HSCROLL, 106 | WM_HSCROLLCLIPBOARD, 107 | WM_ICONERASEBKGND, 108 | WM_IME_CHAR, 109 | WM_IME_COMPOSITION, 110 | WM_IME_COMPOSITIONFULL, 111 | WM_IME_CONTROL, 112 | WM_IME_ENDCOMPOSITION, 113 | WM_IME_KEYDOWN, 114 | WM_IME_KEYLAST, 115 | WM_IME_KEYUP, 116 | WM_IME_NOTIFY, 117 | WM_IME_REQUEST, 118 | WM_IME_SELECT, 119 | WM_IME_SETCONTEXT, 120 | WM_IME_STARTCOMPOSITION, 121 | WM_INITDIALOG, 122 | WM_INITMENU, 123 | WM_INITMENUPOPUP, 124 | WM_INPUT, 125 | WM_INPUTLANGCHANGE, 126 | WM_INPUTLANGCHANGEREQUEST, 127 | WM_INPUT_DEVICE_CHANGE, 128 | WM_KEYDOWN, 129 | WM_KEYFIRST, 130 | WM_KEYLAST, 131 | WM_KEYUP, 132 | WM_KILLFOCUS, 133 | WM_LBUTTONDBLCLK, 134 | WM_LBUTTONDOWN, 135 | WM_LBUTTONUP, 136 | WM_MBUTTONDBLCLK, 137 | WM_MBUTTONDOWN, 138 | WM_MBUTTONUP, 139 | WM_MDIACTIVATE, 140 | WM_MDICASCADE, 141 | WM_MDICREATE, 142 | WM_MDIDESTROY, 143 | WM_MDIGETACTIVE, 144 | WM_MDIICONARRANGE, 145 | WM_MDIMAXIMIZE, 146 | WM_MDINEXT, 147 | WM_MDIREFRESHMENU, 148 | WM_MDIRESTORE, 149 | WM_MDISETMENU, 150 | WM_MDITILE, 151 | WM_MEASUREITEM, 152 | WM_MENUCHAR, 153 | WM_MENUCOMMAND, 154 | WM_MENUDRAG, 155 | WM_MENUGETOBJECT, 156 | WM_MENURBUTTONUP, 157 | WM_MENUSELECT, 158 | WM_MOUSEACTIVATE, 159 | WM_MOUSEFIRST, 160 | WM_MOUSEHWHEEL, 161 | WM_MOUSELAST, 162 | WM_MOUSEMOVE, 163 | WM_MOUSEWHEEL, 164 | WM_MOVE, 165 | WM_MOVING, 166 | WM_NCACTIVATE, 167 | WM_NCCALCSIZE, 168 | WM_NCCREATE, 169 | WM_NCDESTROY, 170 | WM_NCHITTEST, 171 | WM_NCLBUTTONDBLCLK, 172 | WM_NCLBUTTONDOWN, 173 | WM_NCLBUTTONUP, 174 | WM_NCMBUTTONDBLCLK, 175 | WM_NCMBUTTONDOWN, 176 | WM_NCMBUTTONUP, 177 | WM_NCMOUSEHOVER, 178 | WM_NCMOUSELEAVE, 179 | WM_NCMOUSEMOVE, 180 | WM_NCPAINT, 181 | WM_NCPOINTERDOWN, 182 | WM_NCPOINTERUP, 183 | WM_NCPOINTERUPDATE, 184 | WM_NCRBUTTONDBLCLK, 185 | WM_NCRBUTTONDOWN, 186 | WM_NCRBUTTONUP, 187 | WM_NCXBUTTONDBLCLK, 188 | WM_NCXBUTTONDOWN, 189 | WM_NCXBUTTONUP, 190 | WM_NEXTDLGCTL, 191 | WM_NEXTMENU, 192 | WM_NOTIFY, 193 | WM_NOTIFYFORMAT, 194 | WM_NULL, 195 | WM_PAINT, 196 | WM_PAINTCLIPBOARD, 197 | WM_PAINTICON, 198 | WM_PALETTECHANGED, 199 | WM_PALETTEISCHANGING, 200 | WM_PARENTNOTIFY, 201 | WM_PASTE, 202 | WM_PENWINFIRST, 203 | WM_PENWINLAST, 204 | WM_POINTERACTIVATE, 205 | WM_POINTERCAPTURECHANGED, 206 | WM_POINTERDEVICECHANGE, 207 | WM_POINTERDEVICEINRANGE, 208 | WM_POINTERDEVICEOUTOFRANGE, 209 | WM_POINTERDOWN, 210 | WM_POINTERENTER, 211 | WM_POINTERHWHEEL, 212 | WM_POINTERLEAVE, 213 | WM_POINTERROUTEDAWAY, 214 | WM_POINTERROUTEDRELEASED, 215 | WM_POINTERROUTEDTO, 216 | WM_POINTERUP, 217 | WM_POINTERUPDATE, 218 | WM_POINTERWHEEL, 219 | WM_POWER, 220 | WM_POWERBROADCAST, 221 | WM_PRINT, 222 | WM_PRINTCLIENT, 223 | WM_QUERYDRAGICON, 224 | WM_QUERYENDSESSION, 225 | WM_QUERYNEWPALETTE, 226 | WM_QUERYOPEN, 227 | WM_QUERYUISTATE, 228 | WM_QUEUESYNC, 229 | WM_QUIT, 230 | WM_RBUTTONDBLCLK, 231 | WM_RBUTTONDOWN, 232 | WM_RBUTTONUP, 233 | WM_RENDERALLFORMATS, 234 | WM_RENDERFORMAT, 235 | WM_SETCURSOR, 236 | WM_SETFOCUS, 237 | WM_SETFONT, 238 | WM_SETHOTKEY, 239 | WM_SETICON, 240 | WM_SETREDRAW, 241 | WM_SETTEXT, 242 | WM_SETTINGCHANGE, 243 | WM_SHOWWINDOW, 244 | WM_SIZE, 245 | WM_SIZECLIPBOARD, 246 | WM_SIZING, 247 | WM_SPOOLERSTATUS, 248 | WM_STYLECHANGED, 249 | WM_STYLECHANGING, 250 | WM_SYNCPAINT, 251 | WM_SYSCHAR, 252 | WM_SYSCOLORCHANGE, 253 | WM_SYSCOMMAND, 254 | WM_SYSDEADCHAR, 255 | WM_SYSKEYDOWN, 256 | WM_SYSKEYUP, 257 | WM_TABLET_FIRST, 258 | WM_TABLET_LAST, 259 | WM_TCARD, 260 | WM_THEMECHANGED, 261 | WM_TIMECHANGE, 262 | WM_TIMER, 263 | WM_TOOLTIPDISMISS, 264 | WM_TOUCH, 265 | WM_TOUCHHITTESTING, 266 | WM_UNDO, 267 | WM_UNICHAR, 268 | WM_UNINITMENUPOPUP, 269 | WM_UPDATEUISTATE, 270 | WM_USER, 271 | WM_USERCHANGED, 272 | WM_VKEYTOITEM, 273 | WM_VSCROLL, 274 | WM_VSCROLLCLIPBOARD, 275 | WM_WINDOWPOSCHANGED, 276 | WM_WINDOWPOSCHANGING, 277 | WM_WININICHANGE, 278 | WM_WTSSESSION_CHANGE, 279 | WM_XBUTTONDBLCLK, 280 | WM_XBUTTONDOWN, 281 | WM_XBUTTONUP, 282 | ); 283 | map 284 | }, 285 | ) 286 | } 287 | 288 | static STRING_MESSAGE_NAMES: Mutex>> = Mutex::new(None); 289 | 290 | #[cold] 291 | #[inline(never)] 292 | fn get_string_message_name(msg: &u32) -> String { 293 | let mut name = [0; 256]; 294 | // SAFETY: no safety requirements 295 | let len = unsafe { GetClipboardFormatNameW(*msg, &mut name) }; 296 | String::from_utf16_lossy(&name[..len as usize]) 297 | } 298 | 299 | pub struct Name(pub u32); 300 | 301 | impl Display for Name { 302 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 303 | match self.0 { 304 | // User messages https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-user 305 | WM_USER..=0x7fff => write!(f, "WM_USER+{}", self.0 - WM_USER), 306 | // String-based messages https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-user 307 | 0xC000..=0xFFFF => { 308 | let mut map = STRING_MESSAGE_NAMES 309 | .lock() 310 | .unwrap_or_else(PoisonError::into_inner); 311 | let name = map 312 | .get_or_insert_default() 313 | .entry(self.0) 314 | .or_insert_with_key(get_string_message_name); 315 | f.write_str(name) 316 | } 317 | _ => match messages().get(&self.0) { 318 | // Predefined messages 319 | Some(&name) => f.write_str(name), 320 | // Unknown messages 321 | None => write!(f, "0x{:04x}", self.0), 322 | }, 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/window/paint.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ 2 | DEBUG_BACKGROUND_COLOR, FIRST_LINE_MIDPOINT_OFFSET_FROM_TOP, LABEL_WIDTH, 3 | MICROPHONE_WARNING_COLOR, MICROPHONE_WARNING_WIDTH, RIGHT_COLUMN_WIDTH, 4 | SECOND_LINE_MIDPOINT_OFFSET_FROM_TOP, 5 | }; 6 | use crate::defer; 7 | use crate::metrics::Metrics; 8 | use crate::utils::{RectExt, ScaleBy, ScalingFactor}; 9 | use std::cell::Cell; 10 | use std::mem; 11 | use windows::Win32::Foundation::{ 12 | COLORREF, ERROR_DC_NOT_FOUND, ERROR_FILE_NOT_FOUND, HWND, POINT, RECT, 13 | }; 14 | use windows::Win32::Graphics::Gdi::{ 15 | AC_SRC_ALPHA, AC_SRC_OVER, BLENDFUNCTION, CreateSolidBrush, DT_NOCLIP, DT_NOPREFIX, 16 | DT_SINGLELINE, DeleteObject, FillRect, GetDC, HBRUSH, HDC, ReleaseDC, 17 | }; 18 | use windows::Win32::UI::Controls::{ 19 | BP_PAINTPARAMS, BPBF_TOPDOWNDIB, BPPF_ERASE, BPPF_NOCLIP, BeginBufferedPaint, 20 | BufferedPaintInit, BufferedPaintSetAlpha, BufferedPaintUnInit, CloseThemeData, DTT_COMPOSITED, 21 | DTT_TEXTCOLOR, DTTOPTS, DrawThemeTextEx, EndBufferedPaint, GetThemeTextExtent, HTHEME, 22 | TEXT_BODYTEXT, 23 | }; 24 | use windows::Win32::UI::HiDpi::OpenThemeDataForDpi; 25 | use windows::Win32::UI::WindowsAndMessaging::{ 26 | ULW_ALPHA, USER_DEFAULT_SCREEN_DPI, UpdateLayeredWindow, 27 | }; 28 | use windows::core::{Error, Result, w}; 29 | 30 | pub struct Paint { 31 | /// SAFETY: must only be provided after calling `BufferedPaintInit`. 32 | called_buffered_paint_init: (), 33 | /// Whether to make the window more visible for debugging. 34 | debug: Cell, 35 | /// Brush for drawing the debug background. 36 | debug_background_brush: HBRUSH, 37 | /// Brush for drawing the microphone warning. 38 | microphone_warning_brush: HBRUSH, 39 | } 40 | 41 | impl Drop for Paint { 42 | fn drop(&mut self) { 43 | _ = self.called_buffered_paint_init; 44 | // SAFETY: init and uninit must be called in pairs; we call init when constructing this type 45 | if let Err(e) = unsafe { BufferedPaintUnInit() } { 46 | log::error!("BufferedPaintUnInit failed: {e}"); 47 | } 48 | 49 | if !unsafe { DeleteObject(self.debug_background_brush.into()) }.as_bool() { 50 | log::error!("DeleteObject failed: {}", Error::from_thread()); 51 | } 52 | 53 | if !unsafe { DeleteObject(self.microphone_warning_brush.into()) }.as_bool() { 54 | log::error!("DeleteObject failed: {}", Error::from_thread()); 55 | } 56 | } 57 | } 58 | 59 | impl Paint { 60 | pub fn new() -> Result { 61 | let debug_background_brush = unsafe { CreateSolidBrush(DEBUG_BACKGROUND_COLOR) }; 62 | if debug_background_brush.is_invalid() { 63 | return Err(Error::from_thread()); 64 | } 65 | 66 | let microphone_warning_brush = unsafe { CreateSolidBrush(MICROPHONE_WARNING_COLOR) }; 67 | if microphone_warning_brush.is_invalid() { 68 | return Err(Error::from_thread()); 69 | } 70 | 71 | Ok(Self { 72 | debug: Cell::new(false), 73 | debug_background_brush, 74 | microphone_warning_brush, 75 | called_buffered_paint_init: { 76 | // SAFETY: init and uninit must be called in pairs; after this point, we construct self, so drop will call uninit 77 | unsafe { BufferedPaintInit()? } 78 | }, 79 | // ...DO NOT add more fields after this... 80 | }) 81 | } 82 | 83 | pub fn set_debug(&self, debug: bool) { 84 | self.debug.set(debug); 85 | } 86 | 87 | /// Paint the window using the window's device context. 88 | pub fn render( 89 | &self, 90 | window: HWND, 91 | dpi: ScalingFactor, 92 | rect: RECT, 93 | metrics: &Metrics, 94 | is_muted: bool, 95 | ) { 96 | if let Err(e) = self.render_fallible(window, dpi, rect, metrics, is_muted) { 97 | log::error!("Paint failed: {e}"); 98 | } 99 | } 100 | 101 | /// Toplevel paint method, responsible for dealing with paint buffering and updating the window, 102 | /// but not with drawing any content. 103 | fn render_fallible( 104 | &self, 105 | window: HWND, 106 | dpi: ScalingFactor, 107 | rect: RECT, 108 | metrics: &Metrics, 109 | is_muted: bool, 110 | ) -> Result<()> { 111 | let size = rect.size(); 112 | let position = rect.top_left_corner(); 113 | 114 | // Fetch win HDC so we can create temporary mem HDC of the same size. 115 | let win_hdc = unsafe { GetDC(Some(window)) }; 116 | if win_hdc.is_invalid() { 117 | return Err(Error::from(ERROR_DC_NOT_FOUND)); 118 | } 119 | defer! { 120 | _ = unsafe { ReleaseDC(Some(window), win_hdc) }; 121 | } 122 | 123 | // Use buffered paint to draw into temporary mem HDC... 124 | let (hdc, buffered_paint) = { 125 | let mut hdc = HDC::default(); 126 | let buffered_paint = unsafe { 127 | BeginBufferedPaint( 128 | win_hdc, 129 | &RECT::from_size(size), 130 | // Required for us to manually write the background when debugging. 131 | // Required for DTT_COMPOSITED to work. 132 | // Always 8bpc, regardless of color depth of current monitor. 133 | // (This isn't a big deal since we're only drawing white + transparency, so we don't need HDR.) 134 | // 135 | // Note that BPBF_COMPATIBLEBITMAP is recommended for hidpi applications: 136 | // https://blogs.windows.com/windowsdeveloper/2017/05/19/improving-high-dpi-experience-gdi-based-desktop-apps/ 137 | // But as far as I can tell, this is only because it works with GDI scaling, 138 | // which is a hack / compatibility layer to make non-DPI-aware apps render some elements at higher DPI. 139 | // But we don't need GDI scaling, since we declare ourselves DPI aware, so none of our windows get scaled 140 | // and we just draw everything normally (but with manually-computed larger sizes) at the physical screen resolution. 141 | BPBF_TOPDOWNDIB, 142 | Some(&BP_PAINTPARAMS { 143 | cbSize: mem::size_of::() as u32, 144 | // Do not clip the contents (not necessary as we don't have window overlap), 145 | // and make sure to erase the buffer so we don't get artifacts from previous paints. 146 | dwFlags: BPPF_NOCLIP | BPPF_ERASE, 147 | ..Default::default() 148 | }), 149 | &mut hdc, 150 | ) 151 | }; 152 | if buffered_paint == 0 { 153 | return Err(Error::from_thread()); 154 | } 155 | (hdc, buffered_paint) 156 | }; 157 | defer! { 158 | // ...and don't update (false) the underlying window... 159 | if let Err(e) = unsafe { EndBufferedPaint(buffered_paint, false) } { 160 | log::error!("EndBufferedPaint failed: {e}"); 161 | } 162 | } 163 | 164 | // ...draw the content... 165 | self.draw_content(hdc, buffered_paint, dpi, rect, metrics, is_muted)?; 166 | 167 | // ...and then write the temporary mem HDC to the window, with alpha blending. 168 | unsafe { 169 | UpdateLayeredWindow( 170 | window, 171 | None, 172 | Some(&position), 173 | Some(&size), 174 | Some(hdc), 175 | Some(&POINT { x: 0, y: 0 }), 176 | COLORREF(0), 177 | Some(&BLENDFUNCTION { 178 | BlendOp: AC_SRC_OVER as u8, 179 | SourceConstantAlpha: 255, 180 | AlphaFormat: AC_SRC_ALPHA as u8, // Use source alpha channel 181 | ..Default::default() 182 | }), 183 | ULW_ALPHA, 184 | )? 185 | }; 186 | 187 | Ok(()) 188 | } 189 | 190 | /// Draw the window content to the given device context. 191 | fn draw_content( 192 | &self, 193 | hdc: HDC, 194 | buffered_paint: isize, 195 | dpi: ScalingFactor, 196 | rect: RECT, 197 | metrics: &Metrics, 198 | is_muted: bool, 199 | ) -> Result<()> { 200 | let size = rect.size(); 201 | 202 | let text_style = unsafe { 203 | OpenThemeDataForDpi(None, w!("TEXTSTYLE"), USER_DEFAULT_SCREEN_DPI.scale_by(dpi)) 204 | }; 205 | if text_style.is_invalid() { 206 | return Err(Error::from(ERROR_FILE_NOT_FOUND)); 207 | } 208 | defer! { 209 | if let Err(e) = unsafe { CloseThemeData(text_style) } { 210 | log::error!("CloseThemeData failed: {e}"); 211 | } 212 | } 213 | 214 | let middle_at = |x, y| { 215 | move |r: RECT| { 216 | r.with_horizontal_midpoint_at(x) 217 | .with_vertical_midpoint_at(y) 218 | } 219 | }; 220 | let right_mid_at = 221 | |x, y| move |r: RECT| r.with_right_edge_at(x).with_vertical_midpoint_at(y); 222 | let left_mid_at = |x, y| move |r: RECT| r.with_left_edge_at(x).with_vertical_midpoint_at(y); 223 | 224 | let rect = |r: RECT, color: HBRUSH| { 225 | if unsafe { FillRect(hdc, &r, color) } == 0 { 226 | return Err(Error::from_thread()); 227 | } 228 | // GDI does not properly support alpha, so we need to set the alpha channel manually afterwards. 229 | unsafe { BufferedPaintSetAlpha(buffered_paint, Some(&r), 255)? }; 230 | Ok(()) 231 | }; 232 | 233 | let text = |text: &str, position: &dyn Fn(RECT) -> RECT| { 234 | draw_text(hdc, text_style, text, position) 235 | }; 236 | 237 | // When debugging is enabled, fill in window background. 238 | 239 | if self.debug.get() { 240 | rect(RECT::from_size(size), self.debug_background_brush)?; 241 | } 242 | 243 | // Draw microphone warning if unmuted 244 | 245 | if !is_muted { 246 | rect( 247 | RECT { 248 | top: 0, 249 | left: 0, 250 | bottom: size.cy, 251 | right: MICROPHONE_WARNING_WIDTH.scale_by(dpi), 252 | }, 253 | self.microphone_warning_brush, 254 | )?; 255 | 256 | text( 257 | "🎤", 258 | &middle_at(MICROPHONE_WARNING_WIDTH.scale_by(dpi) / 2, size.cy / 2), 259 | )?; 260 | } 261 | 262 | // Draw metrics 263 | 264 | let cpu = metrics.avg_cpu_percent(); 265 | let mem = metrics.avg_memory_percent(); 266 | let net = metrics.avg_network_mbit(); 267 | let dsk = metrics.avg_disk_mbyte(); 268 | 269 | let right_column = size.cx - LABEL_WIDTH.scale_by(dpi); 270 | let left_column = size.cx - RIGHT_COLUMN_WIDTH.scale_by(dpi) - LABEL_WIDTH.scale_by(dpi); 271 | 272 | let first_line_midpoint = FIRST_LINE_MIDPOINT_OFFSET_FROM_TOP.scale_by(dpi); 273 | let second_line_midpoint = SECOND_LINE_MIDPOINT_OFFSET_FROM_TOP.scale_by(dpi); 274 | 275 | text(" CPU", &left_mid_at(right_column, first_line_midpoint))?; 276 | text(" RAM", &left_mid_at(right_column, second_line_midpoint))?; 277 | text(" NET", &left_mid_at(left_column, first_line_midpoint))?; 278 | text(" DSK", &left_mid_at(left_column, second_line_midpoint))?; 279 | 280 | text( 281 | &format!("{cpu:.0}%"), 282 | &right_mid_at(right_column, first_line_midpoint), 283 | )?; 284 | text( 285 | &format!("{mem:.0}%"), 286 | &right_mid_at(right_column, second_line_midpoint), 287 | )?; 288 | text( 289 | &format!("{net:.0} Mb/s"), 290 | &right_mid_at(left_column, first_line_midpoint), 291 | )?; 292 | text( 293 | &format!("{dsk:.0} MB/s"), 294 | &right_mid_at(left_column, second_line_midpoint), 295 | )?; 296 | 297 | Ok(()) 298 | } 299 | } 300 | 301 | fn draw_text( 302 | hdc: HDC, 303 | text_style: HTHEME, 304 | text: &str, 305 | position: impl FnOnce(RECT) -> RECT, 306 | ) -> Result<()> { 307 | let text: &[u16] = &text.encode_utf16().collect::>(); 308 | 309 | let partid = TEXT_BODYTEXT; 310 | let stateid = 0; 311 | // > DrawText is somewhat faster when DT_NOCLIP is used. 312 | // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-drawtext 313 | // (And we don't need clipping since we generate a rect that's the right size.) 314 | let textflags = DT_NOCLIP | DT_NOPREFIX | DT_SINGLELINE; 315 | 316 | // Get size of text 317 | let text_size = 318 | unsafe { GetThemeTextExtent(text_style, hdc, partid.0, stateid, text, textflags, None)? }; 319 | 320 | // Move text into desired position 321 | let mut output_rect = position(text_size); 322 | 323 | unsafe { 324 | DrawThemeTextEx( 325 | text_style, 326 | hdc, 327 | partid.0, 328 | stateid, 329 | text, 330 | textflags, 331 | &mut output_rect, 332 | Some(&DTTOPTS { 333 | dwSize: mem::size_of::() as u32, 334 | dwFlags: DTT_COMPOSITED | DTT_TEXTCOLOR, 335 | crText: COLORREF(0xffffff), 336 | ..Default::default() 337 | }), 338 | )? 339 | }; 340 | 341 | Ok(()) 342 | } 343 | -------------------------------------------------------------------------------- /src/window/state.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ 2 | HOTKEY_MIC_MUTE, HSHELL_RUDEAPPACTIVATED, HSHELL_WINDOWACTIVATED, IDT_FETCH_AND_REDRAW_TIMER, 3 | IDT_MIC_STATE_TIMER, IDT_TRAY_POSITION_TIMER, IDT_Z_ORDER_TIMER, REDRAW_EVERY_N_FETCHES, 4 | UM_ENABLE_DEBUG_PAINT, UM_ENABLE_KEEP_AWAKE, UM_INITIAL_METRICS, UM_INITIAL_MIC_STATE, 5 | UM_INITIAL_RENDER, UM_QUEUE_MIC_STATE_CHECK, UM_QUEUE_TRAY_POSITION_CHECK, WTS_SESSION_LOCK, 6 | WTS_SESSION_LOGOFF, WTS_SESSION_LOGON, WTS_SESSION_UNLOCK, 7 | }; 8 | use crate::metrics::Metrics; 9 | use crate::utils::ScaleBy; 10 | use crate::window::awake::Awake; 11 | use crate::window::messages; 12 | use crate::window::microphone::Microphone; 13 | use crate::window::paint::Paint; 14 | use crate::window::position::Position; 15 | use crate::window::proc::ProcHandler; 16 | use crate::window::timers::Timers; 17 | use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM}; 18 | use windows::Win32::UI::WindowsAndMessaging::{ 19 | PostQuitMessage, RegisterWindowMessageW, WM_DESTROY, WM_DISPLAYCHANGE, WM_DPICHANGED, 20 | WM_ERASEBKGND, WM_HOTKEY, WM_NCCALCSIZE, WM_NCPAINT, WM_PAINT, WM_TIMER, WM_USER, 21 | WM_WTSSESSION_CHANGE, 22 | }; 23 | use windows::core::{Error, Result, w}; 24 | 25 | pub struct InfoBand { 26 | /// The message ID of the SHELLHOOK message. 27 | shellhook_message: u32, 28 | /// Timer state. 29 | timers: Timers, 30 | /// Awake state. 31 | awake: Awake, 32 | /// Paint state. 33 | paint: Paint, 34 | /// Position and z-order state. 35 | position: Position, 36 | /// Microphone state. 37 | mic: Microphone, 38 | /// Performance metrics. 39 | metrics: Metrics, 40 | } 41 | 42 | impl ProcHandler for InfoBand { 43 | fn new(window: HWND) -> Result { 44 | let shellhook_message = { 45 | let res = unsafe { RegisterWindowMessageW(w!("SHELLHOOK")) }; 46 | if res == 0 { 47 | return Err(Error::from_thread()); 48 | } 49 | res 50 | }; 51 | 52 | Ok(Self { 53 | shellhook_message, 54 | timers: Timers::new(), 55 | awake: Awake::new(), 56 | paint: Paint::new()?, 57 | position: Position::new(window)?, 58 | mic: Microphone::new(window)?, 59 | metrics: Metrics::new()?, 60 | }) 61 | } 62 | 63 | fn handle( 64 | &self, 65 | window: HWND, 66 | message: u32, 67 | wparam: WPARAM, 68 | lparam: LPARAM, 69 | ) -> Option { 70 | Some(match message { 71 | WM_NCCALCSIZE => { 72 | log::debug!("Computing size of client area (WM_NCCALCSIZE)"); 73 | // Handling this is required to ensure our window has no frame (even though it wouldn't be visible). 74 | // https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize 75 | if wparam.0 != 0 { 76 | // > When wParam is TRUE, simply returning 0 without processing the NCCALCSIZE_PARAMS rectangles 77 | // > will cause the client area to resize to the size of the window, including the window frame. 78 | // > This will remove the window frame and caption items from your window, leaving only the client area displayed. 79 | // 80 | // This is exactly what we want. 81 | LRESULT(0) 82 | } else { 83 | // > If wParam is FALSE, lParam points to a RECT structure. 84 | // > On entry, the structure contains the proposed window rectangle for the window. 85 | // > On exit, the structure should contain the screen coordinates of the corresponding window client area. 86 | // 87 | // Similarly, we want the client area to take up the entire window size, so do nothing here as well. 88 | // 89 | // > If the wParam parameter is FALSE, the application should return zero. 90 | LRESULT(0) 91 | } 92 | } 93 | WM_NCPAINT => { 94 | log::debug!("Ignoring frame repaint (WM_NCPAINT)"); 95 | // We don't have a frame, so don't paint it. 96 | LRESULT(0) 97 | } 98 | WM_PAINT => { 99 | log::debug!("Ignoring client repaint (WM_PAINT)"); 100 | // Layered windows don't have to handle WM_PAINT. 101 | // We do need to revalidate the window (here: let DefWindowProc do so), 102 | // or Windows will send us an endless stream of paint requests. 103 | return None; 104 | } 105 | WM_ERASEBKGND => { 106 | log::debug!("Ignoring background erase (WM_ERASEBKGND)"); 107 | // Since we use compositing, we don't need to erase the background. 108 | LRESULT(1) 109 | } 110 | WM_DPICHANGED => { 111 | // Low 16 bits contains DPI 112 | let dpi_raw = u32::from(wparam.0 as u16); 113 | self.position.set_dpi(dpi_raw); 114 | self.position.update_tray_position(); 115 | let (dpi, rect) = self.position.recompute(); 116 | log::info!( 117 | "DPI changed to {dpi_raw} or {}% (WM_DPICHANGED)", 118 | 100.scale_by(dpi) 119 | ); 120 | self.paint 121 | .render(window, dpi, rect, &self.metrics, self.mic.is_muted()); 122 | LRESULT(0) 123 | } 124 | WM_DISPLAYCHANGE => { 125 | log::debug!("Display resolution changed (WM_DISPLAYCHANGE)"); 126 | self.position.update_taskbar_position(); 127 | let (dpi, rect) = self.position.recompute(); 128 | self.paint 129 | .render(window, dpi, rect, &self.metrics, self.mic.is_muted()); 130 | LRESULT(0) 131 | } 132 | WM_DESTROY => { 133 | log::info!("Shutting down (WM_DESTROY)"); 134 | // SAFETY: no preconditions 135 | unsafe { PostQuitMessage(0) }; 136 | LRESULT(0) 137 | } 138 | WM_USER => match wparam { 139 | UM_ENABLE_KEEP_AWAKE => { 140 | log::info!("Enabling keep awake (UM_ENABLE_KEEP_AWAKE)"); 141 | self.awake.enable(); 142 | self.awake.keep_awake(true); 143 | LRESULT(0) 144 | } 145 | UM_ENABLE_DEBUG_PAINT => { 146 | log::info!("Enabling debug paint (UM_ENABLE_DEBUG_PAINT)"); 147 | self.paint.set_debug(true); 148 | LRESULT(0) 149 | } 150 | UM_INITIAL_METRICS => { 151 | log::info!("Initial metrics fetch (UM_INITIAL_METRICS)"); 152 | self.metrics.fetch(); 153 | // Start timer for fetching metrics and redrawing. 154 | self.timers.fetch_and_redraw.reschedule(window); 155 | LRESULT(0) 156 | } 157 | UM_INITIAL_MIC_STATE => { 158 | log::info!("Initial mic state update (UM_INITIAL_MIC_STATE)"); 159 | self.mic.refresh_devices(); 160 | self.mic.update_muted_state(); 161 | LRESULT(0) 162 | } 163 | UM_INITIAL_RENDER => { 164 | log::info!("Initial position & paint (UM_INITIAL_RENDER)"); 165 | self.position.update_taskbar_position(); 166 | self.position.update_tray_position(); 167 | self.position.update_z_order(window); 168 | let (dpi, rect) = self.position.recompute(); 169 | self.paint 170 | .render(window, dpi, rect, &self.metrics, self.mic.is_muted()); 171 | LRESULT(0) 172 | } 173 | UM_QUEUE_TRAY_POSITION_CHECK => { 174 | log::debug!("Queuing tray position check (UM_QUEUE_TRAY_POSITION_CHECK)"); 175 | // If multiple notifications are received in quick succession, rescheduling the timer effectively debounces them. 176 | self.timers.tray_position.reschedule(window); 177 | LRESULT(0) 178 | } 179 | UM_QUEUE_MIC_STATE_CHECK => { 180 | log::debug!("Queuing mic state check (UM_QUEUE_MIC_STATE_CHECK)"); 181 | // If multiple notifications are received in quick succession, rescheduling the timer effectively debounces them. 182 | self.timers.mic_state.reschedule(window); 183 | LRESULT(0) 184 | } 185 | _ => { 186 | log::warn!( 187 | "Unhandled user message (WM_USER id=0x{:08x} lparam=0x{:012x})", 188 | wparam.0, 189 | lparam.0 190 | ); 191 | return None; 192 | } 193 | }, 194 | _ if message == self.shellhook_message => match (wparam, lparam) { 195 | (HSHELL_RUDEAPPACTIVATED, LPARAM(0)) => { 196 | // This seems to indicate that the taskbar itself was focused, 197 | // so we need to re-set ourselves to TOPMOST to stay on top. 198 | log::debug!( 199 | "Reapplying z-order due to shell focus (SHELLHOOK id=0x{:08x})", 200 | wparam.0 201 | ); 202 | self.position.update_z_order(window); 203 | LRESULT(0) 204 | } 205 | ( 206 | HSHELL_WINDOWACTIVATED | HSHELL_RUDEAPPACTIVATED | WPARAM(0x35) | WPARAM(0x36), 207 | _, 208 | ) => { 209 | // Per https://github.com/dechamps/RudeWindowFixer#the-rude-window-manager, 210 | // these are the messages that the shell uses to update its z-order. 211 | log::debug!( 212 | "Queuing z-order check (SHELLHOOK id=0x{:08x} lparam=0x{:012x})", 213 | wparam.0, 214 | lparam.0 215 | ); 216 | // This timer is necessary because we receive shell hook events concurrently with the taskbar process, 217 | // and our logic is much simpler, so we always end up winning the race and using its old z-order. 218 | self.timers.z_order.reschedule(window); 219 | LRESULT(0) 220 | } 221 | _ => { 222 | log::debug!( 223 | "Ignoring shellhook message (SHELLHOOK id=0x{:08x} lparam=0x{:012x})", 224 | wparam.0, 225 | lparam.0 226 | ); 227 | LRESULT(0) 228 | } 229 | }, 230 | WM_WTSSESSION_CHANGE => match wparam { 231 | WTS_SESSION_LOGON => { 232 | log::info!("Resuming updates & keep-awake due to logon (WTS_SESSION_LOGON)"); 233 | self.timers.fetch_and_redraw.reschedule(window); 234 | self.awake.keep_awake(true); 235 | LRESULT(0) 236 | } 237 | WTS_SESSION_LOGOFF => { 238 | log::info!("Pausing updates & keep-awake due to logoff (WTS_SESSION_LOGOFF)"); 239 | self.timers.fetch_and_redraw.kill(window); 240 | self.awake.keep_awake(false); 241 | LRESULT(0) 242 | } 243 | WTS_SESSION_LOCK => { 244 | log::info!("Pausing updates & keep-awake due to lock (WTS_SESSION_LOCK)"); 245 | self.timers.fetch_and_redraw.kill(window); 246 | self.awake.keep_awake(false); 247 | LRESULT(0) 248 | } 249 | WTS_SESSION_UNLOCK => { 250 | log::info!("Resuming updates & keep-awake due to unlock (WTS_SESSION_UNLOCK)"); 251 | self.timers.fetch_and_redraw.reschedule(window); 252 | self.awake.keep_awake(true); 253 | LRESULT(0) 254 | } 255 | _ => { 256 | log::debug!( 257 | "Ignoring session change message (WM_WTSSESSION_CHANGE id=0x{:08x} lparam=0x{:012x})", 258 | wparam.0, 259 | lparam.0 260 | ); 261 | LRESULT(0) 262 | } 263 | }, 264 | WM_HOTKEY => match wparam { 265 | HOTKEY_MIC_MUTE => { 266 | // Refresh to pick up any new devices here. 267 | // We only do this on hotkey press to avoid unnecessary work. 268 | self.mic.refresh_devices(); 269 | 270 | let was_muted = self.mic.is_muted(); 271 | self.mic.set_mute(!was_muted); 272 | self.mic.update_muted_state(); 273 | let now_muted = self.mic.is_muted(); 274 | log::debug!( 275 | "Toggled mic mute (WM_HOTKEY was_muted={was_muted} now_muted={now_muted})" 276 | ); 277 | if was_muted != now_muted { 278 | let (dpi, rect) = self.position.get(); 279 | self.paint 280 | .render(window, dpi, rect, &self.metrics, now_muted); 281 | } 282 | LRESULT(0) 283 | } 284 | _ => { 285 | log::debug!( 286 | "Ignoring hotkey message (WM_HOTKEY id=0x{:08x} lparam=0x{:012x})", 287 | wparam.0, 288 | lparam.0 289 | ); 290 | LRESULT(0) 291 | } 292 | }, 293 | WM_TIMER => match wparam { 294 | IDT_FETCH_AND_REDRAW_TIMER => { 295 | log::trace!("Fetching metrics (IDT_FETCH_AND_REDRAW_TIMER)"); 296 | let fetch_count = self.metrics.fetch(); 297 | 298 | if fetch_count.is_multiple_of(REDRAW_EVERY_N_FETCHES) { 299 | log::trace!("Starting repaint (IDT_FETCH_AND_REDRAW_TIMER)"); 300 | let (dpi, rect) = self.position.get(); 301 | self.paint 302 | .render(window, dpi, rect, &self.metrics, self.mic.is_muted()); 303 | } 304 | LRESULT(0) 305 | } 306 | IDT_TRAY_POSITION_TIMER => { 307 | self.timers.tray_position.kill(window); 308 | 309 | log::debug!("Rechecking tray position (IDT_TRAY_POSITION_TIMER)",); 310 | self.position.update_tray_position(); 311 | let (dpi, rect) = self.position.recompute(); 312 | self.paint 313 | .render(window, dpi, rect, &self.metrics, self.mic.is_muted()); 314 | LRESULT(0) 315 | } 316 | IDT_Z_ORDER_TIMER => { 317 | self.timers.z_order.kill(window); 318 | 319 | log::debug!("Rechecking z-order (IDT_Z_ORDER_TIMER)",); 320 | self.position.update_z_order(window); 321 | LRESULT(0) 322 | } 323 | IDT_MIC_STATE_TIMER => { 324 | self.timers.mic_state.kill(window); 325 | 326 | let was_muted = self.mic.is_muted(); 327 | self.mic.update_muted_state(); 328 | let now_muted = self.mic.is_muted(); 329 | log::debug!( 330 | "Checked mic state (IDT_MIC_STATE_TIMER was_muted={was_muted} now_muted={now_muted})" 331 | ); 332 | if was_muted != now_muted { 333 | let (dpi, rect) = self.position.get(); 334 | self.paint 335 | .render(window, dpi, rect, &self.metrics, now_muted); 336 | } 337 | LRESULT(0) 338 | } 339 | _ => { 340 | log::warn!( 341 | "Unhandled timer message (WM_TIMER id=0x{:08x} lparam=0x{:012x})", 342 | wparam.0, 343 | lparam.0 344 | ); 345 | return None; 346 | } 347 | }, 348 | _ => { 349 | log::debug!( 350 | "Default window proc ({} wparam=0x{:08x} lparam=0x{:012x})", 351 | messages::Name(message), 352 | wparam.0, 353 | lparam.0 354 | ); 355 | return None; 356 | } 357 | }) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "android_system_properties" 7 | version = "0.1.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.100" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 19 | 20 | [[package]] 21 | name = "arc-swap" 22 | version = "1.7.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 25 | 26 | [[package]] 27 | name = "argh" 28 | version = "0.1.13" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" 31 | dependencies = [ 32 | "argh_derive", 33 | "argh_shared", 34 | "rust-fuzzy-search", 35 | ] 36 | 37 | [[package]] 38 | name = "argh_derive" 39 | version = "0.1.13" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" 42 | dependencies = [ 43 | "argh_shared", 44 | "proc-macro2", 45 | "quote", 46 | "syn", 47 | ] 48 | 49 | [[package]] 50 | name = "argh_shared" 51 | version = "0.1.13" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" 54 | dependencies = [ 55 | "serde", 56 | ] 57 | 58 | [[package]] 59 | name = "autocfg" 60 | version = "1.5.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.10.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 69 | 70 | [[package]] 71 | name = "bumpalo" 72 | version = "3.19.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 75 | 76 | [[package]] 77 | name = "cc" 78 | version = "1.2.47" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" 81 | dependencies = [ 82 | "find-msvc-tools", 83 | "shlex", 84 | ] 85 | 86 | [[package]] 87 | name = "cfg-if" 88 | version = "1.0.4" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 91 | 92 | [[package]] 93 | name = "chrono" 94 | version = "0.4.42" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" 97 | dependencies = [ 98 | "iana-time-zone", 99 | "num-traits", 100 | "windows-link", 101 | ] 102 | 103 | [[package]] 104 | name = "core-foundation-sys" 105 | version = "0.8.7" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 108 | 109 | [[package]] 110 | name = "derive_more" 111 | version = "2.0.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 114 | dependencies = [ 115 | "derive_more-impl", 116 | ] 117 | 118 | [[package]] 119 | name = "derive_more-impl" 120 | version = "2.0.1" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 123 | dependencies = [ 124 | "proc-macro2", 125 | "quote", 126 | "syn", 127 | "unicode-xid", 128 | ] 129 | 130 | [[package]] 131 | name = "embed-manifest" 132 | version = "1.5.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "94cdc65b1cf9e871453ce2f86f5aaec24ff2eaa36a1fa3e02e441dddc3613b99" 135 | 136 | [[package]] 137 | name = "find-msvc-tools" 138 | version = "0.1.5" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 141 | 142 | [[package]] 143 | name = "fnv" 144 | version = "1.0.7" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 147 | 148 | [[package]] 149 | name = "iana-time-zone" 150 | version = "0.1.64" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" 153 | dependencies = [ 154 | "android_system_properties", 155 | "core-foundation-sys", 156 | "iana-time-zone-haiku", 157 | "js-sys", 158 | "log", 159 | "wasm-bindgen", 160 | "windows-core", 161 | ] 162 | 163 | [[package]] 164 | name = "iana-time-zone-haiku" 165 | version = "0.1.2" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 168 | dependencies = [ 169 | "cc", 170 | ] 171 | 172 | [[package]] 173 | name = "infoband" 174 | version = "1.7.0" 175 | dependencies = [ 176 | "argh", 177 | "embed-manifest", 178 | "log", 179 | "log4rs", 180 | "serde", 181 | "serde_json", 182 | "windows", 183 | "windows-core", 184 | ] 185 | 186 | [[package]] 187 | name = "itoa" 188 | version = "1.0.15" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 191 | 192 | [[package]] 193 | name = "js-sys" 194 | version = "0.3.82" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" 197 | dependencies = [ 198 | "once_cell", 199 | "wasm-bindgen", 200 | ] 201 | 202 | [[package]] 203 | name = "libc" 204 | version = "0.2.177" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 207 | 208 | [[package]] 209 | name = "lock_api" 210 | version = "0.4.14" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" 213 | dependencies = [ 214 | "scopeguard", 215 | ] 216 | 217 | [[package]] 218 | name = "log" 219 | version = "0.4.28" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 222 | 223 | [[package]] 224 | name = "log-mdc" 225 | version = "0.1.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" 228 | 229 | [[package]] 230 | name = "log4rs" 231 | version = "1.4.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "3e947bb896e702c711fccc2bf02ab2abb6072910693818d1d6b07ee2b9dfd86c" 234 | dependencies = [ 235 | "anyhow", 236 | "arc-swap", 237 | "chrono", 238 | "derive_more", 239 | "fnv", 240 | "libc", 241 | "log", 242 | "log-mdc", 243 | "mock_instant", 244 | "parking_lot", 245 | "thiserror", 246 | "thread-id", 247 | "unicode-segmentation", 248 | "winapi", 249 | ] 250 | 251 | [[package]] 252 | name = "memchr" 253 | version = "2.7.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 256 | 257 | [[package]] 258 | name = "mock_instant" 259 | version = "0.6.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" 262 | 263 | [[package]] 264 | name = "num-traits" 265 | version = "0.2.19" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 268 | dependencies = [ 269 | "autocfg", 270 | ] 271 | 272 | [[package]] 273 | name = "once_cell" 274 | version = "1.21.3" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 277 | 278 | [[package]] 279 | name = "parking_lot" 280 | version = "0.12.5" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" 283 | dependencies = [ 284 | "lock_api", 285 | "parking_lot_core", 286 | ] 287 | 288 | [[package]] 289 | name = "parking_lot_core" 290 | version = "0.9.12" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" 293 | dependencies = [ 294 | "cfg-if", 295 | "libc", 296 | "redox_syscall", 297 | "smallvec", 298 | "windows-link", 299 | ] 300 | 301 | [[package]] 302 | name = "proc-macro2" 303 | version = "1.0.103" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 306 | dependencies = [ 307 | "unicode-ident", 308 | ] 309 | 310 | [[package]] 311 | name = "quote" 312 | version = "1.0.42" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 315 | dependencies = [ 316 | "proc-macro2", 317 | ] 318 | 319 | [[package]] 320 | name = "redox_syscall" 321 | version = "0.5.18" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 324 | dependencies = [ 325 | "bitflags", 326 | ] 327 | 328 | [[package]] 329 | name = "rust-fuzzy-search" 330 | version = "0.1.1" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" 333 | 334 | [[package]] 335 | name = "rustversion" 336 | version = "1.0.22" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 339 | 340 | [[package]] 341 | name = "ryu" 342 | version = "1.0.20" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 345 | 346 | [[package]] 347 | name = "scopeguard" 348 | version = "1.2.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 351 | 352 | [[package]] 353 | name = "serde" 354 | version = "1.0.228" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 357 | dependencies = [ 358 | "serde_core", 359 | "serde_derive", 360 | ] 361 | 362 | [[package]] 363 | name = "serde_core" 364 | version = "1.0.228" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 367 | dependencies = [ 368 | "serde_derive", 369 | ] 370 | 371 | [[package]] 372 | name = "serde_derive" 373 | version = "1.0.228" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 376 | dependencies = [ 377 | "proc-macro2", 378 | "quote", 379 | "syn", 380 | ] 381 | 382 | [[package]] 383 | name = "serde_json" 384 | version = "1.0.145" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 387 | dependencies = [ 388 | "itoa", 389 | "memchr", 390 | "ryu", 391 | "serde", 392 | "serde_core", 393 | ] 394 | 395 | [[package]] 396 | name = "shlex" 397 | version = "1.3.0" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 400 | 401 | [[package]] 402 | name = "smallvec" 403 | version = "1.15.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 406 | 407 | [[package]] 408 | name = "syn" 409 | version = "2.0.111" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" 412 | dependencies = [ 413 | "proc-macro2", 414 | "quote", 415 | "unicode-ident", 416 | ] 417 | 418 | [[package]] 419 | name = "thiserror" 420 | version = "2.0.17" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 423 | dependencies = [ 424 | "thiserror-impl", 425 | ] 426 | 427 | [[package]] 428 | name = "thiserror-impl" 429 | version = "2.0.17" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 432 | dependencies = [ 433 | "proc-macro2", 434 | "quote", 435 | "syn", 436 | ] 437 | 438 | [[package]] 439 | name = "thread-id" 440 | version = "5.0.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "99043e46c5a15af379c06add30d9c93a6c0e8849de00d244c4a2c417da128d80" 443 | dependencies = [ 444 | "libc", 445 | "windows-sys", 446 | ] 447 | 448 | [[package]] 449 | name = "unicode-ident" 450 | version = "1.0.22" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 453 | 454 | [[package]] 455 | name = "unicode-segmentation" 456 | version = "1.12.0" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 459 | 460 | [[package]] 461 | name = "unicode-xid" 462 | version = "0.2.6" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 465 | 466 | [[package]] 467 | name = "wasm-bindgen" 468 | version = "0.2.105" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" 471 | dependencies = [ 472 | "cfg-if", 473 | "once_cell", 474 | "rustversion", 475 | "wasm-bindgen-macro", 476 | "wasm-bindgen-shared", 477 | ] 478 | 479 | [[package]] 480 | name = "wasm-bindgen-macro" 481 | version = "0.2.105" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" 484 | dependencies = [ 485 | "quote", 486 | "wasm-bindgen-macro-support", 487 | ] 488 | 489 | [[package]] 490 | name = "wasm-bindgen-macro-support" 491 | version = "0.2.105" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" 494 | dependencies = [ 495 | "bumpalo", 496 | "proc-macro2", 497 | "quote", 498 | "syn", 499 | "wasm-bindgen-shared", 500 | ] 501 | 502 | [[package]] 503 | name = "wasm-bindgen-shared" 504 | version = "0.2.105" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" 507 | dependencies = [ 508 | "unicode-ident", 509 | ] 510 | 511 | [[package]] 512 | name = "winapi" 513 | version = "0.3.9" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 516 | dependencies = [ 517 | "winapi-i686-pc-windows-gnu", 518 | "winapi-x86_64-pc-windows-gnu", 519 | ] 520 | 521 | [[package]] 522 | name = "winapi-i686-pc-windows-gnu" 523 | version = "0.4.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 526 | 527 | [[package]] 528 | name = "winapi-x86_64-pc-windows-gnu" 529 | version = "0.4.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 532 | 533 | [[package]] 534 | name = "windows" 535 | version = "0.62.2" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" 538 | dependencies = [ 539 | "windows-collections", 540 | "windows-core", 541 | "windows-future", 542 | "windows-numerics", 543 | ] 544 | 545 | [[package]] 546 | name = "windows-collections" 547 | version = "0.3.2" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" 550 | dependencies = [ 551 | "windows-core", 552 | ] 553 | 554 | [[package]] 555 | name = "windows-core" 556 | version = "0.62.2" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" 559 | dependencies = [ 560 | "windows-implement", 561 | "windows-interface", 562 | "windows-link", 563 | "windows-result", 564 | "windows-strings", 565 | ] 566 | 567 | [[package]] 568 | name = "windows-future" 569 | version = "0.3.2" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" 572 | dependencies = [ 573 | "windows-core", 574 | "windows-link", 575 | "windows-threading", 576 | ] 577 | 578 | [[package]] 579 | name = "windows-implement" 580 | version = "0.60.2" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" 583 | dependencies = [ 584 | "proc-macro2", 585 | "quote", 586 | "syn", 587 | ] 588 | 589 | [[package]] 590 | name = "windows-interface" 591 | version = "0.59.3" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" 594 | dependencies = [ 595 | "proc-macro2", 596 | "quote", 597 | "syn", 598 | ] 599 | 600 | [[package]] 601 | name = "windows-link" 602 | version = "0.2.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 605 | 606 | [[package]] 607 | name = "windows-numerics" 608 | version = "0.3.1" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" 611 | dependencies = [ 612 | "windows-core", 613 | "windows-link", 614 | ] 615 | 616 | [[package]] 617 | name = "windows-result" 618 | version = "0.4.1" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" 621 | dependencies = [ 622 | "windows-link", 623 | ] 624 | 625 | [[package]] 626 | name = "windows-strings" 627 | version = "0.5.1" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" 630 | dependencies = [ 631 | "windows-link", 632 | ] 633 | 634 | [[package]] 635 | name = "windows-sys" 636 | version = "0.59.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 639 | dependencies = [ 640 | "windows-targets", 641 | ] 642 | 643 | [[package]] 644 | name = "windows-targets" 645 | version = "0.52.6" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 648 | dependencies = [ 649 | "windows_aarch64_gnullvm", 650 | "windows_aarch64_msvc", 651 | "windows_i686_gnu", 652 | "windows_i686_gnullvm", 653 | "windows_i686_msvc", 654 | "windows_x86_64_gnu", 655 | "windows_x86_64_gnullvm", 656 | "windows_x86_64_msvc", 657 | ] 658 | 659 | [[package]] 660 | name = "windows-threading" 661 | version = "0.2.1" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" 664 | dependencies = [ 665 | "windows-link", 666 | ] 667 | 668 | [[package]] 669 | name = "windows_aarch64_gnullvm" 670 | version = "0.52.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 673 | 674 | [[package]] 675 | name = "windows_aarch64_msvc" 676 | version = "0.52.6" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 679 | 680 | [[package]] 681 | name = "windows_i686_gnu" 682 | version = "0.52.6" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 685 | 686 | [[package]] 687 | name = "windows_i686_gnullvm" 688 | version = "0.52.6" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 691 | 692 | [[package]] 693 | name = "windows_i686_msvc" 694 | version = "0.52.6" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 697 | 698 | [[package]] 699 | name = "windows_x86_64_gnu" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 703 | 704 | [[package]] 705 | name = "windows_x86_64_gnullvm" 706 | version = "0.52.6" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 709 | 710 | [[package]] 711 | name = "windows_x86_64_msvc" 712 | version = "0.52.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 715 | --------------------------------------------------------------------------------