killpy 0.19.2__tar.gz → 0.21.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {killpy-0.19.2 → killpy-0.21.0}/PKG-INFO +114 -2
  2. {killpy-0.19.2 → killpy-0.21.0}/README.md +113 -1
  3. {killpy-0.19.2 → killpy-0.21.0}/killpy/__main__.py +9 -0
  4. {killpy-0.19.2 → killpy-0.21.0}/killpy/cli.py +52 -5
  5. {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/delete.py +8 -3
  6. killpy-0.21.0/killpy/commands/doctor.py +291 -0
  7. {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/stats.py +38 -1
  8. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/conda.py +4 -3
  9. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pipx.py +32 -26
  10. killpy-0.21.0/killpy/intelligence/__init__.py +56 -0
  11. killpy-0.21.0/killpy/intelligence/git_analyzer.py +119 -0
  12. killpy-0.21.0/killpy/intelligence/scoring.py +227 -0
  13. killpy-0.21.0/killpy/intelligence/suggestions.py +120 -0
  14. killpy-0.21.0/killpy/intelligence/tracker.py +123 -0
  15. {killpy-0.19.2 → killpy-0.21.0}/killpy/models.py +82 -2
  16. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/PKG-INFO +114 -2
  17. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/SOURCES.txt +7 -1
  18. {killpy-0.19.2 → killpy-0.21.0}/pyproject.toml +11 -3
  19. {killpy-0.19.2 → killpy-0.21.0}/killpy/__init__.py +0 -0
  20. {killpy-0.19.2 → killpy-0.21.0}/killpy/cleaner.py +0 -0
  21. {killpy-0.19.2 → killpy-0.21.0}/killpy/cleaners/__init__.py +0 -0
  22. {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/__init__.py +0 -0
  23. {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/clean.py +0 -0
  24. {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/list.py +0 -0
  25. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/__init__.py +0 -0
  26. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/artifacts.py +0 -0
  27. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/base.py +0 -0
  28. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/cache.py +0 -0
  29. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/hatch.py +0 -0
  30. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pipenv.py +0 -0
  31. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/poetry.py +0 -0
  32. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pyenv.py +0 -0
  33. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/tox.py +0 -0
  34. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/uv.py +0 -0
  35. {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/venv.py +0 -0
  36. {killpy-0.19.2 → killpy-0.21.0}/killpy/files/__init__.py +0 -0
  37. {killpy-0.19.2 → killpy-0.21.0}/killpy/scanner.py +0 -0
  38. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/dependency_links.txt +0 -0
  39. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/entry_points.txt +0 -0
  40. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/requires.txt +0 -0
  41. {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/top_level.txt +0 -0
  42. {killpy-0.19.2 → killpy-0.21.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: killpy
3
- Version: 0.19.2
3
+ Version: 0.21.0
4
4
  Summary: List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹.
5
5
  Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
6
6
  Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
@@ -224,6 +224,24 @@ killpy list --json # output as a JSON array
224
224
  killpy list --json-stream # stream as NDJSON — one line per env
225
225
  ```
226
226
 
227
+ `--json` example output:
228
+
229
+ ```json
230
+ [
231
+ {
232
+ "path": "projects/my-app/.venv",
233
+ "absolute_path": "/home/user/projects/my-app/.venv",
234
+ "name": "my-app/.venv",
235
+ "type": "venv",
236
+ "last_accessed": "2025-11-02T14:23:01+00:00",
237
+ "size_bytes": 54393984,
238
+ "size_human": "51.88 MB",
239
+ "managed_by": null,
240
+ "is_system_critical": false
241
+ }
242
+ ]
243
+ ```
244
+
227
245
  `--json-stream` is ideal for piping into `jq` or processing in scripts before the full scan completes:
228
246
 
229
247
  ```bash
@@ -252,6 +270,7 @@ ______________________________________________________________________
252
270
  killpy stats
253
271
  killpy stats --path ~/projects
254
272
  killpy stats --json
273
+ killpy stats --history # cumulative scan history
255
274
  ```
256
275
 
257
276
  Example output:
@@ -283,6 +302,98 @@ Removes `__pycache__` directories recursively under the target path.
283
302
 
284
303
  ______________________________________________________________________
285
304
 
305
+ ### `killpy doctor` — smart health report
306
+
307
+ ```
308
+ Usage: killpy doctor [OPTIONS]
309
+
310
+ Options:
311
+ --path DIRECTORY Root directory to scan [default: cwd]
312
+ --all Show all environments grouped by category
313
+ (HIGH / MEDIUM / LOW). Default shows only the top 5.
314
+ --json Output as JSON.
315
+ --help Show this message and exit.
316
+ ```
317
+
318
+ `doctor` analyses every detected virtual environment in two phases:
319
+
320
+ **Phase 1 — Scoring (for sorting only)**
321
+
322
+ A numeric score between 0 and 1 is computed from four weighted signals:
323
+
324
+ | Signal | Description |
325
+ |--------|-------------|
326
+ | **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
327
+ | **Age** | Days since last access, linear up to 365 days. |
328
+ | **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
329
+ | **Git inactivity** | The associated git repository has no recent commits. |
330
+
331
+ The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
332
+
333
+ **Phase 2 — Rule-based classification**
334
+
335
+ Category is assigned deterministically by applying the following rules in order:
336
+
337
+ | Priority | Rule | Category |
338
+ |----------|------|----------|
339
+ | 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
340
+ | 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
341
+ | 3 | Active git repository **or** `age < 120 days` | `LOW` |
342
+ | 4 | `age ≥ 120 days` | `MEDIUM` |
343
+ | 5 | Fallback | `LOW` |
344
+
345
+ Age and orphan status are the dominant signals. Size does not affect the category.
346
+
347
+ | Category | Recommended action |
348
+ |----------|--------------------|
349
+ | `HIGH` | Delete — unused and orphaned |
350
+ | `MEDIUM` | Review — possibly unused |
351
+ | `LOW` | Keep — actively used / Keep |
352
+
353
+ Examples:
354
+
355
+ ```bash
356
+ killpy doctor # top 5 offenders in current directory
357
+ killpy doctor --path ~ # scan home folder
358
+ killpy doctor --all # show all environments by category
359
+ killpy doctor --json # machine-readable output
360
+ killpy doctor --path ~/projects --all # full report for a specific tree
361
+ ```
362
+
363
+ Example output (default):
364
+
365
+ ```
366
+ ──────────── Environment Health Report ────────────
367
+ Scanned: /home/user/projects
368
+ Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
369
+ HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
370
+
371
+ Top 5 Offenders
372
+ ┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
373
+ │ Path │ Size │ Age (days)│ Score │ Category │
374
+ ├──────────────────────────┼────────┼───────────┼───────┼──────────┤
375
+ │ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
376
+ │ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
377
+ └──────────────────────────┴────────┴───────────┴───────┴──────────┘
378
+
379
+ Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
380
+ (12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
381
+ ```
382
+
383
+ Use `--all` to see all three category tables at once:
384
+
385
+ ```bash
386
+ killpy doctor --all
387
+ ```
388
+
389
+ The JSON output is useful for scripting or auditing:
390
+
391
+ ```bash
392
+ killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
393
+ ```
394
+
395
+ ______________________________________________________________________
396
+
286
397
  ## killpy vs alternatives
287
398
 
288
399
  | Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
@@ -410,7 +521,7 @@ Keep your repo free of `__pycache__` on every commit:
410
521
 
411
522
  ```yaml
412
523
  - repo: https://github.com/Tlaloc-Es/KillPy
413
- rev: 0.16.0
524
+ rev: 0.21.0
414
525
  hooks:
415
526
  - id: killpy
416
527
  pass_filenames: false
@@ -437,6 +548,7 @@ ______________________________________________________________________
437
548
  - [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
438
549
  - [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
439
550
  - [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
551
+ - [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
440
552
 
441
553
  ______________________________________________________________________
442
554
 
@@ -204,6 +204,24 @@ killpy list --json # output as a JSON array
204
204
  killpy list --json-stream # stream as NDJSON — one line per env
205
205
  ```
206
206
 
207
+ `--json` example output:
208
+
209
+ ```json
210
+ [
211
+ {
212
+ "path": "projects/my-app/.venv",
213
+ "absolute_path": "/home/user/projects/my-app/.venv",
214
+ "name": "my-app/.venv",
215
+ "type": "venv",
216
+ "last_accessed": "2025-11-02T14:23:01+00:00",
217
+ "size_bytes": 54393984,
218
+ "size_human": "51.88 MB",
219
+ "managed_by": null,
220
+ "is_system_critical": false
221
+ }
222
+ ]
223
+ ```
224
+
207
225
  `--json-stream` is ideal for piping into `jq` or processing in scripts before the full scan completes:
208
226
 
209
227
  ```bash
@@ -232,6 +250,7 @@ ______________________________________________________________________
232
250
  killpy stats
233
251
  killpy stats --path ~/projects
234
252
  killpy stats --json
253
+ killpy stats --history # cumulative scan history
235
254
  ```
236
255
 
237
256
  Example output:
@@ -263,6 +282,98 @@ Removes `__pycache__` directories recursively under the target path.
263
282
 
264
283
  ______________________________________________________________________
265
284
 
285
+ ### `killpy doctor` — smart health report
286
+
287
+ ```
288
+ Usage: killpy doctor [OPTIONS]
289
+
290
+ Options:
291
+ --path DIRECTORY Root directory to scan [default: cwd]
292
+ --all Show all environments grouped by category
293
+ (HIGH / MEDIUM / LOW). Default shows only the top 5.
294
+ --json Output as JSON.
295
+ --help Show this message and exit.
296
+ ```
297
+
298
+ `doctor` analyses every detected virtual environment in two phases:
299
+
300
+ **Phase 1 — Scoring (for sorting only)**
301
+
302
+ A numeric score between 0 and 1 is computed from four weighted signals:
303
+
304
+ | Signal | Description |
305
+ |--------|-------------|
306
+ | **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
307
+ | **Age** | Days since last access, linear up to 365 days. |
308
+ | **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
309
+ | **Git inactivity** | The associated git repository has no recent commits. |
310
+
311
+ The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
312
+
313
+ **Phase 2 — Rule-based classification**
314
+
315
+ Category is assigned deterministically by applying the following rules in order:
316
+
317
+ | Priority | Rule | Category |
318
+ |----------|------|----------|
319
+ | 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
320
+ | 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
321
+ | 3 | Active git repository **or** `age < 120 days` | `LOW` |
322
+ | 4 | `age ≥ 120 days` | `MEDIUM` |
323
+ | 5 | Fallback | `LOW` |
324
+
325
+ Age and orphan status are the dominant signals. Size does not affect the category.
326
+
327
+ | Category | Recommended action |
328
+ |----------|--------------------|
329
+ | `HIGH` | Delete — unused and orphaned |
330
+ | `MEDIUM` | Review — possibly unused |
331
+ | `LOW` | Keep — actively used / Keep |
332
+
333
+ Examples:
334
+
335
+ ```bash
336
+ killpy doctor # top 5 offenders in current directory
337
+ killpy doctor --path ~ # scan home folder
338
+ killpy doctor --all # show all environments by category
339
+ killpy doctor --json # machine-readable output
340
+ killpy doctor --path ~/projects --all # full report for a specific tree
341
+ ```
342
+
343
+ Example output (default):
344
+
345
+ ```
346
+ ──────────── Environment Health Report ────────────
347
+ Scanned: /home/user/projects
348
+ Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
349
+ HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
350
+
351
+ Top 5 Offenders
352
+ ┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
353
+ │ Path │ Size │ Age (days)│ Score │ Category │
354
+ ├──────────────────────────┼────────┼───────────┼───────┼──────────┤
355
+ │ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
356
+ │ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
357
+ └──────────────────────────┴────────┴───────────┴───────┴──────────┘
358
+
359
+ Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
360
+ (12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
361
+ ```
362
+
363
+ Use `--all` to see all three category tables at once:
364
+
365
+ ```bash
366
+ killpy doctor --all
367
+ ```
368
+
369
+ The JSON output is useful for scripting or auditing:
370
+
371
+ ```bash
372
+ killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
373
+ ```
374
+
375
+ ______________________________________________________________________
376
+
266
377
  ## killpy vs alternatives
267
378
 
268
379
  | Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
@@ -390,7 +501,7 @@ Keep your repo free of `__pycache__` on every commit:
390
501
 
391
502
  ```yaml
392
503
  - repo: https://github.com/Tlaloc-Es/KillPy
393
- rev: 0.16.0
504
+ rev: 0.21.0
394
505
  hooks:
395
506
  - id: killpy
396
507
  pass_filenames: false
@@ -417,6 +528,7 @@ ______________________________________________________________________
417
528
  - [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
418
529
  - [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
419
530
  - [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
531
+ - [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
420
532
 
421
533
  ______________________________________________________________________
422
534
 
@@ -9,9 +9,11 @@ from killpy.cleaner import Cleaner
9
9
  from killpy.cli import TableApp
10
10
  from killpy.commands.clean import clean
11
11
  from killpy.commands.delete import delete_cmd
12
+ from killpy.commands.doctor import doctor_cmd
12
13
  from killpy.commands.list import list_cmd
13
14
  from killpy.commands.stats import stats_cmd
14
15
  from killpy.files import format_size
16
+ from killpy.intelligence.tracker import UsageTracker
15
17
  from killpy.scanner import Scanner
16
18
 
17
19
 
@@ -63,6 +65,12 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
63
65
  f"freed [bold]{format_size(freed)}[/bold]."
64
66
  )
65
67
 
68
+ # Best-effort: record deletion in history.
69
+ try:
70
+ UsageTracker().record_deletion(freed)
71
+ except Exception: # noqa: BLE001
72
+ pass
73
+
66
74
 
67
75
  @click.group(invoke_without_command=True)
68
76
  @click.option(
@@ -109,6 +117,7 @@ cli.add_command(clean)
109
117
  cli.add_command(list_cmd, name="list")
110
118
  cli.add_command(delete_cmd, name="delete")
111
119
  cli.add_command(stats_cmd, name="stats")
120
+ cli.add_command(doctor_cmd, name="doctor")
112
121
 
113
122
 
114
123
  if __name__ == "__main__":
@@ -8,6 +8,7 @@ from importlib.metadata import PackageNotFoundError, version
8
8
  from pathlib import Path
9
9
  from typing import Any, TypedDict
10
10
 
11
+ from rich.text import Text
11
12
  from textual.app import App, ComposeResult
12
13
  from textual.binding import Binding
13
14
  from textual.widgets import (
@@ -24,9 +25,23 @@ from textual.widgets import (
24
25
  from killpy.cleaner import Cleaner, CleanerError
25
26
  from killpy.cleaners import remove_pycache
26
27
  from killpy.files import format_size
28
+ from killpy.intelligence import SuggestionEngine, score_all
27
29
  from killpy.models import Environment
28
30
  from killpy.scanner import Scanner
29
31
 
32
+ _HEALTH_STYLES: dict[str, tuple[str, str]] = {
33
+ "HIGH": ("HIGH", "bold red"),
34
+ "MEDIUM": ("MED", "bold yellow"),
35
+ "LOW": ("LOW", "bold green"),
36
+ }
37
+
38
+
39
+ def _health_text(category: str) -> Text | str:
40
+ if category in _HEALTH_STYLES:
41
+ label, style = _HEALTH_STYLES[category]
42
+ return Text(label, style=style)
43
+ return ""
44
+
30
45
 
31
46
  def is_venv_tab(func):
32
47
  def wrapper(self, *args, **kwargs):
@@ -68,6 +83,7 @@ class VenvRow(TypedDict):
68
83
  last_modified: str
69
84
  size: int
70
85
  size_human: str
86
+ health: str
71
87
  status: str
72
88
  environment: Environment
73
89
 
@@ -87,6 +103,7 @@ class TableApp(App):
87
103
  "Last Modified",
88
104
  "Size",
89
105
  "Size (Human Readable)",
106
+ "Health",
90
107
  "Status",
91
108
  ]
92
109
  PIPX_HEADERS = ["Package", "Size", "Size (Human Readable)", "Status"]
@@ -95,7 +112,8 @@ class TableApp(App):
95
112
  VENV_COL_LAST_MODIFIED = 2
96
113
  VENV_COL_SIZE = 3
97
114
  VENV_COL_SIZE_HUMAN = 4
98
- VENV_COL_STATUS = 5
115
+ VENV_COL_HEALTH = 5
116
+ VENV_COL_STATUS = 6
99
117
 
100
118
  PIPX_COL_PACKAGE = 0
101
119
  PIPX_COL_SIZE = 1
@@ -125,6 +143,7 @@ class TableApp(App):
125
143
  self._venv_display_indices: list[int] = []
126
144
  self._multi_select_mode: bool = False
127
145
  self._selected_venv_indices: set[int] = set()
146
+ self._health_by_path: dict[str, str] = {}
128
147
  self.cleaner = Cleaner()
129
148
  self.scanner = Scanner(
130
149
  types={
@@ -315,6 +334,7 @@ class TableApp(App):
315
334
  "last_modified": environment.last_accessed_str,
316
335
  "size": environment.size_bytes,
317
336
  "size_human": environment.size_human,
337
+ "health": self._health_by_path.get(str(environment.path), ""),
318
338
  "status": "",
319
339
  "environment": environment,
320
340
  }
@@ -332,6 +352,7 @@ class TableApp(App):
332
352
  row["last_modified"],
333
353
  row["size"],
334
354
  row["size_human"],
355
+ _health_text(row["health"]),
335
356
  row["status"],
336
357
  )
337
358
 
@@ -373,6 +394,7 @@ class TableApp(App):
373
394
  row["last_modified"],
374
395
  row["size"],
375
396
  row["size_human"],
397
+ _health_text(row["health"]),
376
398
  self._compute_row_status(i, row),
377
399
  )
378
400
 
@@ -453,6 +475,11 @@ class TableApp(App):
453
475
  self.venv_rows.sort(
454
476
  key=lambda row: row["size_human"].lower(), reverse=reverse
455
477
  )
478
+ elif column_index == self.VENV_COL_HEALTH:
479
+ _order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "": 3}
480
+ self.venv_rows.sort(
481
+ key=lambda row: _order.get(row["health"], 3), reverse=reverse
482
+ )
456
483
  elif column_index == self.VENV_COL_STATUS:
457
484
  self.venv_rows.sort(key=lambda row: row["status"].lower(), reverse=reverse)
458
485
  else:
@@ -545,6 +572,21 @@ class TableApp(App):
545
572
  status_label.update(
546
573
  f"Found {venv_count} virtual environments and {pipx_count} pipx packages"
547
574
  )
575
+ await self._compute_health_scores()
576
+
577
+ async def _compute_health_scores(self) -> None:
578
+ """Score venv environments and populate the Health column."""
579
+ if not self.venv_rows:
580
+ return
581
+ envs = [row["environment"] for row in self.venv_rows]
582
+ scored = await asyncio.to_thread(lambda: score_all(envs, run_git=False))
583
+ engine = SuggestionEngine()
584
+ suggestions = engine.classify_all(scored)
585
+ for suggestion in suggestions:
586
+ self._health_by_path[str(suggestion.env_path)] = suggestion.category
587
+ for row in self.venv_rows:
588
+ row["health"] = self._health_by_path.get(row["path"], "")
589
+ self.render_venv_table()
548
590
 
549
591
  async def action_clean_pycache(self):
550
592
  total_freed_space = await asyncio.to_thread(remove_pycache, self.root_dir)
@@ -612,11 +654,12 @@ class TableApp(App):
612
654
  return
613
655
  elif current_status == EnvStatus.MARKED_TO_DELETE.value:
614
656
  row["status"] = ""
615
- table.update_cell_at((cursor_cell.row, 5), "")
657
+ table.update_cell_at((cursor_cell.row, self.VENV_COL_STATUS), "")
616
658
  else:
617
659
  row["status"] = EnvStatus.MARKED_TO_DELETE.value
618
660
  table.update_cell_at(
619
- (cursor_cell.row, 5), EnvStatus.MARKED_TO_DELETE.value
661
+ (cursor_cell.row, self.VENV_COL_STATUS),
662
+ EnvStatus.MARKED_TO_DELETE.value,
620
663
  )
621
664
 
622
665
  @is_venv_tab
@@ -633,7 +676,10 @@ class TableApp(App):
633
676
  if self.delete_environment(row["environment"]):
634
677
  self.bytes_release += int(row["size"])
635
678
  row["status"] = EnvStatus.DELETED.value
636
- table.update_cell_at((cursor_cell.row, 5), EnvStatus.DELETED.value)
679
+ table.update_cell_at(
680
+ (cursor_cell.row, self.VENV_COL_STATUS),
681
+ EnvStatus.DELETED.value,
682
+ )
637
683
  self.query_one("#status-label", Label).update(
638
684
  f"{format_size(self.bytes_release)} deleted"
639
685
  )
@@ -773,7 +819,8 @@ class TableApp(App):
773
819
  self._selected_venv_indices.add(data_index)
774
820
  self._update_multi_select_label()
775
821
  table.update_cell_at(
776
- (cursor_cell.row, 5), self._compute_row_status(data_index, row)
822
+ (cursor_cell.row, self.VENV_COL_STATUS),
823
+ self._compute_row_status(data_index, row),
777
824
  )
778
825
 
779
826
  @is_venv_tab
@@ -11,6 +11,7 @@ from rich.console import Console
11
11
 
12
12
  from killpy.cleaner import Cleaner, CleanerError
13
13
  from killpy.files import format_size
14
+ from killpy.intelligence.tracker import UsageTracker
14
15
  from killpy.models import Environment
15
16
  from killpy.scanner import Scanner
16
17
 
@@ -29,9 +30,7 @@ def _filter_envs(
29
30
 
30
31
  if older_than is not None:
31
32
  cutoff = now - timedelta(days=older_than)
32
- result = [
33
- e for e in result if e.last_accessed < cutoff
34
- ]
33
+ result = [e for e in result if e.last_accessed < cutoff]
35
34
 
36
35
  return result
37
36
 
@@ -135,5 +134,11 @@ def delete_cmd(
135
134
  + (f" — [red]{errors} error(s)[/red]" if errors else "")
136
135
  )
137
136
 
137
+ # Best-effort: record deletion in history.
138
+ try:
139
+ UsageTracker().record_deletion(freed)
140
+ except Exception: # noqa: BLE001
141
+ pass
142
+
138
143
  if errors:
139
144
  sys.exit(1)