Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions crates/core/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,8 +516,10 @@ impl AgentState {
(Response::Ok, false)
}
Command::AdoptWindows { hwnds } => {
let targets: Vec<crate::hide::Target> =
hwnds.iter().map(|&h| crate::hide::Target::bare(h, 0)).collect();
let targets: Vec<crate::hide::Target> = hwnds
.iter()
.map(|&h| crate::hide::Target::bare(h, 0))
.collect();
// 恢复工具的手动隐藏不施加副作用,仅隐藏并纳入记录。
let mut setting = self.config.setting.clone();
setting.mute_after_hide = false;
Expand Down Expand Up @@ -1090,7 +1092,10 @@ pub fn run(options: AgentOptions) {
let hwnd = match create_agent_window() {
Ok(hwnd) => hwnd,
Err(e) => {
log_error!("创建代理窗口失败,核心无法启动: {}", crate::util::win_err(&e));
log_error!(
"创建代理窗口失败,核心无法启动: {}",
crate::util::win_err(&e)
);
return;
}
};
Expand Down
5 changes: 4 additions & 1 deletion crates/core/src/effects_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ mod tests {

impl Effects for Recorder {
fn mute(&self, pid: u32, mute: bool) {
self.calls.lock().unwrap().push(format!("mute:{pid}:{mute}"));
self.calls
.lock()
.unwrap()
.push(format!("mute:{pid}:{mute}"));
}
fn suspend(&self, pid: u32, _enhanced: bool) {
self.calls.lock().unwrap().push(format!("suspend:{pid}"));
Expand Down
108 changes: 98 additions & 10 deletions crates/core/src/hide.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ pub fn expand_descendants(roots: &[u32], edges: &[(u32, u32)]) -> Vec<u32> {
/// [`HideController::commit_hide`] 原样执行,两段之间由调用方落盘意图。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HidePlan {
/// 本次新增的隐藏目标(已剔除死句柄 / 已隐藏项 / 查不到 PID 的项)。
/// 本次新增的隐藏目标(已剔除死句柄 / 不可见项 / 已隐藏项 / 查不到 PID 的项)。
pub fresh: Vec<Target>,
/// 本次新增的静音进程。
pub mute: Vec<ProcRecord>,
Expand Down Expand Up @@ -310,8 +310,10 @@ impl<W: WindowManager, E: Effects> HideController<W, E> {
/// 与 PID 补查(仍查不到的目标剔除)。`freeze_pids` 为最终要冻结的 PID 集
/// (可能已含子进程树),仅在 `freeze_after_hide` 开启时生效。
///
/// 隐藏是累加的,`show` 时一并恢复。已在隐藏 / 静音 / 冻结集内的目标会被
/// 跳过——挂起是计数式的,重复施加会让解冻次数对不上。
/// 只有「由本程序从可见变为不可见」的窗口才进入隐藏集,恢复即逆转这次
/// 改变;本来就不可见的目标不入集。隐藏是累加的,`show` 时一并恢复。
/// 已在隐藏 / 静音 / 冻结集内的目标会被跳过——挂起是计数式的,重复施加
/// 会让解冻次数对不上。
pub fn plan_hide(
&mut self,
setting: &Setting,
Expand All @@ -323,9 +325,12 @@ impl<W: WindowManager, E: Effects> HideController<W, E> {

let mut fresh: Vec<Target> = Vec::new();
for t in targets {
// 只隐藏当前可见的窗口:本来就不可见的目标(如程序自行藏到托盘,
// Steam 的关闭按钮即是)不入集,恢复时也就不会被错误地弹出来。
if known.contains(&t.hwnd)
|| fresh.iter().any(|f| f.hwnd == t.hwnd)
|| !self.wm.is_window(t.hwnd)
|| !self.wm.is_visible(t.hwnd)
{
continue;
}
Expand Down Expand Up @@ -992,7 +997,14 @@ mod tests {
assert_eq!(*controller.effects.pauses.borrow(), 1, "应发送一次暂停键");

let outcome = controller.show();
assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 });
assert_eq!(
outcome,
ShowOutcome {
shown: 1,
stale: 0,
refound: 0
}
);
assert!(!controller.is_hidden());
assert!(controller.wm.is_visible(10), "恢复后微信应可见");
assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻");
Expand Down Expand Up @@ -1212,7 +1224,14 @@ mod tests {
..Default::default()
});

assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 });
assert_eq!(
outcome,
ShowOutcome {
shown: 1,
stale: 0,
refound: 0
}
);
assert!(!controller.is_hidden(), "恢复完成后应回到未隐藏状态");
assert!(controller.wm.is_visible(10), "崩溃前隐藏的窗口应被找回");
assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻进程");
Expand Down Expand Up @@ -1280,7 +1299,11 @@ mod tests {
let outcome = controller.show();
assert_eq!(
outcome,
ShowOutcome { shown: 1, stale: 2, refound: 0 },
ShowOutcome {
shown: 1,
stale: 2,
refound: 0
},
"死句柄与被复用句柄都应跳过"
);
assert!(!controller.wm.is_visible(20), "被复用的句柄不得被弹出来");
Expand Down Expand Up @@ -1354,11 +1377,23 @@ mod tests {
};
let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 0);
let mut controller = HideController::new(wm, MockEffects::default());
controller.apply_hide(&setting, &[Target::from_window(&win("微信", 10, "WeChat.exe", "C:\\WeChat.exe"))], &[]);
controller.apply_hide(
&setting,
&[Target::from_window(&win(
"微信",
10,
"WeChat.exe",
"C:\\WeChat.exe",
))],
&[],
);

assert!(controller.tracks_window(10));
assert!(controller.update_title(10, "微信 - 新会话"));
assert!(!controller.update_title(10, "微信 - 新会话"), "标题未变不算更新");
assert!(
!controller.update_title(10, "微信 - 新会话"),
"标题未变不算更新"
);
assert!(
!controller.update_title(10, bosskey_common::NO_TITLE),
"NO_TITLE 不参与同步"
Expand All @@ -1378,8 +1413,14 @@ mod tests {
];
assert!(sync_rule_titles(&mut rules, 10, "新标题"));
assert_eq!(rules[0].title, "新标题");
assert!(!sync_rule_titles(&mut rules, 10, "新标题"), "标题未变不算更新");
assert!(!sync_rule_titles(&mut rules, 99, "别的"), "句柄不匹配不更新");
assert!(
!sync_rule_titles(&mut rules, 10, "新标题"),
"标题未变不算更新"
);
assert!(
!sync_rule_titles(&mut rules, 99, "别的"),
"句柄不匹配不更新"
);
assert!(
!sync_rule_titles(&mut rules, 10, NO_TITLE),
"NO_TITLE 不参与同步"
Expand Down Expand Up @@ -1452,6 +1493,53 @@ mod tests {
assert!(controller.wm.is_visible(99), "可见窗口不受影响");
}

#[test]
fn already_invisible_windows_are_not_recorded_or_restored() {
let setting = Setting {
hide_current: false,
mute_after_hide: true,
..Setting::default()
};
// Steam 用关闭按钮把自己藏了起来(窗口仍存在但不可见),记事本可见;
// 两者都被规则命中。
let wm = MockWm::new(
vec![
win_pid("Steam", 10, "steam.exe", 500, "C:\\steam.exe"),
win_pid("记事本", 20, "notepad.exe", 600, "C:\\notepad.exe"),
],
0,
);
wm.hide(10);
let mut controller = HideController::new(wm, MockEffects::default());

controller.apply_hide(
&setting,
&[Target::bare(10, 500), Target::bare(20, 600)],
&[],
);
assert_eq!(controller.hidden_count(), 1, "已不可见的窗口不应入集");
assert_eq!(
*controller.effects.mutes.borrow(),
vec![(600, true)],
"只对真正被隐藏的窗口施加副作用"
);

let outcome = controller.show();
assert_eq!(
outcome,
ShowOutcome {
shown: 1,
stale: 0,
refound: 0
}
);
assert!(controller.wm.is_visible(20), "记事本应恢复");
assert!(
!controller.wm.is_visible(10),
"Steam 自己藏起来的窗口不得被恢复弹出"
);
}

#[test]
fn release_windows_frees_whole_process_and_shows_unknown_handles() {
let setting = Setting {
Expand Down
16 changes: 12 additions & 4 deletions crates/core/src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@ impl Msg {
Msg::ErrCoreExited => "核心已退出",
Msg::ErrNotifyCore => "无法通知核心",
Msg::ErrCoreTimeout => "核心响应超时",
Msg::ErrCoreExeMissing => "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。",
Msg::ErrCoreExeMissing => {
"未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。"
}
Msg::ErrFreezePartial => "{failed}/{total} 个进程冻结失败",
Msg::ErrResumePartial => "{failed}/{total} 个进程解冻失败",
Msg::ErrUrlSchemeNotAllowed => "只允许打开 http/https/mailto 链接",
Expand All @@ -106,7 +108,9 @@ impl Msg {
Msg::HiddenBody => "Windows hidden",
Msg::ShownBody => "Windows restored",
Msg::ConfigExeMissing => "Settings app not found",
Msg::RecoveryPersistFailedBody => "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit",
Msg::RecoveryPersistFailedBody => {
"Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit"
}
Msg::AutostartOffTitle => "Startup disabled",
Msg::AutostartOffBody => "Boss Key will no longer start with Windows",
Msg::AutostartOnTitle => "Startup enabled",
Expand All @@ -123,7 +127,9 @@ impl Msg {
Msg::ErrCoreExited => "The core has exited",
Msg::ErrNotifyCore => "Cannot reach the core",
Msg::ErrCoreTimeout => "The core did not respond in time",
Msg::ErrCoreExeMissing => "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again.",
Msg::ErrCoreExeMissing => {
"Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again."
}
Msg::ErrFreezePartial => "Failed to freeze {failed} of {total} processes",
Msg::ErrResumePartial => "Failed to resume {failed} of {total} processes",
Msg::ErrUrlSchemeNotAllowed => "Only http/https/mailto links may be opened",
Expand Down Expand Up @@ -161,7 +167,9 @@ impl Msg {
Msg::ErrCoreExited => "核心已結束",
Msg::ErrNotifyCore => "無法通知核心",
Msg::ErrCoreTimeout => "核心回應逾時",
Msg::ErrCoreExeMissing => "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。",
Msg::ErrCoreExeMissing => {
"找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。"
}
Msg::ErrFreezePartial => "{failed}/{total} 個程序凍結失敗",
Msg::ErrResumePartial => "{failed}/{total} 個程序解除凍結失敗",
Msg::ErrUrlSchemeNotAllowed => "僅允許開啟 http/https/mailto 連結",
Expand Down
5 changes: 4 additions & 1 deletion crates/core/src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,10 @@ mod tests {
let dir = temp_dir();
let logger = Logger::new(dir.clone(), 7);
// 直接验证格式化结果(warn_at 走全局 logger,测试里用本地实例复刻其格式)。
logger.log(Level::Warn, &format!("{} ({}:{})", "落盘失败", "agent.rs", 42));
logger.log(
Level::Warn,
&format!("{} ({}:{})", "落盘失败", "agent.rs", 42),
);
let logs: Vec<_> = fs::read_dir(&dir).unwrap().flatten().collect();
let content = fs::read_to_string(logs[0].path()).unwrap();
assert!(content.contains("[WARN] 落盘失败 (agent.rs:42)"));
Expand Down
5 changes: 1 addition & 4 deletions crates/core/src/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,7 @@ mod tests {
std::fs::write(&path, "{ not valid json !!").unwrap();
assert_eq!(load(&path), None, "损坏文件应按无快照处理而非 panic");
assert!(!path.exists(), "损坏文件不应留在原名");
assert!(
corrupt_path(&path).exists(),
"损坏文件应改名保留现场供排查"
);
assert!(corrupt_path(&path).exists(), "损坏文件应改名保留现场供排查");
}

#[test]
Expand Down
14 changes: 11 additions & 3 deletions crates/core/src/win_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ impl Drop for WinEventHook {
#[cfg(test)]
mod tests {
use super::*;
use windows::Win32::UI::WindowsAndMessaging::{EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR};
use windows::Win32::UI::WindowsAndMessaging::{
EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR,
};

#[test]
fn only_toplevel_destroy_show_namechange_are_relevant() {
Expand All @@ -103,8 +105,14 @@ mod tests {
assert!(relevant(EVENT_OBJECT_SHOW, obj, child));
assert!(relevant(EVENT_OBJECT_NAMECHANGE, obj, child));
assert!(!relevant(EVENT_OBJECT_HIDE, obj, child), "隐藏事件不用追踪");
assert!(!relevant(EVENT_OBJECT_FOCUS, obj, child), "区间内的无关事件应过滤");
assert!(!relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), "非窗口对象应过滤");
assert!(
!relevant(EVENT_OBJECT_FOCUS, obj, child),
"区间内的无关事件应过滤"
);
assert!(
!relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child),
"非窗口对象应过滤"
);
assert!(!relevant(EVENT_OBJECT_DESTROY, obj, 3), "子对象事件应过滤");
}
}
5 changes: 1 addition & 4 deletions crates/core/tests/agent_window_tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,7 @@ fn destroying_a_hidden_window_clears_the_record_in_real_time() {
);
std::thread::sleep(Duration::from_millis(50));
}
assert!(
!recovery_path.exists(),
"记录清空后恢复文件应同步清除"
);
assert!(!recovery_path.exists(), "记录清空后恢复文件应同步清除");

let quit = client.send(&Command::Quit).unwrap();
assert_eq!(quit, Response::Ok);
Expand Down
4 changes: 4 additions & 0 deletions docs/en/guide/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Some stripped-down or older Windows builds (Windows 7 and earlier) may not inclu
A settings window that will not open **does not affect the core's hiding features** — the core is a fully native program and does not depend on WebView2. You can still hide windows with your configured hotkeys.
:::

## Will restoring pop up programs I "closed" to the tray?

No. Hiding only records windows that were **visible at the time**, and restoring only reverses Boss Key's own hiding; windows an app hid by itself (Steam's close button, for example, merely hides its window) are untouched and will not be shown on restore.

## Antivirus flags or blocks Boss Key — what now?

Boss Key listens for global hotkeys and hides windows, behaviour that antivirus software sometimes misreads. v3 uses a native single-file Rust implementation, which considerably reduces false positives. If it is still blocked:
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ v3 的配置程序(`config.exe`)使用 Tauri 编写,其运行依赖系统
配置界面无法打开**不影响核心的隐藏功能**——核心是纯原生程序,不依赖 WebView2。你仍可用已配置好的热键正常隐藏窗口。
:::

## 恢复时会把已"关闭"到托盘的程序弹出来吗?

不会。隐藏时只记录**当时可见**的窗口,恢复只逆转 Boss Key 自己的隐藏动作;程序自行藏到托盘的窗口(如 Steam 的关闭按钮只是隐藏窗口)不受影响,恢复时不会被弹出。

## 杀毒软件报毒 / 拦截怎么办?

Boss Key 会监听全局热键、隐藏窗口,这类行为有时会被杀软误判。v3 已改用 Rust 原生单文件实现,显著降低了误报概率。若仍被拦截:
Expand Down
4 changes: 4 additions & 0 deletions docs/zh-tw/guide/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ v3 的設定程式(`config.exe`)使用 Tauri 撰寫,其執行相依於系
設定介面無法開啟**不影響核心的隱藏功能**——核心是純原生程式,不相依於 WebView2。您仍可用已設定好的快速鍵正常隱藏視窗。
:::

## 復原時會把已「關閉」到通知區域的程式彈出來嗎?

不會。隱藏時只記錄**當時可見**的視窗,復原只逆轉 Boss Key 自己的隱藏動作;程式自行藏到通知區域的視窗(如 Steam 的關閉按鈕只是隱藏視窗)不受影響,復原時不會被彈出。

## 防毒軟體誤判/攔截怎麼辦?

Boss Key 會監聽全域快速鍵、隱藏視窗,這類行為有時會被防毒軟體誤判。v3 已改用 Rust 原生單一檔案實作,顯著降低了誤判機率。若仍被攔截:
Expand Down
Loading