/ linuxy / src-tauri / src / watcher.rs
Swadhin Biswas Ci/improve release notes (#41) · 8/28/2026
04d550f
116 lines · 3.4 KB
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);
    }
}