Release 1.0.1 - #2
MoonTheRipper wants to merge 2 commits into
Conversation
- add theme system (dark/light/auto/frosted) with UI selector - support terminal app choice and keep-open behavior per step - track last-launched processes and close them on exit if enabled - implement Windows app discovery (Steam/Epic/installed apps)
📝 WalkthroughWalkthroughAdds theming (dark, light, auto, frosted), per-step keep-open and terminal_app options, Windows app discovery (Steam/Epic/Uninstall), LastLaunch process tracking with close-on-exit, a Kill & Wipe workflow (UI and backend), startup flag handling, and related UI/settings/dialog updates. Changes
Sequence Diagram(s)sequenceDiagram
participant Frontend
participant Tauri
participant LastLaunch
participant Lifecycle
rect rgba(100, 150, 200, 0.5)
Note over Frontend,Tauri: Launch profile with process tracking
Frontend->>Tauri: invoke launch_profile (enabled steps)
Tauri->>Tauri: determine process_names (filter by process_name && !keep_open)
Tauri->>LastLaunch: set_last_launch_processes(process_names)
LastLaunch->>LastLaunch: sanitize, sort, dedupe, store
end
rect rgba(150, 100, 200, 0.5)
Note over Frontend,Lifecycle: App exit / Quit (close-on-exit)
Frontend->>Tauri: window close / tray quit
Tauri->>Lifecycle: close_apps_on_exit(app)
Lifecycle->>Tauri: load_config() (check close_on_exit)
Lifecycle->>LastLaunch: get_processes()
Lifecycle->>Lifecycle: iterate & attempt kill each process
Lifecycle->>Tauri: return
end
sequenceDiagram
participant Frontend
participant Dialogs
participant Tauri
participant KillWipeModule
rect rgba(200, 150, 100, 0.5)
Note over Frontend,Dialogs: User triggers Kill & Wipe
Frontend->>Dialogs: showKillAndWipe(settings)
Dialogs->>Frontend: user confirms with options (+create_shortcut)
Frontend->>Tauri: invoke kill_and_wipe(options)
Tauri->>KillWipeModule: run(options) (blocking thread)
KillWipeModule->>Tauri: KillWipeReport (killed_count, failures, dns_flushed, etc.)
Tauri->>Frontend: return report
Frontend->>Dialogs: show summary / info (maybe create shortcut or post-logout message)
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src-tauri/src/discovery.rs`:
- Around line 393-419: extract_exe_path currently uses lower.find(".exe") which
can match a ".exe" inside a directory name; change the logic to locate the
".exe" that is actually the file suffix by searching from the end (e.g., use
rfind) or by scanning for ".exe" occurrences and selecting one where the
character immediately after the match is a path separator or end-of-string;
update the function extract_exe_path to use that end-aware match (still
trim/expand and return same substring text[..idx+4]) so you only extract valid
executable file names.
In `@src-tauri/src/launcher.rs`:
- Around line 106-138: The launch logic breaks when command contains spaces
because cmd.exe start treats the first quoted argument as a window title; fix
this by inserting an explicit empty title argument and quoting the command when
needed: in the branch that constructs Command::new("cmd") change the args to
include the empty title (e.g., args(["/C", "start", "", "cmd", flag,
"ed_command])) where quoted_command = format!("\"{}\"", command) when
command is non-empty; apply the same quoting approach in the Command::new("wt")
branch when you append ["cmd", flag, "ed_command] so multi-word commands
(like "npm run dev") are passed as a single argument and not split by the shell
(refer to the terminal_app handling and the cmd/wt arg construction around where
flag and command are used).
🧹 Nitpick comments (8)
frontend/src/steps.js (1)
26-28: Badge and detail logic duplicated withstartup.js.The
badgeLabelternary (Lines 26-28) and thegetStepDetailterminal branch (Lines 106-111) are near-identical copies ofstartup.jsLines 28-33. If a new terminal type is added (e.g., PowerShell), both files must be updated in lockstep. Consider extractinggetStepBadgeLabel(step)and a sharedgetStepDetail(step)into a common module (orconfig.js).Also applies to: 106-111
frontend/src/startup.js (1)
166-170:escapeHtmlis also duplicated acrosssteps.jsandstartup.js.Both files define an identical
escapeHtmlfunction. Consider extracting it to a shared utility module alongside the badge/detail helpers.src-tauri/src/discovery.rs (1)
421-451: Duplicateexpand_env_varsimplementation — consider extracting to a shared utility.This function duplicates logic in
src-tauri/src/launcher.rs:182-199. The implementations have significant behavioral differences: this version preserves unexpanded%VAR%patterns and handles%%as a literal%, while the launcher version breaks on the first unexpandable variable. The discovery.rs implementation is more robust and should be consolidated into a shared utility module to prevent divergence and avoid bugs in launcher.rs when encountering unexpandable variables.frontend/src/styles.css (1)
38-104: Light and auto theme blocks duplicate all variables — consider a shared base.The
[data-theme="light"]block (lines 38–58) and the[data-theme="auto"]block (lines 60–80) are identical, and the dark-mode media query (lines 82–104) duplicates the:rootdefaults. This works correctly but triples the maintenance surface for color tokens.A future refactor could extract shared palettes into CSS custom property layers or a preprocessor mixin, but this is fine for now.
src-tauri/src/lib.rs (2)
77-88: Inconsistent indentation insideon_window_eventclosure.Lines 80–88 use irregular indentation that breaks from the 8-space nesting used elsewhere in the builder chain. This appears to be a formatting issue.
🔧 Suggested formatting fix
.on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { - let cfg = config::load_config(); - if cfg.settings.minimize_to_tray { - api.prevent_close(); - let _ = window.hide(); - return; + let cfg = config::load_config(); + if cfg.settings.minimize_to_tray { + api.prevent_close(); + let _ = window.hide(); + return; + } + lifecycle::close_apps_on_exit(&window.app_handle()); } - lifecycle::close_apps_on_exit(&window.app_handle()); - } - }) + })
80-80: Config loaded from disk on everyCloseRequestedevent.
config::load_config()performs a synchronous file read. Since this only fires on close requests it's not a hot path, but note that on some platformsCloseRequestedcan fire multiple times (e.g., repeated close button clicks while a dialog is open). The cost is negligible here, so this is just a heads-up rather than a required change.src-tauri/src/launcher.rs (1)
182-198:expand_env_varsstops expanding on first unresolvable variable.If the input is
%HOME%\%MISSING_VAR%\foo, the function expands%HOME%but then breaks on%MISSING_VAR%, leaving it unexpanded. Consider skipping unresolvable variables instead of breaking:🔧 Suggested fix
fn expand_env_vars(input: &str) -> String { let mut result = input.to_string(); - // Expand %VAR% patterns - while let Some(start) = result.find('%') { + let mut pos = 0; + while pos < result.len() { + let start = match result[pos..].find('%') { + Some(s) => pos + s, + None => break, + }; if let Some(end) = result[start + 1..].find('%') { let var_name = &result[start + 1..start + 1 + end]; if let Ok(value) = std::env::var(var_name) { result = format!("{}{}{}", &result[..start], value, &result[start + 2 + end..]); + pos = start + value.len(); } else { - // Can't expand, skip this one - break; + pos = start + 2 + end; } } else { break; } } result }frontend/src/dialogs.js (1)
295-334:keep_opennot included in the field cleanup block, but still safe.Lines 298–303 delete type-specific fields when switching step types, but
keep_openis omitted from the cleanup. This is fine because every branch in theswitchbelow unconditionally setskeep_open, so stale values can't leak. Adding it to the cleanup block would be slightly more defensive but isn't required.
| #[cfg(target_os = "windows")] | ||
| fn extract_exe_path(raw: &str) -> Option<String> { | ||
| let expanded = expand_env_vars(raw); | ||
| let mut text = expanded.trim().to_string(); | ||
| if text.is_empty() { | ||
| return None; | ||
| } | ||
|
|
||
| if text.starts_with('"') { | ||
| if let Some(end) = text[1..].find('"') { | ||
| let end_idx = 1 + end; | ||
| text = text[1..end_idx].to_string(); | ||
| } | ||
| } | ||
|
|
||
| let lower = text.to_lowercase(); | ||
| if let Some(idx) = lower.find(".exe") { | ||
| let path = text[..idx + 4].trim().to_string(); | ||
| if path.is_empty() { | ||
| None | ||
| } else { | ||
| Some(path) | ||
| } | ||
| } else { | ||
| None | ||
| } | ||
| } |
There was a problem hiding this comment.
extract_exe_path can match .exe substring in directory names.
lower.find(".exe") on Line 409 matches the first occurrence of .exe in the entire path, which could be a directory component (e.g., C:\foo.executive\bar.exe → incorrectly extracts C:\foo.exe). This is mitigated by the downstream .exists() check in both scan_windows (Line 362) and scan_epic (Line 278), so real-world impact is low, but worth noting.
A slightly more robust approach would be to search from the end or specifically match .exe at a path-segment boundary.
Proposed fix
fn extract_exe_path(raw: &str) -> Option<String> {
let expanded = expand_env_vars(raw);
let mut text = expanded.trim().to_string();
if text.is_empty() {
return None;
}
if text.starts_with('"') {
if let Some(end) = text[1..].find('"') {
let end_idx = 1 + end;
text = text[1..end_idx].to_string();
}
}
let lower = text.to_lowercase();
- if let Some(idx) = lower.find(".exe") {
+ if let Some(idx) = lower.rfind(".exe") {
let path = text[..idx + 4].trim().to_string();
if path.is_empty() {
None
} else {
Some(path)
}
} else {
None
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[cfg(target_os = "windows")] | |
| fn extract_exe_path(raw: &str) -> Option<String> { | |
| let expanded = expand_env_vars(raw); | |
| let mut text = expanded.trim().to_string(); | |
| if text.is_empty() { | |
| return None; | |
| } | |
| if text.starts_with('"') { | |
| if let Some(end) = text[1..].find('"') { | |
| let end_idx = 1 + end; | |
| text = text[1..end_idx].to_string(); | |
| } | |
| } | |
| let lower = text.to_lowercase(); | |
| if let Some(idx) = lower.find(".exe") { | |
| let path = text[..idx + 4].trim().to_string(); | |
| if path.is_empty() { | |
| None | |
| } else { | |
| Some(path) | |
| } | |
| } else { | |
| None | |
| } | |
| } | |
| #[cfg(target_os = "windows")] | |
| fn extract_exe_path(raw: &str) -> Option<String> { | |
| let expanded = expand_env_vars(raw); | |
| let mut text = expanded.trim().to_string(); | |
| if text.is_empty() { | |
| return None; | |
| } | |
| if text.starts_with('"') { | |
| if let Some(end) = text[1..].find('"') { | |
| let end_idx = 1 + end; | |
| text = text[1..end_idx].to_string(); | |
| } | |
| } | |
| let lower = text.to_lowercase(); | |
| if let Some(idx) = lower.rfind(".exe") { | |
| let path = text[..idx + 4].trim().to_string(); | |
| if path.is_empty() { | |
| None | |
| } else { | |
| Some(path) | |
| } | |
| } else { | |
| None | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@src-tauri/src/discovery.rs` around lines 393 - 419, extract_exe_path
currently uses lower.find(".exe") which can match a ".exe" inside a directory
name; change the logic to locate the ".exe" that is actually the file suffix by
searching from the end (e.g., use rfind) or by scanning for ".exe" occurrences
and selecting one where the character immediately after the match is a path
separator or end-of-string; update the function extract_exe_path to use that
end-aware match (still trim/expand and return same substring text[..idx+4]) so
you only extract valid executable file names.
| if terminal_app == "cmd" || terminal_app == "command-prompt" { | ||
| let mut cmd = Command::new("cmd"); | ||
|
|
||
| if !working_dir.is_empty() { | ||
| cmd.current_dir(&working_dir); | ||
| } | ||
| if command.is_empty() { | ||
| cmd.args(["/C", "start", "cmd"]); | ||
| } else { | ||
| let flag = if keep_open { "/K" } else { "/C" }; | ||
| cmd.args(["/C", "start", "cmd", flag, command]); | ||
| } | ||
|
|
||
| cmd.creation_flags(CREATE_NO_WINDOW) | ||
| .spawn() | ||
| .map_err(|e| format!("Failed to launch terminal: {}", e))?; | ||
| if !working_dir.is_empty() { | ||
| cmd.current_dir(&working_dir); | ||
| } | ||
|
|
||
| cmd.creation_flags(CREATE_NO_WINDOW) | ||
| .spawn() | ||
| .map_err(|e| format!("Failed to launch Command Prompt: {}", e))?; | ||
| } else { | ||
| let mut cmd = Command::new("wt"); | ||
|
|
||
| if !working_dir.is_empty() { | ||
| cmd.args(["-d", &working_dir]); | ||
| } | ||
|
|
||
| if !command.is_empty() { | ||
| let flag = if keep_open { "/K" } else { "/C" }; | ||
| cmd.args(["cmd", flag, command]); | ||
| } | ||
|
|
||
| cmd.creation_flags(CREATE_NO_WINDOW) | ||
| .spawn() | ||
| .map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?; | ||
| } |
There was a problem hiding this comment.
Commands containing spaces may break when passed through cmd /C start.
When command contains spaces (e.g., "npm run dev"), the argument chain ["/C", "start", "cmd", flag, command] on line 113 gets concatenated by cmd.exe into a single command string. The start built-in interprets the first quoted argument as a window title, and unquoted arguments with spaces are split incorrectly.
The same issue applies to the wt branch on line 132.
A safer approach for the CMD branch would be to pass the command through explicitly:
🔧 Suggested fix for the cmd branch
if command.is_empty() {
cmd.args(["/C", "start", "cmd"]);
} else {
let flag = if keep_open { "/K" } else { "/C" };
- cmd.args(["/C", "start", "cmd", flag, command]);
+ let combined = format!("start \"\" cmd {} {}", flag, command);
+ cmd.args(["/C", &combined]);
}The empty "" after start serves as the window title, preventing start from misinterpreting the command.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if terminal_app == "cmd" || terminal_app == "command-prompt" { | |
| let mut cmd = Command::new("cmd"); | |
| if !working_dir.is_empty() { | |
| cmd.current_dir(&working_dir); | |
| } | |
| if command.is_empty() { | |
| cmd.args(["/C", "start", "cmd"]); | |
| } else { | |
| let flag = if keep_open { "/K" } else { "/C" }; | |
| cmd.args(["/C", "start", "cmd", flag, command]); | |
| } | |
| cmd.creation_flags(CREATE_NO_WINDOW) | |
| .spawn() | |
| .map_err(|e| format!("Failed to launch terminal: {}", e))?; | |
| if !working_dir.is_empty() { | |
| cmd.current_dir(&working_dir); | |
| } | |
| cmd.creation_flags(CREATE_NO_WINDOW) | |
| .spawn() | |
| .map_err(|e| format!("Failed to launch Command Prompt: {}", e))?; | |
| } else { | |
| let mut cmd = Command::new("wt"); | |
| if !working_dir.is_empty() { | |
| cmd.args(["-d", &working_dir]); | |
| } | |
| if !command.is_empty() { | |
| let flag = if keep_open { "/K" } else { "/C" }; | |
| cmd.args(["cmd", flag, command]); | |
| } | |
| cmd.creation_flags(CREATE_NO_WINDOW) | |
| .spawn() | |
| .map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?; | |
| } | |
| if terminal_app == "cmd" || terminal_app == "command-prompt" { | |
| let mut cmd = Command::new("cmd"); | |
| if command.is_empty() { | |
| cmd.args(["/C", "start", "cmd"]); | |
| } else { | |
| let flag = if keep_open { "/K" } else { "/C" }; | |
| let combined = format!("start \"\" cmd {} {}", flag, command); | |
| cmd.args(["/C", &combined]); | |
| } | |
| if !working_dir.is_empty() { | |
| cmd.current_dir(&working_dir); | |
| } | |
| cmd.creation_flags(CREATE_NO_WINDOW) | |
| .spawn() | |
| .map_err(|e| format!("Failed to launch Command Prompt: {}", e))?; | |
| } else { | |
| let mut cmd = Command::new("wt"); | |
| if !working_dir.is_empty() { | |
| cmd.args(["-d", &working_dir]); | |
| } | |
| if !command.is_empty() { | |
| let flag = if keep_open { "/K" } else { "/C" }; | |
| cmd.args(["cmd", flag, command]); | |
| } | |
| cmd.creation_flags(CREATE_NO_WINDOW) | |
| .spawn() | |
| .map_err(|e| format!("Failed to launch Windows Terminal: {}", e))?; | |
| } |
🤖 Prompt for AI Agents
In `@src-tauri/src/launcher.rs` around lines 106 - 138, The launch logic breaks
when command contains spaces because cmd.exe start treats the first quoted
argument as a window title; fix this by inserting an explicit empty title
argument and quoting the command when needed: in the branch that constructs
Command::new("cmd") change the args to include the empty title (e.g.,
args(["/C", "start", "", "cmd", flag, "ed_command])) where quoted_command =
format!("\"{}\"", command) when command is non-empty; apply the same quoting
approach in the Command::new("wt") branch when you append ["cmd", flag,
"ed_command] so multi-word commands (like "npm run dev") are passed as a
single argument and not split by the shell (refer to the terminal_app handling
and the cmd/wt arg construction around where flag and command are used).
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@frontend/src/main.js`:
- Line 237: The call to showInfo in frontend/src/main.js uses a typo and an
informal tone ("pannicked"); update the message passed to showInfo (the call
site where showInfo('Cleanup Complete', 'We cleaned up everything while you
pannicked!') is used) to correct the typo and use a neutral, professional string
such as "Cleanup completed successfully" (or similar) for the second argument,
keeping the title "Cleanup Complete" or adjusting it to match casing if desired.
Ensure only the message literal is changed and no other logic around the
showInfo(...) invocation is modified.
In `@src-tauri/src/config.rs`:
- Around line 47-61: The KillWipeSettings struct currently defaults all fields
(including logout) to true; change logout to default to false by replacing its
#[serde(default = "default_true")] with #[serde(default = "default_false")] (and
add a default_false() fn returning false if not already present), and update the
default_kill_wipe() helper to set KillWipeSettings.logout = false so the default
config and programmatic defaults no longer opt into logout by default; keep the
other default_true usages unchanged.
In `@src-tauri/src/kill_wipe.rs`:
- Around line 306-329: The clear_firefox_profiles function currently deletes
places.sqlite (and its -wal/-shm variants), which removes bookmarks as well as
history; update the paths list in clear_firefox_profiles to omit
"places.sqlite", "places.sqlite-wal", and "places.sqlite-shm" so only
cache/cookies/history are removed, or if full-profile wipe is intended add an
explicit warning/confirmation path before calling clear_firefox_profiles to
inform users that bookmarks will be lost; locate the paths vector inside
clear_firefox_profiles and remove or conditionally guard the places.sqlite
entries accordingly.
- Around line 183-200: The loop over targets (a HashSet of image names)
increments killed (the killed counter) per image, so killed is actually a count
of killed image names, not terminated process instances; either rename killed to
killed_image_count and update any UI label that reports "<n> processes killed"
to reflect "images" (or "executables") killed, or, if you need the true process
count, parse the Command::new("taskkill") output (in the Command::output result
for each target) and count the number of "SUCCESS:"/PID lines returned (or parse
stdout/stderr for lines indicating each terminated PID) and add that number to a
new killed_process_count while still recording failures in failures; update
references to killed accordingly (e.g., replace uses of killed with
killed_image_count or killed_process_count as chosen).
- Around line 133-209: kill_user_processes currently uses taskkill /IM by image
name and can inadvertently terminate the Tauri WebView (e.g.,
msedgewebview2.exe) because it isn't protected; update either the
critical_processes() list to include common WebView names (msedgewebview2.exe,
webview2.exe, etc.) or enhance kill_user_processes to detect and exclude child
processes of the current PID (querying parent PID relationships before adding to
targets) so the WebView spawned by the app is not targeted by taskkill;
reference critical_processes(), kill_user_processes(), current_exe, and the
taskkill invocation when making the change.
- Around line 235-273: clear_browser_data currently runs regardless of running
browser processes which causes many file-lock failures when clear_browsers is
true but kill_processes is false; change clear_browser_data to accept a
kill_processes: bool parameter and, when kill_processes is false, probe for
running browser processes (e.g., "chrome.exe", "msedge.exe", "brave.exe",
"firefox.exe") before attempting to clear profiles and push a clear, user-facing
warning into the failures vector (or into the cleared/failures return) for each
detected running browser telling the caller/UI that files may be locked and
recommend enabling kill_processes; keep using clear_chromium_profiles and
clear_firefox_profiles as-is and only add the pre-check + warning when you
detect active processes.
- Around line 449-473: The critical_processes() list currently omits the Windows
shell; update the array inside critical_processes() to include "explorer.exe"
(and optionally "taskmgr.exe", "conhost.exe", "rundll32.exe" if you want to
protect additional user-shell utilities) so the function returns a set that
prevents killing the desktop shell; if the behavior intentionally expects a
logout after kill_processes, instead add a clear comment or documentation near
the critical_processes() or the kill_processes/logout flow noting that explorer
will be terminated intentionally.
In `@src-tauri/src/lib.rs`:
- Around line 82-93: The on_window_event closure has mixed indentation; reformat
the entire closure body so all statements inside the closure use a consistent
indentation level (align the if let tauri::WindowEvent::CloseRequested block,
the cfg = config::load_config() call, the cfg.settings.minimize_to_tray branch,
api.prevent_close(), let _ = window.hide(), and
lifecycle::close_apps_on_exit(&window.app_handle())) and ensure matching brace
alignment for the closure and its inner if so the code is uniformly indented and
readable.
🧹 Nitpick comments (8)
frontend/src/styles.css (2)
38-104: Light and auto theme variables are fully duplicated.Lines 38–58 (
:root[data-theme="light"]) and lines 60–80 (:root[data-theme="auto"]) are identical. The auto theme only diverges inside theprefers-color-scheme: darkmedia query. Consider sharing a common selector to avoid maintaining two identical copies.♻️ Suggested approach
-:root[data-theme="light"] { - --bg-primary: `#f5f7fb`; - /* ... all light variables ... */ -} - -:root[data-theme="auto"] { - --bg-primary: `#f5f7fb`; - /* ... same light variables repeated ... */ -} +:root[data-theme="light"], +:root[data-theme="auto"] { + --bg-primary: `#f5f7fb`; + /* ... light variables once ... */ +}
786-794: Warning box uses hardcoded color instead of theme variable.The
rgba(220, 38, 38, ...)values won't adapt to themes where--dangerdiffers (e.g., frosted theme uses#f87171). Minor inconsistency — acceptable if the red warning aesthetic is intentional across all themes.frontend/src/main.js (1)
242-252:handleKillAndWipecalled fromsetTimeout— result is fire-and-forget.The
asyncfunctionhandleKillAndWipeis called insidesetTimeoutwithout awaiting. Any unhandled rejection from the Kill & Wipe flow will not propagate tohandleStartupFlags. Theconsole.errorinsiderunKillAndWipecovers the main failure path, so this is likely fine in practice, but worth noting.src-tauri/src/commands.rs (1)
402-419: Save error silently ignored before logout.Line 414 discards the
save_configresult withlet _. If the save fails, thepost_logout_message_pendingflag won't be persisted, and the post-logout message won't appear after re-login. Consider at least logging the error since this is the last chance before logout.♻️ Suggested fix
- let _ = config::save_config(&cfg); + if let Err(e) = config::save_config(&cfg) { + eprintln!("Failed to save config before logout: {}", e); + }src-tauri/src/kill_wipe.rs (4)
90-108:argsis not passed throughescape_ps_string, unlike all other interpolated values.Currently safe because
argsis a hardcoded string literal without special characters. However, ifargsever includes user-controlled or dynamic content in the future, this becomes a PowerShell injection vector. Consider escaping it for consistency and defensive coding.Suggested fix
$Shortcut.TargetPath = '{}'; \ - $Shortcut.Arguments = '{}'; \ + $Shortcut.Arguments = '{}'; \ $Shortcut.WorkingDirectory = '{}'; \escape_ps_string(&exe_str), - args, + escape_ps_string(args), escape_ps_string(
211-233:TEMPandTMPtypically resolve to the same directory on Windows.Both environment variables usually point to the same path (e.g.,
C:\Users\<user>\AppData\Local\Temp). This results inclear_directory_contentsbeing called twice on the same directory—the second call will encounter mostly already-deleted entries and produce spurious failure messages for items deleted in the first pass. Consider deduplicating:Suggested fix
let mut paths = Vec::new(); if let Ok(temp) = std::env::var("TEMP") { paths.push(PathBuf::from(temp)); } if let Ok(tmp) = std::env::var("TMP") { - paths.push(PathBuf::from(tmp)); + let tmp = PathBuf::from(tmp); + if !paths.contains(&tmp) { + paths.push(tmp); + } } paths.push(PathBuf::from(r"C:\Windows\Temp"));
373-394: Error details are collected but discarded — only the count is reported.
clear_directory_contentsgathers per-file errors into aHashMapbut theErrmessage only includes the count ("Failed to delete N items"). The caller inclear_temp_folders(line 228) then formats this as"<path>: Failed to delete N items", losing all specifics. Consider either propagating the individual errors or simplifying to a counter since theHashMapentries are never read.Suggested simplification
fn clear_directory_contents(path: &Path) -> Result<(), String> { let entries = std::fs::read_dir(path).map_err(|e| e.to_string())?; - let mut errors: HashMap<String, String> = HashMap::new(); + let mut error_count = 0usize; for entry in entries.flatten() { let p = entry.path(); let result = if p.is_dir() { std::fs::remove_dir_all(&p).map_err(|e| e.to_string()) } else { std::fs::remove_file(&p).map_err(|e| e.to_string()) }; - if let Err(e) = result { - errors.insert(p.to_string_lossy().to_string(), e); + if result.is_err() { + error_count += 1; } } - if errors.is_empty() { + if error_count == 0 { Ok(()) } else { - Err(format!("Failed to delete {} items", errors.len())) + Err(format!("Failed to delete {} items", error_count)) } }
410-429: CSV parser doesn't handle RFC 4180 escaped quotes ("") but is adequate fortasklistoutput.In standard CSV, a literal
"inside a quoted field is escaped as"". This parser would misinterpret that by toggling thein_quotesflag twice. Since Windowstasklistoutput won't contain quotes in process names or usernames, this works in practice. A brief comment noting the limited scope would help future maintainers.
| const config = getConfig(); | ||
| if (!config?.settings?.post_logout_message_pending) return; | ||
|
|
||
| await showInfo('Cleanup Complete', 'We cleaned up everything while you pannicked!'); |
There was a problem hiding this comment.
Typo: "pannicked" → "panicked".
- await showInfo('Cleanup Complete', 'We cleaned up everything while you pannicked!');
+ await showInfo('Cleanup Complete', 'We cleaned up everything while you were away!');The tone could also be reconsidered — "pannicked" (even corrected) may not suit all users. A neutral message like "Cleanup completed successfully" might be more professional.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await showInfo('Cleanup Complete', 'We cleaned up everything while you pannicked!'); | |
| await showInfo('Cleanup Complete', 'We cleaned up everything while you were away!'); |
🤖 Prompt for AI Agents
In `@frontend/src/main.js` at line 237, The call to showInfo in
frontend/src/main.js uses a typo and an informal tone ("pannicked"); update the
message passed to showInfo (the call site where showInfo('Cleanup Complete', 'We
cleaned up everything while you pannicked!') is used) to correct the typo and
use a neutral, professional string such as "Cleanup completed successfully" (or
similar) for the second argument, keeping the title "Cleanup Complete" or
adjusting it to match casing if desired. Ensure only the message literal is
changed and no other logic around the showInfo(...) invocation is modified.
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct KillWipeSettings { | ||
| #[serde(default = "default_true")] | ||
| pub confirm_before: bool, | ||
| #[serde(default = "default_true")] | ||
| pub kill_processes: bool, | ||
| #[serde(default = "default_true")] | ||
| pub clear_temp: bool, | ||
| #[serde(default = "default_true")] | ||
| pub clear_browsers: bool, | ||
| #[serde(default = "default_true")] | ||
| pub flush_dns: bool, | ||
| #[serde(default = "default_true")] | ||
| pub logout: bool, | ||
| } |
There was a problem hiding this comment.
logout defaults to true — risky destructive default.
All KillWipeSettings fields default to true, including logout. This means if a user's config file is missing the kill_wipe section (e.g., upgrading from an older version), the first Kill & Wipe run will include logging out by default. Consider defaulting logout to false to require explicit opt-in for this destructive action.
🛡️ Suggested fix
pub struct KillWipeSettings {
#[serde(default = "default_true")]
pub confirm_before: bool,
#[serde(default = "default_true")]
pub kill_processes: bool,
#[serde(default = "default_true")]
pub clear_temp: bool,
#[serde(default = "default_true")]
pub clear_browsers: bool,
#[serde(default = "default_true")]
pub flush_dns: bool,
- #[serde(default = "default_true")]
+ #[serde(default)]
pub logout: bool,
}Also update default_kill_wipe():
fn default_kill_wipe() -> KillWipeSettings {
KillWipeSettings {
confirm_before: true,
kill_processes: true,
clear_temp: true,
clear_browsers: true,
flush_dns: true,
- logout: true,
+ logout: false,
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[derive(Debug, Clone, Serialize, Deserialize)] | |
| pub struct KillWipeSettings { | |
| #[serde(default = "default_true")] | |
| pub confirm_before: bool, | |
| #[serde(default = "default_true")] | |
| pub kill_processes: bool, | |
| #[serde(default = "default_true")] | |
| pub clear_temp: bool, | |
| #[serde(default = "default_true")] | |
| pub clear_browsers: bool, | |
| #[serde(default = "default_true")] | |
| pub flush_dns: bool, | |
| #[serde(default = "default_true")] | |
| pub logout: bool, | |
| } | |
| #[derive(Debug, Clone, Serialize, Deserialize)] | |
| pub struct KillWipeSettings { | |
| #[serde(default = "default_true")] | |
| pub confirm_before: bool, | |
| #[serde(default = "default_true")] | |
| pub kill_processes: bool, | |
| #[serde(default = "default_true")] | |
| pub clear_temp: bool, | |
| #[serde(default = "default_true")] | |
| pub clear_browsers: bool, | |
| #[serde(default = "default_true")] | |
| pub flush_dns: bool, | |
| #[serde(default)] | |
| pub logout: bool, | |
| } |
🤖 Prompt for AI Agents
In `@src-tauri/src/config.rs` around lines 47 - 61, The KillWipeSettings struct
currently defaults all fields (including logout) to true; change logout to
default to false by replacing its #[serde(default = "default_true")] with
#[serde(default = "default_false")] (and add a default_false() fn returning
false if not already present), and update the default_kill_wipe() helper to set
KillWipeSettings.logout = false so the default config and programmatic defaults
no longer opt into logout by default; keep the other default_true usages
unchanged.
| fn kill_user_processes() -> (usize, Vec<String>) { | ||
| #[cfg(target_os = "windows")] | ||
| { | ||
| let current_exe = std::env::current_exe() | ||
| .ok() | ||
| .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) | ||
| .unwrap_or_default() | ||
| .to_lowercase(); | ||
| let current_user = std::env::var("USERNAME").unwrap_or_default().to_lowercase(); | ||
|
|
||
| let critical = critical_processes(); | ||
|
|
||
| let output = Command::new("tasklist") | ||
| .args(["/V", "/FO", "CSV", "/NH"]) | ||
| .creation_flags(CREATE_NO_WINDOW) | ||
| .output(); | ||
|
|
||
| let mut killed = 0usize; | ||
| let mut failures = Vec::new(); | ||
|
|
||
| if let Ok(output) = output { | ||
| let stdout = String::from_utf8_lossy(&output.stdout); | ||
| let mut targets = HashSet::new(); | ||
|
|
||
| for line in stdout.lines() { | ||
| let fields = parse_csv_line(line); | ||
| if fields.len() < 7 { | ||
| continue; | ||
| } | ||
| let image = fields[0].trim().to_lowercase(); | ||
| let user = fields[6].trim().to_lowercase(); | ||
|
|
||
| if image.is_empty() { | ||
| continue; | ||
| } | ||
| if image == current_exe { | ||
| continue; | ||
| } | ||
| if critical.contains(&image) { | ||
| continue; | ||
| } | ||
| if is_system_user(&user) { | ||
| continue; | ||
| } | ||
| if !is_current_user(&user, ¤t_user) { | ||
| continue; | ||
| } | ||
| targets.insert(image); | ||
| } | ||
|
|
||
| for name in targets { | ||
| let output = Command::new("taskkill") | ||
| .args(["/F", "/IM", &name]) | ||
| .creation_flags(CREATE_NO_WINDOW) | ||
| .output(); | ||
|
|
||
| match output { | ||
| Ok(out) => { | ||
| if out.status.success() { | ||
| killed += 1; | ||
| } else { | ||
| let stderr = String::from_utf8_lossy(&out.stderr); | ||
| failures.push(format!("{}: {}", name, stderr.trim())); | ||
| } | ||
| } | ||
| Err(e) => failures.push(format!("{}: {}", name, e)), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return (killed, failures); | ||
| } | ||
| #[cfg(not(target_os = "windows"))] | ||
| { | ||
| (0, vec![]) | ||
| } | ||
| } |
There was a problem hiding this comment.
taskkill /F /IM is a blunt instrument — kills all instances by image name, and the Tauri WebView process is not protected.
The current exe is excluded (line 168), but Tauri apps typically spawn a separate WebView process (e.g., msedgewebview2.exe or similar). This WebView process won't match current_exe and is not in the critical list, so it will be killed — crashing the application's UI mid-operation.
Consider adding the Tauri WebView process name to the critical list or detecting it dynamically (e.g., by matching child processes of the current PID).
#!/bin/bash
# Check if there's any reference to webview process protection elsewhere in the codebase
rg -n -i 'webview\|msedge' --type rust -C3🤖 Prompt for AI Agents
In `@src-tauri/src/kill_wipe.rs` around lines 133 - 209, kill_user_processes
currently uses taskkill /IM by image name and can inadvertently terminate the
Tauri WebView (e.g., msedgewebview2.exe) because it isn't protected; update
either the critical_processes() list to include common WebView names
(msedgewebview2.exe, webview2.exe, etc.) or enhance kill_user_processes to
detect and exclude child processes of the current PID (querying parent PID
relationships before adding to targets) so the WebView spawned by the app is not
targeted by taskkill; reference critical_processes(), kill_user_processes(),
current_exe, and the taskkill invocation when making the change.
| for name in targets { | ||
| let output = Command::new("taskkill") | ||
| .args(["/F", "/IM", &name]) | ||
| .creation_flags(CREATE_NO_WINDOW) | ||
| .output(); | ||
|
|
||
| match output { | ||
| Ok(out) => { | ||
| if out.status.success() { | ||
| killed += 1; | ||
| } else { | ||
| let stderr = String::from_utf8_lossy(&out.stderr); | ||
| failures.push(format!("{}: {}", name, stderr.trim())); | ||
| } | ||
| } | ||
| Err(e) => failures.push(format!("{}: {}", name, e)), | ||
| } | ||
| } |
There was a problem hiding this comment.
killed_count counts unique image names, not actual process instances.
taskkill /F /IM terminates all instances of a given executable. Since targets is a HashSet of image names, killed_count reflects the number of distinct executables killed, not the total number of processes terminated. If the UI displays this as "X processes killed", it will undercount. Consider renaming to killed_image_count or parsing taskkill output for actual counts.
🤖 Prompt for AI Agents
In `@src-tauri/src/kill_wipe.rs` around lines 183 - 200, The loop over targets (a
HashSet of image names) increments killed (the killed counter) per image, so
killed is actually a count of killed image names, not terminated process
instances; either rename killed to killed_image_count and update any UI label
that reports "<n> processes killed" to reflect "images" (or "executables")
killed, or, if you need the true process count, parse the
Command::new("taskkill") output (in the Command::output result for each target)
and count the number of "SUCCESS:"/PID lines returned (or parse stdout/stderr
for lines indicating each terminated PID) and add that number to a new
killed_process_count while still recording failures in failures; update
references to killed accordingly (e.g., replace uses of killed with
killed_image_count or killed_process_count as chosen).
| fn clear_browser_data() -> (Vec<String>, Vec<String>) { | ||
| let mut cleared = Vec::new(); | ||
| let mut failures = Vec::new(); | ||
|
|
||
| let local_app = std::env::var("LOCALAPPDATA").ok(); | ||
| let roam_app = std::env::var("APPDATA").ok(); | ||
|
|
||
| if let Some(local) = local_app { | ||
| let local = PathBuf::from(local); | ||
| let chromium = vec![ | ||
| ("Chrome", local.join(r"Google\Chrome\User Data")), | ||
| ("Edge", local.join(r"Microsoft\Edge\User Data")), | ||
| ("Brave", local.join(r"BraveSoftware\Brave-Browser\User Data")), | ||
| ]; | ||
|
|
||
| for (name, base) in chromium { | ||
| if base.exists() { | ||
| let (ok, err) = clear_chromium_profiles(&base); | ||
| if ok { | ||
| cleared.push(name.to_string()); | ||
| } | ||
| failures.extend(err.into_iter().map(|e| format!("{}: {}", name, e))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if let Some(roam) = roam_app { | ||
| let ff_base = PathBuf::from(roam).join(r"Mozilla\Firefox\Profiles"); | ||
| if ff_base.exists() { | ||
| let (ok, err) = clear_firefox_profiles(&ff_base); | ||
| if ok { | ||
| cleared.push("Firefox".to_string()); | ||
| } | ||
| failures.extend(err.into_iter().map(|e| format!("Firefox: {}", e))); | ||
| } | ||
| } | ||
|
|
||
| (cleared, failures) | ||
| } |
There was a problem hiding this comment.
Browser data clearing will largely fail if browsers are still running.
If clear_browsers is true but kill_processes is false, browser profile files (SQLite databases, cache directories) will be locked by running browser processes, producing many failures. This is handled gracefully (errors are collected), but it may confuse users. Consider either documenting this dependency or warning the user in the UI when clear_browsers is enabled without kill_processes.
🤖 Prompt for AI Agents
In `@src-tauri/src/kill_wipe.rs` around lines 235 - 273, clear_browser_data
currently runs regardless of running browser processes which causes many
file-lock failures when clear_browsers is true but kill_processes is false;
change clear_browser_data to accept a kill_processes: bool parameter and, when
kill_processes is false, probe for running browser processes (e.g.,
"chrome.exe", "msedge.exe", "brave.exe", "firefox.exe") before attempting to
clear profiles and push a clear, user-facing warning into the failures vector
(or into the cleared/failures return) for each detected running browser telling
the caller/UI that files may be locked and recommend enabling kill_processes;
keep using clear_chromium_profiles and clear_firefox_profiles as-is and only add
the pre-check + warning when you detect active processes.
| fn clear_firefox_profiles(base: &Path) -> (bool, Vec<String>) { | ||
| let mut errors = Vec::new(); | ||
| let profiles = list_all_dirs(base); | ||
|
|
||
| for profile in &profiles { | ||
| let paths = vec![ | ||
| profile.join("cache2"), | ||
| profile.join("cookies.sqlite"), | ||
| profile.join("cookies.sqlite-wal"), | ||
| profile.join("cookies.sqlite-shm"), | ||
| profile.join("places.sqlite"), | ||
| profile.join("places.sqlite-wal"), | ||
| profile.join("places.sqlite-shm"), | ||
| ]; | ||
|
|
||
| for p in paths { | ||
| if let Err(e) = remove_path(&p) { | ||
| errors.push(format!("{}: {}", p.to_string_lossy(), e)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| (!profiles.is_empty(), errors) | ||
| } |
There was a problem hiding this comment.
Deleting places.sqlite destroys Firefox bookmarks, not just history.
Firefox stores both browsing history and bookmarks in places.sqlite. Removing this file permanently deletes the user's bookmarks, which is significantly more destructive than a typical "clear browsing data" operation. If the intent is to clear only cache/cookies/history, remove places.sqlite (and its WAL/SHM) from the list. If full wipe is intentional, consider warning the user that bookmarks will be lost.
🤖 Prompt for AI Agents
In `@src-tauri/src/kill_wipe.rs` around lines 306 - 329, The
clear_firefox_profiles function currently deletes places.sqlite (and its
-wal/-shm variants), which removes bookmarks as well as history; update the
paths list in clear_firefox_profiles to omit "places.sqlite",
"places.sqlite-wal", and "places.sqlite-shm" so only cache/cookies/history are
removed, or if full-profile wipe is intended add an explicit
warning/confirmation path before calling clear_firefox_profiles to inform users
that bookmarks will be lost; locate the paths vector inside
clear_firefox_profiles and remove or conditionally guard the places.sqlite
entries accordingly.
| fn critical_processes() -> HashSet<String> { | ||
| let mut set = HashSet::new(); | ||
| for name in [ | ||
| "system", | ||
| "system idle process", | ||
| "smss.exe", | ||
| "csrss.exe", | ||
| "wininit.exe", | ||
| "winlogon.exe", | ||
| "services.exe", | ||
| "lsass.exe", | ||
| "lsm.exe", | ||
| "svchost.exe", | ||
| "fontdrvhost.exe", | ||
| "dwm.exe", | ||
| "registry", | ||
| "memcompression", | ||
| "securityhealthservice.exe", | ||
| "sihost.exe", | ||
| "ctfmon.exe", | ||
| ] { | ||
| set.insert(name.to_string()); | ||
| } | ||
| set | ||
| } |
There was a problem hiding this comment.
explorer.exe is absent from the critical processes list.
If the user selects kill_processes without logout, killing explorer.exe removes the desktop shell (taskbar, Start menu, file manager). The user would be left with a non-functional desktop. If this is intentional (because logout always follows), it should be documented. If not, add explorer.exe to the critical list.
Additionally, consider whether other essential user-shell processes like taskmgr.exe, conhost.exe, or rundll32.exe should be protected.
Suggested addition
"ctfmon.exe",
+ "explorer.exe",
+ "taskmgr.exe",
+ "conhost.exe",
+ "rundll32.exe",
+ "dllhost.exe",
+ "searchhost.exe",
+ "startmenuexperiencehost.exe",
+ "shellexperiencehost.exe",
+ "runtimebroker.exe",
+ "textinputhost.exe",
] {🤖 Prompt for AI Agents
In `@src-tauri/src/kill_wipe.rs` around lines 449 - 473, The critical_processes()
list currently omits the Windows shell; update the array inside
critical_processes() to include "explorer.exe" (and optionally "taskmgr.exe",
"conhost.exe", "rundll32.exe" if you want to protect additional user-shell
utilities) so the function returns a set that prevents killing the desktop
shell; if the behavior intentionally expects a logout after kill_processes,
instead add a clear comment or documentation near the critical_processes() or
the kill_processes/logout flow noting that explorer will be terminated
intentionally.
| .on_window_event(|window, event| { | ||
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | ||
| // Check minimize_to_tray setting | ||
| let cfg = config::load_config(); | ||
| if cfg.settings.minimize_to_tray { | ||
| api.prevent_close(); | ||
| let _ = window.hide(); | ||
| } | ||
| let cfg = config::load_config(); | ||
| if cfg.settings.minimize_to_tray { | ||
| api.prevent_close(); | ||
| let _ = window.hide(); | ||
| return; | ||
| } | ||
| }) | ||
| lifecycle::close_apps_on_exit(&window.app_handle()); | ||
| } | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Inconsistent indentation in on_window_event closure.
The handler body mixes indentation levels — some lines are indented with 12 spaces and others with 8. This appears to be a formatting issue.
♻️ Suggested fix
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
- // Check minimize_to_tray setting
- let cfg = config::load_config();
- if cfg.settings.minimize_to_tray {
- api.prevent_close();
- let _ = window.hide();
- return;
+ let cfg = config::load_config();
+ if cfg.settings.minimize_to_tray {
+ api.prevent_close();
+ let _ = window.hide();
+ return;
+ }
+ lifecycle::close_apps_on_exit(&window.app_handle());
}
- lifecycle::close_apps_on_exit(&window.app_handle());
- }
- })
+ })📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .on_window_event(|window, event| { | |
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | |
| // Check minimize_to_tray setting | |
| let cfg = config::load_config(); | |
| if cfg.settings.minimize_to_tray { | |
| api.prevent_close(); | |
| let _ = window.hide(); | |
| } | |
| let cfg = config::load_config(); | |
| if cfg.settings.minimize_to_tray { | |
| api.prevent_close(); | |
| let _ = window.hide(); | |
| return; | |
| } | |
| }) | |
| lifecycle::close_apps_on_exit(&window.app_handle()); | |
| } | |
| }) | |
| .on_window_event(|window, event| { | |
| if let tauri::WindowEvent::CloseRequested { api, .. } = event { | |
| let cfg = config::load_config(); | |
| if cfg.settings.minimize_to_tray { | |
| api.prevent_close(); | |
| let _ = window.hide(); | |
| return; | |
| } | |
| lifecycle::close_apps_on_exit(&window.app_handle()); | |
| } | |
| }) |
🤖 Prompt for AI Agents
In `@src-tauri/src/lib.rs` around lines 82 - 93, The on_window_event closure has
mixed indentation; reformat the entire closure body so all statements inside the
closure use a consistent indentation level (align the if let
tauri::WindowEvent::CloseRequested block, the cfg = config::load_config() call,
the cfg.settings.minimize_to_tray branch, api.prevent_close(), let _ =
window.hide(), and lifecycle::close_apps_on_exit(&window.app_handle())) and
ensure matching brace alignment for the closure and its inner if so the code is
uniformly indented and readable.
Changelog update and feature additions.
Summary by CodeRabbit
New Features
Improvements
Documentation