Swadhin Biswas Ci/improve release notes (#41) · 8/28/2026
04d550f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tauri::{AppHandle, Emitter};
const POLL_INTERVAL_SECS: u64 = 5;
const STABLE_POLLS_REQUIRED: u8 = 2;
#[derive(Clone, Debug, Default)]
struct TrackedFile {
last_size: u64,
stable_polls: u8,
notified: bool,
}
fn is_appimage_path(path: &Path) -> bool {
matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("AppImage") | Some("appimage")
)
}
fn update_tracking_state(state: &mut TrackedFile, current_size: u64) {
if state.last_size == current_size {
state.stable_polls = state.stable_polls.saturating_add(1);
} else {
state.last_size = current_size;
state.stable_polls = 0;
}
}
pub fn start_watcher(app_handle: AppHandle) {
std::thread::spawn(move || {
let Some(home_dir) = dirs::home_dir() else {
return;
};
let downloads_dir = home_dir.join("Downloads");
if !downloads_dir.exists() {
return;
}
let mut tracked_files: HashMap<PathBuf, TrackedFile> = HashMap::new();
loop {
std::thread::sleep(Duration::from_secs(POLL_INTERVAL_SECS));
let mut seen_paths = HashSet::new();
if let Ok(entries) = fs::read_dir(&downloads_dir) {
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_file() {
continue;
}
let path = entry.path();
if !is_appimage_path(&path) {
continue;
}
let Ok(metadata) = entry.metadata() else {
continue;
};
let current_size = metadata.len();
if current_size == 0 {
continue;
}
seen_paths.insert(path.clone());
let state = tracked_files.entry(path.clone()).or_default();
update_tracking_state(state, current_size);
if !state.notified && state.stable_polls >= STABLE_POLLS_REQUIRED {
let _ = app_handle
.emit("appimage-detected", path.to_string_lossy().to_string());
state.notified = true;
}
}
}
tracked_files.retain(|path, _| seen_paths.contains(path));
}
});
}
#[cfg(test)]
mod tests {
use super::{update_tracking_state, TrackedFile};
#[test]
fn marks_file_stable_only_after_repeated_same_size() {
let mut state = TrackedFile::default();
update_tracking_state(&mut state, 128);
assert_eq!(state.stable_polls, 0);
update_tracking_state(&mut state, 128);
assert_eq!(state.stable_polls, 1);
update_tracking_state(&mut state, 128);
assert_eq!(state.stable_polls, 2);
}
#[test]
fn resets_stability_when_size_changes() {
let mut state = TrackedFile::default();
update_tracking_state(&mut state, 128);
update_tracking_state(&mut state, 128);
update_tracking_state(&mut state, 256);
assert_eq!(state.last_size, 256);
assert_eq!(state.stable_polls, 0);
}
}