#![allow(unsafe_code)]
#![allow(unsafe_op_in_unsafe_fn)]
use actl_core::state::{Phase, SessionState, SignalPaths};
use windows::Win32::Foundation::{COLORREF, HWND, LPARAM, LRESULT, RECT, WAIT_OBJECT_0, WPARAM};
use windows::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, BeginPaint, CreateBitmap, CreateCompatibleDC,
CreateDIBSection, CreateSolidBrush, DIB_RGB_COLORS, DT_END_ELLIPSIS, DT_LEFT, DT_SINGLELINE,
DT_VCENTER, DeleteObject, DrawTextW, Ellipse, EndPaint, FillRect, GetStockObject, HGDIOBJ,
InvalidateRect, NULL_PEN,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::OpenMutexW;
use windows::Win32::UI::Input::KeyboardAndMouse::{
MOD_ALT, MOD_CONTROL, RegisterHotKey, UnregisterHotKey, VK_F12,
};
use windows::Win32::UI::Shell::{
NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NOTIFYICONDATAW, Shell_NotifyIconW,
};
use windows::Win32::UI::WindowsAndMessaging::GetClientRect;
use windows::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CS_HREDRAW, CS_VREDRAW, CreatePopupMenu, CreateWindowExW, DefWindowProcW,
DestroyMenu, DispatchMessageW, GWLP_USERDATA, GetMessageW, GetSystemMetrics, HTCAPTION,
HWND_BROADCAST, IDC_ARROW, KillTimer, LWA_ALPHA, LoadCursorW, MF_SEPARATOR, MF_STRING, MSG,
PostQuitMessage, RegisterClassW, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
SM_YVIRTUALSCREEN, SPI_GETWORKAREA, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SetForegroundWindow,
SetLayeredWindowAttributes, SetTimer, SystemParametersInfoW, TPM_RETURNCMD, TPM_RIGHTBUTTON,
TrackPopupMenu, TranslateMessage, WINDOW_EX_STYLE, WINDOW_STYLE, WM_APP, WM_DESTROY,
WM_EXITSIZEMOVE, WM_HOTKEY, WM_LBUTTONDBLCLK, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_NCHITTEST,
WM_PAINT, WM_RBUTTONUP, WM_TIMER, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
WS_EX_TOPMOST, WS_POPUP, WS_VISIBLE,
};
use windows::Win32::UI::WindowsAndMessaging::{CreateIconIndirect, ICONINFO};
use windows::core::w;
static HOTKEY_OK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
const WIDTH: i32 = 420;
const HEIGHT: i32 = 96;
fn dpi_scale() -> i32 {
static DPI: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
let dpi = *DPI.get_or_init(|| unsafe { windows::Win32::UI::HiDpi::GetDpiForSystem() });
dpi as i32 / 96
}
fn scale(v: i32) -> i32 {
v * dpi_scale()
}
const TIMER_ID: usize = 1;
const TIMER_MS: u32 = 250;
const HOTKEY_ID: i32 = 1;
const STALE_MS: u64 = 10_000;
enum Mode {
Stopped,
Active,
Busy,
Idle,
Stale,
}
struct Snapshot {
mode: Mode,
line1: String,
line2: String,
}
fn state_line(s: &SessionState) -> String {
let step = s
.step
.as_deref()
.map(|x| format!(" {x}"))
.unwrap_or_default();
let app = s
.app
.as_deref()
.map(|a| format!(" · {a}"))
.unwrap_or_default();
format!("{}{}{}", s.command, app, step)
}
fn poll() -> Snapshot {
let paths = SignalPaths::default();
if paths.stop_requested() {
return Snapshot {
mode: Mode::Stopped,
line1: "停止请求".into(),
line2: "点击解除".into(),
};
}
let lock_held = input_lock_held();
let st = paths.read_state();
let age = st
.as_ref()
.map(|s| actl_core::state::unix_ms().saturating_sub(s.ts_ms))
.unwrap_or(u64::MAX);
if lock_held {
return Snapshot {
mode: Mode::Active,
line1: st
.as_ref()
.map(state_line)
.unwrap_or_else(|| "未知命令".into()),
line2: "注入中".into(),
};
}
match st {
Some(s) if age <= STALE_MS && s.phase == Phase::Running => Snapshot {
mode: Mode::Busy,
line1: state_line(&s),
line2: "执行中".into(),
},
Some(s) if age <= STALE_MS => Snapshot {
mode: Mode::Idle,
line1: state_line(&s),
line2: match s.phase {
Phase::Done => "✓ 完成".to_string(),
Phase::Error => "✗ 失败".to_string(),
Phase::Stopped => "已停止".to_string(),
Phase::Running => unreachable!(),
},
},
Some(_) => Snapshot {
mode: Mode::Stale,
line1: "actl".into(),
line2: "失联 >10s".into(),
},
None => Snapshot {
mode: Mode::Idle,
line1: "actl".into(),
line2: "空闲".into(),
},
}
}
fn input_lock_held() -> bool {
use std::sync::atomic::{AtomicBool, Ordering};
static HELD: AtomicBool = AtomicBool::new(false);
unsafe {
match OpenMutexW(
windows::Win32::System::Threading::SYNCHRONIZATION_ACCESS_RIGHTS(0x0010_0000),
false,
w!("actl-input-lock"),
) {
Ok(h) => {
let held = mutex_is_signaled(h);
let _ = windows::Win32::Foundation::CloseHandle(h);
HELD.store(held, Ordering::Relaxed);
held
}
Err(_) => HELD.load(Ordering::Relaxed),
}
}
}
unsafe fn mutex_is_signaled(h: windows::Win32::Foundation::HANDLE) -> bool {
windows::Win32::System::Threading::WaitForSingleObject(h, 0) == WAIT_OBJECT_0
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
unsafe fn font(height: i32) -> windows::Win32::Graphics::Gdi::HFONT {
static BIG: std::sync::OnceLock<isize> = std::sync::OnceLock::new();
static MID: std::sync::OnceLock<isize> = std::sync::OnceLock::new();
static DIM: std::sync::OnceLock<isize> = std::sync::OnceLock::new();
use windows::Win32::Graphics::Gdi::{
CLEARTYPE_QUALITY, CLIP_DEFAULT_PRECIS, CreateFontW, DEFAULT_CHARSET, FIXED_PITCH,
FW_NORMAL, FW_SEMIBOLD, OUT_DEFAULT_PRECIS,
};
let (slot, weight) = match height {
17 => (&BIG, FW_SEMIBOLD.0 as i32),
13 => (&MID, FW_NORMAL.0 as i32),
_ => (&DIM, FW_NORMAL.0 as i32),
};
let h = *slot.get_or_init(|| {
CreateFontW(
-scale(height), 0,
0,
0,
weight,
0,
0,
0,
DEFAULT_CHARSET,
OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS,
CLEARTYPE_QUALITY,
FIXED_PITCH.0 as u32,
w!("Source Code Pro"),
)
.0 as isize
});
windows::Win32::Graphics::Gdi::HFONT(h as *mut _)
}
unsafe fn draw(hwnd: HWND, snap: &Snapshot) {
use windows::Win32::Graphics::Gdi::{SelectObject, SetBkColor, SetTextColor};
let mut ps = std::mem::zeroed();
let hdc = BeginPaint(hwnd, &mut ps);
let mut rc = RECT::default();
let _ = GetClientRect(hwnd, &mut rc);
const BG: u32 = 0x001E_1E1E;
let (dot, state_fg) = match snap.mode {
Mode::Stopped => (0x0049_51F8u32, 0x0049_51F8u32), Mode::Active => (0x0041_B3E3u32, 0x0041_B3E3u32), Mode::Busy => (0x0050_B93Fu32, 0x0050_B93Fu32), Mode::Idle => (0x009E_948Bu32, 0x009E_948Bu32), Mode::Stale => (0x0081_766Eu32, 0x0081_766Eu32), };
let fill = |r: &RECT, color: u32| {
let b = CreateSolidBrush(COLORREF(color));
FillRect(hdc, r, b);
let _ = DeleteObject(HGDIOBJ(b.0));
};
fill(&rc, BG);
let dt = DT_LEFT | DT_SINGLELINE | DT_END_ELLIPSIS | DT_VCENTER;
let left = scale(18);
let line = |r: &mut RECT, s: &str, h: i32, color: u32| {
let old_f = SelectObject(hdc, HGDIOBJ(font(h).0));
let old_c = SetTextColor(hdc, COLORREF(color));
SetBkColor(hdc, COLORREF(BG));
let mut buf = wide(s);
let _ = DrawTextW(hdc, &mut buf, r as *mut RECT, dt);
SetTextColor(hdc, old_c);
SelectObject(hdc, old_f);
};
let mut r1 = RECT {
left,
top: scale(12),
right: rc.right - scale(12),
bottom: scale(38),
};
let dot_d = scale(11);
let dot_y = scale(54) - dot_d / 2;
let mut r2 = RECT {
left: left + dot_d + scale(9),
top: scale(44),
right: rc.right - scale(12),
bottom: scale(64),
};
let mut r3 = RECT {
left,
top: scale(72),
right: rc.right - scale(12),
bottom: scale(88),
};
line(&mut r1, &snap.line1, 17, 0x00F3_EDE6);
{
let b = CreateSolidBrush(COLORREF(dot));
let old_br = SelectObject(hdc, HGDIOBJ(b.0));
let old_pen = SelectObject(hdc, GetStockObject(NULL_PEN));
let _ = Ellipse(hdc, left, dot_y, left + dot_d, dot_y + dot_d);
SelectObject(hdc, old_pen);
SelectObject(hdc, old_br);
let _ = DeleteObject(HGDIOBJ(b.0));
}
line(&mut r2, &snap.line2, 13, state_fg);
let hint = if HOTKEY_OK.load(std::sync::atomic::Ordering::Relaxed) {
"Ctrl+Alt+F12 停止 · 点击解除 · ✕ 退出"
} else {
"点击本条=停止 · 再点=解除 · ✕ 退出"
};
line(&mut r3, hint, 11, 0x006E_6E6E);
let _ = EndPaint(hwnd, &ps);
}
unsafe fn make_brand_icon() -> windows::Win32::UI::WindowsAndMessaging::HICON {
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Gdi::{GetDC, ReleaseDC};
const SZ: i32 = 32;
let radius = 7.0f32;
let dot_r = 8.0f32;
let mut px = vec![0u32; (SZ * SZ) as usize];
for y in 0..SZ {
for x in 0..SZ {
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
let cx = fx.clamp(radius, SZ as f32 - radius);
let cy = fy.clamp(radius, SZ as f32 - radius);
let in_round = (fx - cx) * (fx - cx) + (fy - cy) * (fy - cy) <= radius * radius;
if !in_round {
continue;
}
let dcx = fx - SZ as f32 / 2.0;
let dcy = fy - SZ as f32 / 2.0;
px[(y * SZ + x) as usize] = if dcx * dcx + dcy * dcy <= dot_r * dot_r {
0xFF3F_B950 } else {
0xFF1E_1E1E };
}
}
let bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: SZ,
biHeight: -SZ, biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let hdc = GetDC(None);
let mem = CreateCompatibleDC(Some(hdc));
let mut bits: *mut std::ffi::c_void = std::ptr::null_mut();
let color =
CreateDIBSection(Some(mem), &bmi, DIB_RGB_COLORS, &mut bits, None, 0).unwrap_or_default();
if !color.is_invalid() && !bits.is_null() {
std::ptr::copy_nonoverlapping(px.as_ptr() as *const u8, bits as *mut u8, px.len() * 4);
}
let _ = windows::Win32::Graphics::Gdi::DeleteDC(mem);
let _ = ReleaseDC(None, hdc);
let mask = CreateBitmap(SZ, SZ, 1, 1, None);
let ii = ICONINFO {
fIcon: true.into(),
hbmMask: mask,
hbmColor: color,
..Default::default()
};
let icon = CreateIconIndirect(&ii).unwrap_or_default();
let _ = HWND(std::ptr::null_mut()); icon
}
unsafe fn context_menu(hwnd: HWND) {
use windows::Win32::Foundation::POINT;
let Ok(menu) = CreatePopupMenu() else {
return;
};
let _ = AppendMenuW(menu, MF_STRING, 1, w!("清除停止标志"));
let _ = AppendMenuW(menu, MF_STRING, 2, w!("显示 / 隐藏状态条"));
let _ = AppendMenuW(menu, MF_SEPARATOR, 0, None);
let _ = AppendMenuW(menu, MF_STRING, 3, w!("退出 actl-signal"));
let mut pt = POINT::default();
let _ = windows::Win32::UI::WindowsAndMessaging::GetCursorPos(&mut pt);
let _ = SetForegroundWindow(hwnd);
let cmd = TrackPopupMenu(
menu,
TPM_RETURNCMD | TPM_RIGHTBUTTON,
pt.x,
pt.y,
None,
hwnd,
None,
)
.0;
let _ = DestroyMenu(menu);
match cmd {
1 => {
SignalPaths::default().clear_stop();
let _ = InvalidateRect(Some(hwnd), None, true);
}
2 => toggle_visible(hwnd),
3 => {
let _ = windows::Win32::UI::WindowsAndMessaging::DestroyWindow(hwnd);
}
_ => {}
}
}
unsafe fn toggle_visible(hwnd: HWND) {
use windows::Win32::UI::WindowsAndMessaging::{IsWindowVisible, SW_HIDE, SW_SHOW, ShowWindow};
if IsWindowVisible(hwnd).as_bool() {
let _ = ShowWindow(hwnd, SW_HIDE);
} else {
let _ = ShowWindow(hwnd, SW_SHOW);
}
}
unsafe fn save_position(hwnd: HWND) {
use windows::Win32::Foundation::RECT;
use windows::Win32::UI::WindowsAndMessaging::GetWindowRect;
let mut rc = RECT::default();
if GetWindowRect(hwnd, &mut rc).is_ok() {
let dir = SignalPaths::default().dir;
let _ = std::fs::create_dir_all(&dir);
let _ = std::fs::write(
dir.join("signal-pos.txt"),
format!("{},{}", rc.left, rc.top),
);
}
}
unsafe fn load_position(w: i32, h: i32) -> (i32, i32) {
use windows::Win32::Foundation::RECT;
let mut work = RECT::default();
let _ = SystemParametersInfoW(
SPI_GETWORKAREA,
0,
Some(&mut work as *mut RECT as *mut _),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
);
let default = (work.right - w - 16, work.top + 16);
let Some((x, y)) = std::fs::read_to_string(SignalPaths::default().dir.join("signal-pos.txt"))
.ok()
.and_then(|t| {
let (xs, ys) = t.split_once(',')?;
Some((
xs.trim().parse::<i32>().ok()?,
ys.trim().parse::<i32>().ok()?,
))
})
else {
return default;
};
let (vx, vy, vw, vh) = (
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
GetSystemMetrics(SM_CXVIRTUALSCREEN),
GetSystemMetrics(SM_CYVIRTUALSCREEN),
);
let x = x.clamp(vx, vx + vw - w);
let y = y.clamp(vy, vy + vh - h);
(x, y)
}
unsafe extern "system" fn wnd_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match msg {
WM_TIMER if wparam.0 == TIMER_ID => {
let snap = poll();
let key = format!(
"{:?}|{}|{}",
std::mem::discriminant(&snap.mode),
snap.line1,
snap.line2
);
let prev =
windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW(hwnd, GWLP_USERDATA);
if prev != 0 {
drop(Box::from_raw(prev as *mut String));
}
let changed = prev == 0 || unsafe { &*(prev as *const String) } != &key;
let _ = windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW(
hwnd,
GWLP_USERDATA,
Box::into_raw(Box::new(key)) as isize,
);
if changed {
let _ = InvalidateRect(Some(hwnd), None, true);
}
LRESULT(0)
}
WM_PAINT => {
let snap = poll();
draw(hwnd, &snap);
LRESULT(0)
}
WM_LBUTTONDOWN => {
SignalPaths::default().clear_stop();
let _ = InvalidateRect(Some(hwnd), None, true);
LRESULT(0)
}
WM_RBUTTONUP => {
context_menu(hwnd);
LRESULT(0)
}
WM_NCHITTEST => {
let _ = DefWindowProcW(hwnd, msg, wparam, lparam);
LRESULT(HTCAPTION as isize)
}
WM_EXITSIZEMOVE => {
save_position(hwnd);
LRESULT(0)
}
WM_HOTKEY if wparam.0 as i32 == HOTKEY_ID => {
SignalPaths::default().request_stop();
let _ = InvalidateRect(Some(hwnd), None, true);
LRESULT(0)
}
WM_APP => {
let mouse = (lparam.0 & 0xFFFF) as u32;
match mouse {
WM_LBUTTONUP => {
SignalPaths::default().clear_stop();
let _ = InvalidateRect(Some(hwnd), None, true);
}
WM_LBUTTONDBLCLK => toggle_visible(hwnd),
WM_RBUTTONUP => context_menu(hwnd),
_ => {}
}
LRESULT(0)
}
WM_DESTROY => {
let _ = KillTimer(Some(hwnd), TIMER_ID);
let _ = UnregisterHotKey(Some(hwnd), HOTKEY_ID);
let mut nid: NOTIFYICONDATAW = std::mem::zeroed();
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
nid.hWnd = hwnd;
nid.uID = 1;
let _ = Shell_NotifyIconW(NIM_DELETE, &nid);
PostQuitMessage(0);
LRESULT(0)
}
_ => DefWindowProcW(hwnd, msg, wparam, lparam),
}
}
fn main() {
unsafe {
let _ = windows::Win32::UI::HiDpi::SetProcessDpiAwarenessContext(
windows::Win32::UI::HiDpi::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
);
let hinstance = GetModuleHandleW(None).expect("hinstance");
let class_name = w!("actl_signal_window");
let wc = WNDCLASSW {
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: hinstance.into(),
hIcon: make_brand_icon(),
hCursor: LoadCursorW(None, IDC_ARROW).unwrap_or_default(),
lpszClassName: class_name,
..Default::default()
};
let atom = RegisterClassW(&wc);
assert!(atom != 0, "RegisterClassW failed");
let mut work = RECT::default();
let _ = SystemParametersInfoW(
SPI_GETWORKAREA,
0,
Some(&mut work as *mut RECT as *mut _),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
);
let _ = &work; let (x, y) = load_position(scale(WIDTH), scale(HEIGHT));
let ex = WINDOW_EX_STYLE(
WS_EX_TOPMOST.0 | WS_EX_NOACTIVATE.0 | WS_EX_TOOLWINDOW.0 | WS_EX_LAYERED.0,
);
let hwnd = CreateWindowExW(
ex,
class_name,
w!("actl 信号"),
WINDOW_STYLE(WS_POPUP.0 | WS_VISIBLE.0),
x,
y,
scale(WIDTH),
scale(HEIGHT),
None,
None,
Some(hinstance.into()),
None,
)
.expect("CreateWindowExW");
let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), 235, LWA_ALPHA);
let mut nid: NOTIFYICONDATAW = std::mem::zeroed();
nid.cbSize = std::mem::size_of::<NOTIFYICONDATAW>() as u32;
nid.hWnd = hwnd;
nid.uID = 1;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_APP;
nid.hIcon = make_brand_icon();
let tip = wide("actl 信号(空闲)");
nid.szTip[..tip.len() - 1].copy_from_slice(&tip[..tip.len() - 1]);
let _ = Shell_NotifyIconW(NIM_ADD, &nid);
HOTKEY_OK.store(
RegisterHotKey(
Some(hwnd),
HOTKEY_ID,
MOD_CONTROL | MOD_ALT,
VK_F12.0 as u32,
)
.is_ok(),
std::sync::atomic::Ordering::Relaxed,
);
SetTimer(Some(hwnd), TIMER_ID, TIMER_MS, None);
let mut msg = MSG::default();
while GetMessageW(&mut msg, None, 0, 0).as_bool() {
let _ = TranslateMessage(&msg);
let _ = DispatchMessageW(&msg);
}
let _ = HWND_BROADCAST;
let _ = GWLP_USERDATA;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn brand_icon_is_created() {
unsafe {
let icon = make_brand_icon();
assert!(
!icon.is_invalid(),
"CreateIconIndirect must produce a valid HICON"
);
}
}
}