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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cargo Check
Expand All @@ -24,7 +24,7 @@ jobs:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Run Tests
Expand All @@ -34,7 +34,7 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
Expand All @@ -46,7 +46,7 @@ jobs:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
Expand Down
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ license = "MIT"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
crossterm = "0.27"
dirs = "5.0"
crossterm = "0.29"
dirs = "6.0"
glob = "0.3"
ratatui = "0.29"
ratatui = "0.30"
rayon = "1.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
walkdir = "2.4"
trash = "5"

[dev-dependencies]
tempfile = "3.10"
Binary file added assets/everykill_logo/everykill.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/everykill_logo/everykill.psd
Binary file not shown.
Binary file added assets/everykill_logo/everykill_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/everykill_logo/variants/everykill_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
220 changes: 2 additions & 218 deletions docs/PROBLEMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,237 +7,21 @@
## Table of Contents

1. [Executive Summary](#executive-summary)
2. [High-Priority Issues (Fix Before v1.0)](#high-priority-issues-fix-before-v10)
3. [Medium-Priority Issues (Follow-Up PRs)](#medium-priority-issues-follow-up-prs)
4. [Low-Priority Issues (Optional)](#low-priority-issues-optional)
5. [Epic Issues / Feature Gaps](#epic-issues--feature-gaps)
2. [Low-Priority Issues (Optional)](#low-priority-issues-optional)
3. [Epic Issues / Feature Gaps](#epic-issues--feature-gaps)

---

## Executive Summary

The everykill project demonstrates **excellent software engineering practices** with a well-architected Rust codebase, comprehensive testing (47 unit tests), and smart algorithms. All **critical issues** have been resolved for the v1.0 release.

Additionally, **3 high-priority issues** affect error handling and UX, and **11 medium-priority issues** need attention for robustness.

Beyond code quality issues, there are **6 major feature gaps** from PROBLEMS.md that need to be addressed for a complete v1.0 release (CI/CD pipelines, binary building, ecosystem markers, marketing).

**With ~2 hours of focused work** on critical/high items + **addressing the epic features**, this project will be ready for v1.0.

---

## Medium-Priority Issues (Follow-Up PRs)

### 🟡 MEDIUM #6: Integer Overflow in Size Calculation

**Status:** ❌ UNSOLVED
**Location:** `src/scanner/size.rs`
**Severity:** MEDIUM
**Effort:** 10 minutes

**Problem:** Size accumulation uses `u64` (max ~18.4 EB) without overflow checking.

**Risk:** Very low in practice (modern disks ~100TB), but theoretically unsound.

**Fix:** Use `checked_add()` with error handling.

---

### 🟡 MEDIUM #7: Hardcoded SKIP_DIRS Not Configurable

**Status:** ❌ UNSOLVED
**Location:** `src/scanner/dir.rs:5`
**Severity:** MEDIUM
**Effort:** 15 minutes

**Problem:**
```rust
const SKIP_DIRS: &[&str] = &[".git", ".svn", ".hg", "node_modules/.cache", ".cache"];
```

Can't override `.git`, `.svn`, etc. even with `--exclude` flag. What if user wants to scan inside version control dirs for a size audit?

**Fix:** Add `--no-skip-hidden` flag or merge with `--exclude` logic.

---

### 🟡 MEDIUM #8: Background Thread Termination Is Silent

**Status:** ❌ UNSOLVED
**Location:** `src/ui/tui.rs:140-144`
**Severity:** MEDIUM
**Effort:** 5 minutes

**Problem:** Thread exits with no logging if receiver drops.

**Impact:** Hard to debug if thread exits unexpectedly.

**Fix:** Add `eprintln!()` to log reason.

---

### 🟡 MEDIUM #9: Unused Parameter in Render Function

**Status:** ❌ UNSOLVED
**Location:** `src/ui/tui.rs:251`
**Severity:** MEDIUM
**Effort:** 5 minutes

**Problem:** `_viewport_height` parameter is accepted but never used.

**Fix:** Remove if unused, or implement if future optimization planned.

---

### 🟡 MEDIUM #10: Mouse Click Bounds Checking Incomplete

**Status:** ❌ UNSOLVED
**Location:** `src/ui/tui.rs:433-434`
**Severity:** MEDIUM
**Effort:** 15 minutes

**Problem:**
```rust
let visible_row = list_row + state.scroll_offset; // ← No bounds check before indexing
```

**Risk:** Index out of bounds panic if clicking below visible list.

**Fix:** Add bounds check before using `visible_row`.

---

### 🟡 MEDIUM #11: Duplicate Ecosystem Filtering Logic

**Status:** ❌ UNSOLVED
**Location:** `src/args.rs:94-110` + `src/ui/tui.rs:115-127`
**Severity:** MEDIUM
**Effort:** 20 minutes

**Problem:** Same filtering logic in two places.

**Impact:** Changes must be mirrored; risk of divergence.

**Fix:** Extract to shared function or call `args.get_ecosystems()` instead of reimplementing.

*(Also mentioned in CRITICAL #2)*

---

### 🟡 MEDIUM #12: No Validation of Ecosystem Configuration Files

**Status:** ❌ UNSOLVED
**Location:** `src/config/ecosystem.rs:95-107`
**Severity:** MEDIUM
**Effort:** 15 minutes

**Problem:**

If all ecosystem JSON files are corrupt, app runs with 0 ecosystems (no error to user).

```rust
pub fn load_ecosystems() -> anyhow::Result<Vec<Ecosystem>> {
let mut ecosystems = Vec::new();
for entry in glob::glob("ecosystems/*.json")? {
match entry {
Ok(path) => match load_ecosystem_from_path(&path) {
Ok(eco) => ecosystems.push(eco),
Err(e) => eprintln!("Warning: failed to load {:?}: {}", path, e), // ← Just warns!
},
}
}
Ok(ecosystems) // ← Could be empty!
}
```

**Fix:** Return error if no valid ecosystems loaded:
```rust
if ecosystems.is_empty() {
return Err(anyhow!("No valid ecosystems found in ecosystems/"));
}
```

---

### 🟡 MEDIUM #13: Size Loading State Is Unclear

**Status:** ❌ UNSOLVED
**Location:** `src/config/ecosystem.rs:84`
**Severity:** MEDIUM
**Effort:** 30 minutes

**Problem:** Folders show "…" during loading with no indication of progress.

**Impact:** Users don't know if app is frozen or just calculating.

**Fix:** Add `SizeState` enum instead of just `u64`:
```rust
pub enum SizeBytes {
Unknown,
Calculating,
Known(u64),
}
```

---

### 🟡 MEDIUM #14: No Recovery From Partial Deletion

**Status:** ❌ UNSOLVED
**Location:** `src/deleter.rs:24-65`
**Severity:** MEDIUM
**Effort:** 60 minutes

**Problem:**

If deletion fails midway (e.g., permission denied on subdir), folder is partially deleted but app reports success.

**Impact:** Folder partially deleted; next run might not find it; user doesn't know cleanup is incomplete.

**Fix:** Use trash crate or atomic moves instead of `remove_dir_all()`.

---

### 🟡 MEDIUM #15: Wrong Default for Confidence Enum

**Status:** ❌ UNSOLVED
**Location:** `src/config/ecosystem.rs:8`
**Severity:** MEDIUM
**Effort:** 5 minutes

**Problem:**
```rust
#[derive(Default)]
pub enum Confidence {
#[default]
Certain, // ← But should be Undetected!
...
}
```

When `DiscoveredFolder::new()` is called directly (in tests), confidence defaults to Certain. But folders are only "Certain" if they matched unambiguously.

**Fix:** Change default to `Undetected`.

---

### 🟡 MEDIUM #16: Unclear Default Ecosystem Behavior

**Status:** ❌ UNSOLVED
**Location:** `src/args.rs:107-109`
**Severity:** MEDIUM
**Effort:** 15 minutes

**Problem:**

Documentation claims "use local patterns when no --target", but code returns ALL ecosystems when neither `--all` nor `--target` specified.

**Impact:** Ambiguous intended behavior; hard to maintain.

**Fix:** Clarify docs or change implementation to match intended design.

---

## Low-Priority Issues (Optional)

### 🟢 LOW #17-25: Nine Low-Priority Improvements
Expand Down
3 changes: 2 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ impl Args {
}
}

/// Get ecosystems to scan
/// Get ecosystems to scan.
/// Returns all ecosystems when neither `--all` nor `--target` is specified.
pub fn get_ecosystems(&self, all_ecosystems: &[Ecosystem]) -> Vec<Ecosystem> {
if self.all {
all_ecosystems.to_vec()
Expand Down
5 changes: 4 additions & 1 deletion src/config/ecosystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ use std::path::{Path, PathBuf};
/// How certain we are that a discovered folder belongs to an ecosystem.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Confidence {
#[default]
Certain,
Confirmed,
Ambiguous,
#[default]
Undetected,
}

Expand Down Expand Up @@ -103,6 +103,9 @@ pub fn load_ecosystems() -> anyhow::Result<Vec<Ecosystem>> {
Err(e) => eprintln!("Warning: glob error: {}", e),
}
}
if ecosystems.is_empty() {
return Err(anyhow::anyhow!("No valid ecosystems found in ecosystems/"));
}
Ok(ecosystems)
}

Expand Down
2 changes: 1 addition & 1 deletion src/deleter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub fn delete_folders(folders: &[DiscoveredFolder], dry_run: bool) -> DeleteSumm
summary.deleted_count += 1;
summary.freed_bytes += folder.size_bytes;
} else {
match std::fs::remove_dir_all(&folder.path) {
match trash::delete(&folder.path) {
Ok(_) => {
println!(
"Deleted: {} ({} - {})",
Expand Down
27 changes: 13 additions & 14 deletions src/scanner/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,16 @@ pub fn scan_directory(
walker = walker.max_depth(depth);
}

let mut all_exclude_dirs: Vec<String> = exclude_dirs.to_vec();
for &skip in SKIP_DIRS {
if !all_exclude_dirs.contains(&skip.to_string()) {
all_exclude_dirs.push(skip.to_string());
}
}

for entry in walker
.into_iter()
.filter_entry(|e| !should_skip_entry(e, exclude_dirs, exclude_hidden))
.filter_entry(|e| !should_skip_entry(e, &all_exclude_dirs, exclude_hidden))
{
match entry {
Ok(entry) => {
Expand Down Expand Up @@ -140,7 +147,7 @@ pub fn scan_directory(

folders
.into_iter()
.filter(|f| !path_contains_skip_dir(&f.path))
.filter(|f| !path_contains_skip_dir(&f.path, &all_exclude_dirs))
.collect()
}

Expand All @@ -156,28 +163,20 @@ fn should_skip_entry(
return true;
}

if exclude_dirs.iter().any(|d| name_str == *d) {
return true;
}

SKIP_DIRS.contains(&name_str.as_ref())
exclude_dirs.iter().any(|d| name_str == *d)
}

fn path_contains_skip_dir(path: &Path) -> bool {
fn path_contains_skip_dir(path: &Path, exclude_dirs: &[String]) -> bool {
path.components().any(|c| {
if let std::path::Component::Normal(name) = c {
should_skip(name)
let name_str = name.to_string_lossy();
exclude_dirs.iter().any(|d| name_str == *d)
} else {
false
}
})
}

fn should_skip(name: &std::ffi::OsStr) -> bool {
let name_str = name.to_string_lossy();
SKIP_DIRS.iter().any(|s| name_str == *s)
}

#[cfg(unix)]
fn get_unique_id(path: &Path) -> Option<u64> {
use std::os::unix::fs::MetadataExt;
Expand Down
Loading