use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use tracing::{debug, error, info};
use windows::core::PCWSTR;
use windows::Win32::Foundation::*;
use windows::Win32::Graphics::Gdi::*;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::*;
const OVERLAY_CLASS_NAME: PCWSTR = windows::core::w!("ComputerUseActionOverlay");
const OVERLAY_CHANGE_COOLDOWN_MS: u64 = 100;
const MINIMUM_DISPLAY_MS: u64 = 300;
const WINDOW_CREATION_DELAY_MS: u64 = 50;
static ACTION_OVERLAY_ENABLED: AtomicBool = AtomicBool::new(true);
static OVERLAY_ENV_CHECKED: once_cell::sync::Lazy<()> = once_cell::sync::Lazy::new(|| {
if let Ok(val) = std::env::var("COMPUTERUSE_ACTION_OVERLAY") {
if val == "0" || val.eq_ignore_ascii_case("false") || val.eq_ignore_ascii_case("off") {
ACTION_OVERLAY_ENABLED.store(false, Ordering::SeqCst);
tracing::info!("Action overlay disabled via COMPUTERUSE_ACTION_OVERLAY env var");
}
}
});
static OVERLAY_SHOWN_AT: once_cell::sync::Lazy<Mutex<Option<Instant>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
static OVERLAY_STATE: once_cell::sync::Lazy<RwLock<OverlayState>> =
once_cell::sync::Lazy::new(|| RwLock::new(OverlayState::default()));
static LAST_OVERLAY_CHANGE: once_cell::sync::Lazy<Mutex<Option<Instant>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
static SHOULD_CLOSE: once_cell::sync::Lazy<Mutex<Option<Arc<AtomicBool>>>> =
once_cell::sync::Lazy::new(|| Mutex::new(None));
#[derive(Default, Clone)]
struct OverlayState {
is_visible: bool,
message: String,
sub_message: Option<String>,
window_handle: Option<isize>,
}
pub fn set_action_overlay_enabled(enabled: bool) {
let was_enabled = ACTION_OVERLAY_ENABLED.swap(enabled, Ordering::SeqCst);
if was_enabled != enabled {
info!(
"Action overlay {} (was {})",
if enabled { "enabled" } else { "disabled" },
if was_enabled { "enabled" } else { "disabled" }
);
if !enabled {
hide_action_overlay();
}
}
}
pub fn is_action_overlay_enabled() -> bool {
ACTION_OVERLAY_ENABLED.load(Ordering::SeqCst)
}
pub fn show_action_overlay(message: impl Into<String>, sub_message: Option<String>) {
#[allow(clippy::let_unit_value)]
let _check = &*OVERLAY_ENV_CHECKED;
if !ACTION_OVERLAY_ENABLED.load(Ordering::SeqCst) {
debug!("Action overlay disabled, skipping show");
return;
}
let message = message.into();
info!("action_overlay show: {}", message);
{
if let Ok(last_change) = LAST_OVERLAY_CHANGE.lock() {
if let Some(last) = *last_change {
if last.elapsed() < Duration::from_millis(OVERLAY_CHANGE_COOLDOWN_MS) {
debug!(
"Skipping overlay change - cooldown active ({:.1}ms remaining)",
(Duration::from_millis(OVERLAY_CHANGE_COOLDOWN_MS) - last.elapsed())
.as_millis()
);
return;
}
}
}
}
if let Ok(mut last_change) = LAST_OVERLAY_CHANGE.lock() {
*last_change = Some(Instant::now());
}
if let Ok(mut shown_at) = OVERLAY_SHOWN_AT.lock() {
*shown_at = Some(Instant::now());
}
if let Ok(mut state) = OVERLAY_STATE.write() {
state.message = message.clone();
state.sub_message = sub_message.clone();
state.is_visible = true;
}
thread::spawn(move || {
if let Err(e) = create_overlay_window() {
error!("Failed to create action overlay: {}", e);
}
});
thread::sleep(Duration::from_millis(WINDOW_CREATION_DELAY_MS));
debug!(
"action_overlay show complete (waited {}ms for window)",
WINDOW_CREATION_DELAY_MS
);
}
pub fn hide_action_overlay() {
if let Ok(mut shown_at) = OVERLAY_SHOWN_AT.lock() {
if let Some(shown) = shown_at.take() {
let elapsed = shown.elapsed();
let min_display = Duration::from_millis(MINIMUM_DISPLAY_MS);
if elapsed < min_display {
let wait_time = min_display - elapsed;
debug!(
"action_overlay waiting {}ms to meet minimum display time",
wait_time.as_millis()
);
thread::sleep(wait_time);
}
}
}
info!("action_overlay hide");
let handle = {
if let Ok(mut state) = OVERLAY_STATE.write() {
let was_visible = state.is_visible;
state.is_visible = false;
let h = state.window_handle.take();
debug!(
"hide(): was_visible={}, took_handle={}",
was_visible,
h.is_some()
);
h
} else {
None
}
};
if let Ok(mut global_close) = SHOULD_CLOSE.lock() {
if let Some(should_close) = global_close.take() {
should_close.store(true, Ordering::SeqCst);
debug!("Signaled overlay thread to close");
}
}
if let Some(h) = handle {
destroy_overlay_window(h);
}
}
pub fn update_action_overlay_message(message: impl Into<String>, sub_message: Option<String>) {
let message = message.into();
let is_visible = OVERLAY_STATE.read().map(|s| s.is_visible).unwrap_or(false);
if !is_visible {
show_action_overlay(message, sub_message);
return;
}
let hwnd = if let Ok(mut state) = OVERLAY_STATE.write() {
state.message = message.clone();
state.sub_message = sub_message.clone();
state.window_handle
} else {
None
};
info!("Updated overlay message: {}", message);
if let Some(h) = hwnd {
unsafe {
let hwnd = HWND(h as *mut _);
if IsWindow(Some(hwnd)).as_bool() {
let _ = InvalidateRect(Some(hwnd), None, true);
}
}
}
}
pub struct ActionOverlayGuard {
_private: (),
}
impl ActionOverlayGuard {
pub fn new(action: &str, element_info: Option<&str>) -> Self {
let sub_message = element_info.map(|s| s.to_string());
show_action_overlay(action, sub_message);
Self { _private: () }
}
}
impl Drop for ActionOverlayGuard {
fn drop(&mut self) {
hide_action_overlay();
}
}
fn to_wide_string(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn create_overlay_window() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
unsafe {
let h_instance = GetModuleHandleW(None)?;
let wc = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(overlay_window_proc),
hInstance: HINSTANCE(h_instance.0),
hCursor: LoadCursorW(None, IDC_ARROW)?,
hbrBackground: HBRUSH(std::ptr::null_mut()),
lpszClassName: OVERLAY_CLASS_NAME,
..Default::default()
};
let atom = RegisterClassExW(&wc);
if atom == 0 {
debug!("Window class already registered or registration failed");
}
let screen_x = GetSystemMetrics(SM_XVIRTUALSCREEN);
let screen_y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
let window_name = to_wide_string("ComputerUse Action Status");
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE(
WS_EX_TOPMOST.0
| WS_EX_LAYERED.0
| WS_EX_TRANSPARENT.0
| WS_EX_TOOLWINDOW.0
| WS_EX_NOACTIVATE.0,
),
OVERLAY_CLASS_NAME,
PCWSTR::from_raw(window_name.as_ptr()),
WINDOW_STYLE(WS_POPUP.0),
screen_x,
screen_y,
screen_width,
screen_height,
None,
None,
Some(HINSTANCE(h_instance.0)),
None,
)?;
let new_handle = hwnd.0 as isize;
let should_destroy = {
if let Ok(mut state) = OVERLAY_STATE.write() {
if !state.is_visible {
info!(
"Window created but is_visible=false - destroying immediately (handle={})",
new_handle
);
true
} else {
if let Some(old) = state.window_handle.take() {
destroy_overlay_window(old);
}
state.window_handle = Some(new_handle);
false
}
} else {
true
}
};
if should_destroy {
let _ = DestroyWindow(hwnd);
return Ok(());
}
SetLayeredWindowAttributes(hwnd, COLORREF(0), 77, LWA_ALPHA)?;
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
info!("Action overlay window created successfully");
let should_close = Arc::new(AtomicBool::new(false));
if let Ok(mut global_close) = SHOULD_CLOSE.lock() {
*global_close = Some(should_close.clone());
}
let mut msg = MSG::default();
loop {
if should_close.load(Ordering::SeqCst) {
debug!("Overlay thread received close signal");
let _ = DestroyWindow(hwnd);
break;
}
if PeekMessageW(&mut msg, Some(hwnd), 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
break;
}
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
} else {
thread::sleep(Duration::from_millis(10));
}
}
Ok(())
}
}
fn destroy_overlay_window(handle: isize) {
unsafe {
let hwnd = HWND(handle as *mut _);
if IsWindow(Some(hwnd)).as_bool() {
let _ = PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0));
debug!("Posted WM_CLOSE to action overlay window");
} else {
debug!(
"Window handle {} is no longer valid, skipping WM_CLOSE",
handle
);
}
}
}
unsafe extern "system" fn overlay_window_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_CREATE => LRESULT(0),
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(hwnd, &mut ps);
let mut rect = RECT::default();
let _ = GetClientRect(hwnd, &mut rect);
let white_brush = CreateSolidBrush(COLORREF(0xFFFFFF));
FillRect(hdc, &rect, white_brush);
let _ = DeleteObject(white_brush.into());
let (message, sub_message) = {
if let Ok(state) = OVERLAY_STATE.read() {
(state.message.clone(), state.sub_message.clone())
} else {
("".to_string(), None)
}
};
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, COLORREF(0x000000));
let font_height = -32; let font = CreateFontW(
font_height,
0,
0,
0,
FW_BLACK.0 as i32,
0,
0,
0,
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY,
DEFAULT_PITCH.0 as u32 | FF_DONTCARE.0 as u32,
windows::core::w!("Segoe UI"),
);
let old_font = SelectObject(hdc, HGDIOBJ::from(font));
let box_width = 800;
let box_height = 400;
let box_x = (rect.right - rect.left - box_width) / 2;
let box_y = (rect.bottom - rect.top - box_height) / 2;
let black_pen = CreatePen(PS_SOLID, 2, COLORREF(0x000000));
let old_pen = SelectObject(hdc, HGDIOBJ::from(black_pen));
let null_brush = GetStockObject(NULL_BRUSH);
let old_brush = SelectObject(hdc, null_brush);
let _ = Rectangle(
hdc,
box_x - 10,
box_y - 10,
box_x + box_width + 10,
box_y + box_height + 10,
);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_brush);
let _ = DeleteObject(black_pen.into());
let mut text_rect = RECT {
left: box_x,
top: box_y + 20,
right: box_x + box_width,
bottom: box_y + 80,
};
let mut message_wide = to_wide_string(&message);
DrawTextW(
hdc,
&mut message_wide,
&mut text_rect,
DT_CENTER | DT_SINGLELINE | DT_VCENTER,
);
if let Some(sub) = sub_message {
let small_font = CreateFontW(
-20, 0,
0,
0,
FW_NORMAL.0 as i32,
0,
0,
0,
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY,
DEFAULT_PITCH.0 as u32 | FF_DONTCARE.0 as u32,
windows::core::w!("Segoe UI"),
);
SelectObject(hdc, HGDIOBJ::from(small_font));
let mut sub_rect = RECT {
left: box_x,
top: box_y + 100,
right: box_x + box_width,
bottom: box_y + box_height - 20,
};
let mut sub_wide = to_wide_string(&sub);
DrawTextW(hdc, &mut sub_wide, &mut sub_rect, DT_CENTER | DT_WORDBREAK);
let _ = DeleteObject(small_font.into());
}
SelectObject(hdc, old_font);
let _ = DeleteObject(font.into());
let _ = EndPaint(hwnd, &ps);
LRESULT(0)
}
WM_CLOSE => {
let _ = DestroyWindow(hwnd);
LRESULT(0)
}
WM_DESTROY => {
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enable_disable() {
assert!(is_action_overlay_enabled());
set_action_overlay_enabled(false);
assert!(!is_action_overlay_enabled());
set_action_overlay_enabled(true);
assert!(is_action_overlay_enabled());
}
}