#![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::{
BeginPaint, CreateSolidBrush, 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::{
CS_HREDRAW, CS_VREDRAW, CreateWindowExW, DefWindowProcW, DispatchMessageW, GWLP_USERDATA,
GetMessageW, HWND_BROADCAST, IDC_ARROW, IDI_APPLICATION, KillTimer, LWA_ALPHA, LoadCursorW,
LoadIconW, MB_OK, MB_SETFOREGROUND, MSG, MessageBoxW, PostQuitMessage, RegisterClassW,
SPI_GETWORKAREA, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, SetLayeredWindowAttributes, SetTimer,
SystemParametersInfoW, TranslateMessage, WINDOW_EX_STYLE, WINDOW_STYLE, WM_APP, WM_DESTROY,
WM_HOTKEY, WM_LBUTTONDOWN, 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::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, OUT_DEFAULT_PRECIS,
};
let slot = match height {
17 => &BIG,
13 => &MID,
_ => &DIM,
};
let h = *slot.get_or_init(|| {
CreateFontW(
-scale(height), 0,
0,
0,
FW_NORMAL.0 as i32,
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 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 => {
let tip = w!("左键点击=清除停止;关闭本窗口即退出(托盘菜单规划中)");
let _ = MessageBoxW(Some(hwnd), tip, w!("actl-signal"), MB_OK | MB_SETFOREGROUND);
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 => {
SignalPaths::default().clear_stop();
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: LoadIconW(None, IDI_APPLICATION).unwrap_or_default(),
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 x = work.right - scale(WIDTH) - scale(12);
let y = work.top + scale(12);
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 = LoadIconW(None, IDI_APPLICATION).unwrap_or_default();
let tip = wide("actl 信号(空闲)");
nid.szTip[..tip.len() - 1].copy_from_slice(&tip[..tip.len() - 1]);
let _ = Shell_NotifyIconW(NIM_ADD, &nid);
assert!(
RegisterHotKey(
Some(hwnd),
HOTKEY_ID,
MOD_CONTROL | MOD_ALT,
VK_F12.0 as u32
)
.is_ok(),
"RegisterHotKey(Ctrl+Alt+F12) failed - another program owns it"
);
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;
}
}