clutterxp 1.1.0__py3-none-any.whl

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 (84) hide show
  1. clutterxp/__init__.py +4 -0
  2. clutterxp/cli/__init__.py +7 -0
  3. clutterxp/cli/app.py +251 -0
  4. clutterxp/cli/cmd_config.py +250 -0
  5. clutterxp/cli/cmd_doctor.py +449 -0
  6. clutterxp/cli/cmd_duplicates.py +253 -0
  7. clutterxp/cli/cmd_gui.py +22 -0
  8. clutterxp/cli/cmd_history.py +233 -0
  9. clutterxp/cli/cmd_init.py +76 -0
  10. clutterxp/cli/cmd_organize.py +226 -0
  11. clutterxp/cli/cmd_rules.py +540 -0
  12. clutterxp/cli/cmd_scan.py +109 -0
  13. clutterxp/cli/cmd_service.py +130 -0
  14. clutterxp/cli/cmd_undo.py +180 -0
  15. clutterxp/cli/cmd_watch.py +147 -0
  16. clutterxp/cli/errors.py +166 -0
  17. clutterxp/cli/render.py +390 -0
  18. clutterxp/config/__init__.py +40 -0
  19. clutterxp/config/jsonschema.py +12 -0
  20. clutterxp/config/loader.py +212 -0
  21. clutterxp/config/placeholders.py +79 -0
  22. clutterxp/config/runtime.py +30 -0
  23. clutterxp/config/schema.py +166 -0
  24. clutterxp/config/validate.py +226 -0
  25. clutterxp/config/writer.py +248 -0
  26. clutterxp/config.example.yaml +76 -0
  27. clutterxp/duplicates/__init__.py +22 -0
  28. clutterxp/duplicates/detector.py +438 -0
  29. clutterxp/duplicates/hashcache.py +149 -0
  30. clutterxp/duplicates/namenorm.py +45 -0
  31. clutterxp/duplicates/quarantine.py +196 -0
  32. clutterxp/duplicates/report.py +110 -0
  33. clutterxp/gui/__init__.py +1 -0
  34. clutterxp/gui/api.py +494 -0
  35. clutterxp/gui/app.py +205 -0
  36. clutterxp/gui/models_qt.py +544 -0
  37. clutterxp/gui/views/__init__.py +21 -0
  38. clutterxp/gui/views/dashboard.py +253 -0
  39. clutterxp/gui/views/duplicates.py +404 -0
  40. clutterxp/gui/views/folders.py +354 -0
  41. clutterxp/gui/views/history.py +281 -0
  42. clutterxp/gui/views/preview.py +277 -0
  43. clutterxp/gui/views/rules.py +527 -0
  44. clutterxp/gui/views/settings.py +172 -0
  45. clutterxp/gui/workers.py +147 -0
  46. clutterxp/history/__init__.py +23 -0
  47. clutterxp/history/db.py +174 -0
  48. clutterxp/history/pathcodec.py +42 -0
  49. clutterxp/history/recovery.py +318 -0
  50. clutterxp/history/schema.sql +43 -0
  51. clutterxp/history/service.py +493 -0
  52. clutterxp/history/undo.py +373 -0
  53. clutterxp/interfaces.py +164 -0
  54. clutterxp/locking.py +113 -0
  55. clutterxp/logging_setup.py +73 -0
  56. clutterxp/models.py +266 -0
  57. clutterxp/mover/__init__.py +26 -0
  58. clutterxp/mover/collision.py +46 -0
  59. clutterxp/mover/fsops.py +191 -0
  60. clutterxp/mover/safe_move.py +741 -0
  61. clutterxp/organizer.py +388 -0
  62. clutterxp/packaging/clutterxp.service +15 -0
  63. clutterxp/paths.py +112 -0
  64. clutterxp/planner.py +339 -0
  65. clutterxp/py.typed +0 -0
  66. clutterxp/rules/__init__.py +16 -0
  67. clutterxp/rules/engine.py +374 -0
  68. clutterxp/rules/matcher.py +222 -0
  69. clutterxp/rules/template.py +171 -0
  70. clutterxp/rules/tokenizer.py +118 -0
  71. clutterxp/safety.py +111 -0
  72. clutterxp/scanner.py +196 -0
  73. clutterxp/service_install.py +208 -0
  74. clutterxp/units.py +59 -0
  75. clutterxp/watch/__init__.py +37 -0
  76. clutterxp/watch/debounce.py +84 -0
  77. clutterxp/watch/events.py +259 -0
  78. clutterxp/watch/ignore.py +77 -0
  79. clutterxp/watch/service.py +662 -0
  80. clutterxp/watch/settle.py +170 -0
  81. clutterxp-1.1.0.dist-info/METADATA +149 -0
  82. clutterxp-1.1.0.dist-info/RECORD +84 -0
  83. clutterxp-1.1.0.dist-info/WHEEL +4 -0
  84. clutterxp-1.1.0.dist-info/entry_points.txt +6 -0
@@ -0,0 +1,170 @@
1
+ """Settler: file completion and stability detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import stat
8
+ import time
9
+ from collections.abc import Callable
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class SettleStatus(str, Enum):
18
+ """Result of checking whether a file is settled and ready to move."""
19
+
20
+ READY = "ready"
21
+ NOT_YET = "not_yet"
22
+ GONE = "gone"
23
+ DROP = "drop"
24
+
25
+
26
+ class Settler:
27
+ """Verifies file stability (stat checks and /proc open-for-write checks)."""
28
+
29
+ def __init__(
30
+ self,
31
+ clock: Callable[[], float] = time.monotonic,
32
+ stat_fn: Callable[[Path | str], Any] = os.lstat,
33
+ proc_dir: Path = Path("/proc"),
34
+ *,
35
+ sample_interval: float = 0.5,
36
+ max_wait: float = 6 * 3600.0,
37
+ ) -> None:
38
+ self._clock = clock
39
+ self._stat_fn = stat_fn
40
+ self._proc_dir = proc_dir
41
+ self.sample_interval = sample_interval
42
+ self.max_wait = max_wait
43
+ self._samples: dict[Path, tuple[tuple[int, int], float]] = {}
44
+ self._first_seen: dict[Path, float] = {}
45
+
46
+ def clear(self, path: Path) -> None:
47
+ """Clear recorded samples and first-seen timestamp for path."""
48
+ self._samples.pop(path, None)
49
+ self._first_seen.pop(path, None)
50
+
51
+ def check(self, path: Path, settle_seconds: float) -> SettleStatus:
52
+ """Check whether candidate file is settled and safe to move.
53
+
54
+ Steps:
55
+ 1. Regular file check: must exist, regular file, not symlink.
56
+ 2. Stable stat check: 2 samples >= sample_interval apart with identical (size, mtime_ns).
57
+ 3. Open for write check: no process on Linux holds path open for write.
58
+ 4. If all pass -> READY.
59
+ """
60
+ # Step 1: Check existence and regular file status
61
+ try:
62
+ st = self._stat_fn(path)
63
+ except (FileNotFoundError, OSError):
64
+ self.clear(path)
65
+ return SettleStatus.GONE
66
+
67
+ is_symlink = getattr(st, "is_symlink", False)
68
+ is_regular = getattr(st, "is_regular", True)
69
+ mode = getattr(st, "st_mode", None)
70
+ if mode is not None:
71
+ is_symlink = stat.S_ISLNK(mode)
72
+ is_regular = stat.S_ISREG(mode)
73
+
74
+ if is_symlink or not is_regular:
75
+ self.clear(path)
76
+ return SettleStatus.DROP
77
+
78
+ now = self._clock()
79
+
80
+ # Track continuous activity and enforce max_wait
81
+ if path not in self._first_seen:
82
+ self._first_seen[path] = now
83
+ elif (now - self._first_seen[path]) > self.max_wait:
84
+ logger.warning(
85
+ "Candidate %s has not settled after %s seconds; dropping",
86
+ path,
87
+ self.max_wait,
88
+ )
89
+ self.clear(path)
90
+ return SettleStatus.DROP
91
+
92
+ # Step 2: Two-sample stable stat check
93
+ size = getattr(st, "st_size", getattr(st, "size", 0))
94
+ mtime_ns = getattr(st, "st_mtime_ns", getattr(st, "mtime_ns", 0))
95
+ current_stat = (size, mtime_ns)
96
+
97
+ interval = min(self.sample_interval, settle_seconds) if settle_seconds > 0 else 0.0
98
+
99
+ if path not in self._samples:
100
+ self._samples[path] = (current_stat, now)
101
+ return SettleStatus.NOT_YET
102
+
103
+ prev_stat, prev_time = self._samples[path]
104
+ if current_stat != prev_stat:
105
+ self._samples[path] = (current_stat, now)
106
+ return SettleStatus.NOT_YET
107
+
108
+ if (now - prev_time) < interval:
109
+ return SettleStatus.NOT_YET
110
+
111
+ # Step 3: Not open for writing
112
+ if self.is_open_for_write(path):
113
+ return SettleStatus.NOT_YET
114
+
115
+ # Step 4: Ready
116
+ self.clear(path)
117
+ return SettleStatus.READY
118
+
119
+ def is_open_for_write(self, path: Path) -> bool:
120
+ """Inspect /proc to determine if any process holds path open for write."""
121
+ if not self._proc_dir.exists() or not self._proc_dir.is_dir():
122
+ return False
123
+
124
+ try:
125
+ target_real = os.path.realpath(str(path))
126
+ except OSError:
127
+ return False
128
+
129
+ try:
130
+ proc_entries = list(self._proc_dir.iterdir())
131
+ except OSError:
132
+ return False
133
+
134
+ for pid_entry in proc_entries:
135
+ if not pid_entry.name.isdigit():
136
+ continue
137
+ fd_dir = pid_entry / "fd"
138
+ try:
139
+ fd_entries = list(fd_dir.iterdir())
140
+ except OSError:
141
+ continue
142
+
143
+ for fd_path in fd_entries:
144
+ try:
145
+ link_dest = str(fd_path.readlink())
146
+ if link_dest == target_real or link_dest == f"{target_real} (deleted)":
147
+ fdinfo_path = pid_entry / "fdinfo" / fd_path.name
148
+ flags = self._read_fdinfo_flags(fdinfo_path)
149
+ if flags is not None and (flags & os.O_ACCMODE) != os.O_RDONLY:
150
+ return True
151
+ except OSError:
152
+ continue
153
+
154
+ return False
155
+
156
+ def _read_fdinfo_flags(self, fdinfo_path: Path) -> int | None:
157
+ """Read and parse octal 'flags:' field from /proc/<pid>/fdinfo/<fd>."""
158
+ try:
159
+ with fdinfo_path.open("r", encoding="utf-8", errors="replace") as f:
160
+ for line in f:
161
+ if line.startswith("flags:"):
162
+ parts = line.split()
163
+ if len(parts) >= 2:
164
+ return int(parts[1], 8)
165
+ except OSError:
166
+ pass
167
+ return None
168
+
169
+
170
+ __all__ = ["SettleStatus", "Settler"]
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.5
2
+ Name: clutterxp
3
+ Version: 1.1.0
4
+ Summary: Configurable, safe, lightweight file organizer: rules, preview, undo, watch, duplicates.
5
+ Keywords: automation,cli,downloads,file-organizer,linux
6
+ Classifier: Environment :: Console
7
+ Classifier: Operating System :: POSIX :: Linux
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: System :: Filesystems
10
+ Classifier: Topic :: Utilities
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: platformdirs>=4.0
13
+ Requires-Dist: pydantic>=2.6
14
+ Requires-Dist: ruamel-yaml-clib>=0.2.12; platform_python_implementation == 'CPython'
15
+ Requires-Dist: ruamel-yaml>=0.18
16
+ Requires-Dist: typer>=0.12
17
+ Requires-Dist: watchdog>=4.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: build>=1.0; extra == 'dev'
20
+ Requires-Dist: hypothesis>=6.100; extra == 'dev'
21
+ Requires-Dist: mypy>=1.10; extra == 'dev'
22
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
23
+ Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: ruff>=0.5; extra == 'dev'
26
+ Provides-Extra: gui
27
+ Requires-Dist: pyside6>=6.6; extra == 'gui'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ClutterXP
31
+
32
+ > **Tired of a messy Downloads folder?** ClutterXP quietly tidies your files into the right place—on your terms, without ever losing a single file.
33
+
34
+ ---
35
+
36
+ Whether your `Downloads` folder is buried under months of receipts, PDFs, wallpapers, installers, and screenshots, or you just want your desktop kept neat, **ClutterXP** does the heavy lifting for you.
37
+
38
+ Use the **clean visual desktop app** or run quick commands from your terminal.
39
+
40
+ ---
41
+
42
+ ## Why you can actually trust it
43
+
44
+ Most file cleanup tools feel risky—what if they delete something important or move a file you didn't want touched? We built ClutterXP with ironclad safety guarantees:
45
+
46
+ - 🛡️ **Never deletes or overwrites**: Files only move if the destination is safe. If a file with the same name already exists, ClutterXP renames the new one (e.g. `report (1).pdf`) or skips it. Your existing files are never replaced or destroyed.
47
+ - 🎯 **Only moves what you tell it to**: If a file doesn't match a rule you created, it stays exactly where it is. No surprise moves, no mystery "unsorted" dump folders.
48
+ - ↩️ **One-click instant undo**: Every action is saved in a local journal. If you ever change your mind, `clutterxp undo` (or a single click in the app) puts every file right back where it was originally.
49
+ - 🔒 **100% private and offline**: Runs completely on your machine. No accounts, no cloud sync, no tracking, and zero internet requests.
50
+
51
+ ---
52
+
53
+ ## Getting Started
54
+
55
+ ### 1. Installation
56
+
57
+ Install ClutterXP with Python (3.10+):
58
+
59
+ ```bash
60
+ # Recommended: install with desktop app (GUI) support
61
+ pip install "clutterxp[gui]"
62
+
63
+ # Or on Kali Linux / Debian using pipx:
64
+ pipx install .
65
+ ```
66
+
67
+ ### 2. Choose how you want to use it
68
+
69
+ #### Option A: The Desktop App
70
+ Just run:
71
+ ```bash
72
+ clutterxp gui
73
+ ```
74
+ You'll get an intuitive dashboard where you can:
75
+ - **Preview moves before they happen** with a clear interactive table.
76
+ - **Set up simple rules** with live matching tests (type a filename and see which rule catches it instantly).
77
+ - **Find duplicate files** and safely quarantine them without risking accidental deletion.
78
+ - **Undo any batch** with a single button.
79
+
80
+ #### Option B: The Terminal (Fast & Scriptable)
81
+ Prefer working from the command line? Use `clutterxp` or the short alias `cxp`:
82
+
83
+ ```bash
84
+ # 1. Create your default starter config
85
+ clutterxp init
86
+
87
+ # 2. Test which rule matches a filename
88
+ clutterxp rules test "invoice_march.pdf"
89
+
90
+ # 3. Preview what would be organized (dry-run, touches nothing)
91
+ clutterxp organize ~/Downloads --dry-run
92
+
93
+ # 4. Organize your Downloads folder
94
+ clutterxp organize ~/Downloads --yes
95
+
96
+ # 5. Changed your mind? Undo the entire batch instantly!
97
+ clutterxp undo --yes
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Features that make life easier
103
+
104
+ ### 📁 Set it and forget it (Background Watcher)
105
+ Turn on automatic sorting. Whenever a new download finishes, ClutterXP waits until it's completely written to disk, then quietly sorts it into place.
106
+ ```bash
107
+ clutterxp watch
108
+ ```
109
+ You can even enable it as a system background service:
110
+ ```bash
111
+ clutterxp service install
112
+ clutterxp service status
113
+ ```
114
+
115
+ ### 🔍 Find and clean duplicate downloads
116
+ Downloaded the same PDF or installer three times? ClutterXP safely identifies duplicate files taking up storage, shows you where they live, and lets you isolate them safely.
117
+ ```bash
118
+ clutterxp duplicates scan ~/Downloads
119
+ ```
120
+
121
+ ### ✏️ Customize your rules in seconds
122
+ Rules are dead simple. Group by keywords, file extensions, size, or age:
123
+ - *"Move anything with `invoice` or `receipt` to `~/Documents/Invoices`"*
124
+ - *"Move `.png` and `.jpg` with `wallpaper` to `~/Pictures/Wallpapers`"*
125
+ - *"Move `.deb`, `.iso`, `.rpm` to `~/Downloads/Installers`"*
126
+
127
+ You can edit them directly in the visual app or via `clutterxp rules add`.
128
+
129
+ ---
130
+
131
+ ## Cheat Sheet
132
+
133
+ | What you want to do | Command |
134
+ |:---|:---|
135
+ | Open the visual desktop app | `clutterxp gui` |
136
+ | Preview organization without moving files | `cxp organize ~/Downloads --dry-run` |
137
+ | Organize a folder | `cxp organize ~/Downloads` |
138
+ | Undo the last batch of moves | `cxp undo` |
139
+ | Watch a folder and sort new files automatically | `cxp watch` |
140
+ | Install background watcher service | `cxp service install` |
141
+ | Scan for duplicate files | `cxp duplicates scan ~/Downloads` |
142
+ | Check system health and file integrity | `cxp doctor` |
143
+ | View organization history | `cxp history` |
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,84 @@
1
+ clutterxp/__init__.py,sha256=NTPQTaRDLLih33Dej5fibtZFRtxjjoW6UC7zGoY583M,83
2
+ clutterxp/interfaces.py,sha256=Zimz4RzcKVeWFkcS5htqTEw2g8eJU4KNK5xjpLbhUT8,4462
3
+ clutterxp/locking.py,sha256=q5z4hcXHyAa7UWSHKkHYOIdv8saenrqzwM_G_lwpC0k,3528
4
+ clutterxp/logging_setup.py,sha256=jPwXjjZMpJJ9qFwN_CRiE8D4refmqXRewgqPG-231Uw,2371
5
+ clutterxp/models.py,sha256=seeIhy48hhMIRBb2wuzBgaHdFNDF9yYKiS8SklKgjKk,8090
6
+ clutterxp/organizer.py,sha256=OXcoY7T025ewjeZ60wEwLJhyiJnecylO-amwicHr_HQ,14466
7
+ clutterxp/paths.py,sha256=oJHZ9UN-CWFbQSuq5h_jY6dZKneWSOO6UTXvMOFAIF8,3685
8
+ clutterxp/planner.py,sha256=QpQciSFYd2M1-q72DMzfmzKmN1XmOCC30J4RoCRTQSs,12509
9
+ clutterxp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ clutterxp/safety.py,sha256=gE9rj2bxyUde8gw3GMy3TKpY78y0qx2Ml6NFV-c1BJk,3483
11
+ clutterxp/scanner.py,sha256=98ku6thFoVPxEjUIWo3jjydtvHQYxfJTyYcpkgYs-1Q,6129
12
+ clutterxp/service_install.py,sha256=7116gxHv7yqLmdYehCXpH0KfFVrhI8rfi-jVSLwrQQM,6566
13
+ clutterxp/units.py,sha256=SKCEUmQVwBsfnDrE5HFetpdI8ry3Isl51RCMTC2v5V4,1918
14
+ clutterxp/cli/__init__.py,sha256=Kz_nwITT56ObyGl0aLJAtFFxtFEU55LlRyEbCVeElsI,197
15
+ clutterxp/cli/app.py,sha256=tLtxvGrY80c5YqW0CwHOK6fIP3D2n33yICuP1NZtpBQ,7144
16
+ clutterxp/cli/cmd_config.py,sha256=N7ge_RrfVT8CD4RESgk_yMBaGJFwCf82nU2Lz0IELbk,9074
17
+ clutterxp/cli/cmd_doctor.py,sha256=VCQHcmiLpXodVhxwMqmo4bPUgAZPUIRwMPu-Aga_6fs,16489
18
+ clutterxp/cli/cmd_duplicates.py,sha256=mXOi2uuimdOiK5IxIqEXF0kHj_fYQsCIvqCYfyOj9cg,8378
19
+ clutterxp/cli/cmd_gui.py,sha256=N9pKrQPgW4zVPUYzjEesfson-3ZKEe6Vau1qhHcx_rk,583
20
+ clutterxp/cli/cmd_history.py,sha256=g6LDRH_otqQsM6FcBCSR-xPazc57lGZMjHH5SMFYT2k,7620
21
+ clutterxp/cli/cmd_init.py,sha256=opEWoDa5_3pdv2lDzwifdyyv17uG4Dr63oyy2bTeLaE,2232
22
+ clutterxp/cli/cmd_organize.py,sha256=zJyZMHd0rFdleoB0T8g3xann-kLT4vzPYKf7U2HotUI,7562
23
+ clutterxp/cli/cmd_rules.py,sha256=IZl7Io343-9r7v59IxqsgStlNLE5-TzVLLDkAKLlXg4,18509
24
+ clutterxp/cli/cmd_scan.py,sha256=m44Qx6OX87lg91UAKvIfc1SfdyVu6MLauFAi19kKoWc,3594
25
+ clutterxp/cli/cmd_service.py,sha256=CL5SvqHiviGcYoNi8mkuWPeJuCBBQyuZ8BLpmRVl0Q8,4119
26
+ clutterxp/cli/cmd_undo.py,sha256=Z0Fvwd5akfTVTbR7KrqpnV4UhoXvdOOCYRkoGgkzIps,5940
27
+ clutterxp/cli/cmd_watch.py,sha256=WWOewgbzn0cbpyBIekuu8E847wk8vu6LSpgT6J9pa9Q,5114
28
+ clutterxp/cli/errors.py,sha256=wB1yZTJdZ7tdQIO1CRcqGWLdX5T2MFvNCKKtAjYMY0c,5206
29
+ clutterxp/cli/render.py,sha256=CKDI7ZAvdvrmqjN8O8y076Aljgk455szIGTvJ6AauRY,13415
30
+ clutterxp/config/__init__.py,sha256=5oqZd6CoBxJPrg1Uw-lt6_xVsotxJa582RVQO5TGAEA,961
31
+ clutterxp/config/jsonschema.py,sha256=s9vG9BU3WIZwViRVmWVrYZWOUU2JBZfOnKjg2tmddk4,294
32
+ clutterxp/config/loader.py,sha256=CIdbrKKskg6ue1bh-Gt5U9Kw0XEHjwHOlpRGSrKQnys,7888
33
+ clutterxp/config/placeholders.py,sha256=7M1SFolDcB-7-2cFJlNMp58MDBBcCR0YoV5bFMTBp40,2466
34
+ clutterxp/config/runtime.py,sha256=MLIs1_kezu9HBvIUaL7VukJMp7Pkv1OWcpE6UAVC7h0,953
35
+ clutterxp/config/schema.py,sha256=1pu4IcJZQT6_OOuCq8zd8Fn6plLNAQMdfSd7c9i3Mfc,6011
36
+ clutterxp/config/validate.py,sha256=w6JnzTdXgHVKTQOnVSpDK1Y1oxHhDa8h_SMcCKVCuzs,9402
37
+ clutterxp/config/writer.py,sha256=KBAKr_AjeUdnS6LjIVzYPTl9_0w1zbe8BKi5HpzvYf0,8254
38
+ clutterxp/duplicates/__init__.py,sha256=i-adUo4yDpNMLisjpbENHrR_zmuDu7SvqPHmhK_4HoM,533
39
+ clutterxp/duplicates/detector.py,sha256=4W1vIrB29futYc8NMp7D3Zise51BQn6fPO_QpM07rQU,16032
40
+ clutterxp/duplicates/hashcache.py,sha256=Di-EmjG0WNjM6TlOiQBiHaI1ex1x_r8x1bqZtsszy-Y,5453
41
+ clutterxp/duplicates/namenorm.py,sha256=nhLopI3RwrJ_BMJtcjxKtoaVDnMuPA9jrE_4rDTfVEc,1392
42
+ clutterxp/duplicates/quarantine.py,sha256=UfTx_KQ-PsnQI9E9iMvdsCLpUcrhF4OONC4E8pP3RfI,5576
43
+ clutterxp/duplicates/report.py,sha256=KMv0D5DmkVzEXVdqAnQ4Byi0JrMJPLUAttNrWJA3JiM,3083
44
+ clutterxp/gui/__init__.py,sha256=7QxwlVXIQEeLiLGnAkJLIk1iUavZzuGBKWeHqwpS3Hs,42
45
+ clutterxp/gui/api.py,sha256=mLzBLj0at6JsJt75Qips8KB_qR7W2D_FOue05z-6KLU,17665
46
+ clutterxp/gui/app.py,sha256=ENE3kbky8sVivfH4rXn8Mww3nPXJ2tqGelw3C3AdFTU,6421
47
+ clutterxp/gui/models_qt.py,sha256=LIqDaE5AznHqEVHRctoXPCSmDHKCFfWR-xx-6v1gepI,17813
48
+ clutterxp/gui/workers.py,sha256=skDvoM-3uOd6u1Tgg1V9hgdnV2xpEteENxRpDk9cMxM,4255
49
+ clutterxp/gui/views/__init__.py,sha256=Izm4c6Zf2AHgEIAUBJUAJSi3rS_fEoATgWgENWbTmqQ,467
50
+ clutterxp/gui/views/dashboard.py,sha256=PG-3jk4MyaCl6hlcsHQkitPaqfjfxzSizXTV79gTEOw,9225
51
+ clutterxp/gui/views/duplicates.py,sha256=oHritzt-KXSTA-vchKGpbcJFEJ7bRgklYFEZ-S7WHr4,15187
52
+ clutterxp/gui/views/folders.py,sha256=vctAOrFCFLsHR8WY7KOkWF1vcOOy5xALH4ZUOsC2KM4,12207
53
+ clutterxp/gui/views/history.py,sha256=vZ3nBt2v-2Oec4la2vP3frrXtpOSeCcoQ7qqnLI4jYM,10099
54
+ clutterxp/gui/views/preview.py,sha256=sfSmg-IRWFNq6D8Hm3Pzs53awvYDWRpADGuTMEcneRw,10348
55
+ clutterxp/gui/views/rules.py,sha256=bW6BdAQuJhdQwDUckw2aS0_b0mfQGvG18D9qA064_zo,19370
56
+ clutterxp/gui/views/settings.py,sha256=mK8qKor-SLXIyRCVkD3z9SjfIv7quRnvY806EuddSV4,6065
57
+ clutterxp/history/__init__.py,sha256=WdRFucyXxETQwkHKkTmlba8r5BVLkXrCYpIl0ZwDzvo,538
58
+ clutterxp/history/db.py,sha256=gCxCWmxxnnZDkozDQDf4UpQcnDsEkHYS73AQVvpH-Cc,5498
59
+ clutterxp/history/pathcodec.py,sha256=vCl7-6HWltKsc78CWjKH_OaA5CjKOSD4Og3osvyQru0,1213
60
+ clutterxp/history/recovery.py,sha256=lpElb_Cp8tsA4L9MOleFPU3VMKZt_Yxs_XrntESyOr8,9712
61
+ clutterxp/history/schema.sql,sha256=t3bSAssLH7VxIUmNjA1KAJKg9KbF9IBX0eRECakukXM,1227
62
+ clutterxp/history/service.py,sha256=LzCicUCAwccZ9p5A5C5GY1ZBRWo6-EuBz2mf1GFvbMs,17902
63
+ clutterxp/history/undo.py,sha256=VefzKrXoX7LzCXTJxFDm5GHKhir6vxBSdlZgwbSwyjc,13360
64
+ clutterxp/mover/__init__.py,sha256=OFdgHyXAXVlWxlFjcEo3eC-m7-5YqCgCyK85jASBYio,571
65
+ clutterxp/mover/collision.py,sha256=BGRY9KgYJubnBlOH_cmr5exns1LYUIOmz3A7rsRb_5E,1469
66
+ clutterxp/mover/fsops.py,sha256=4e5T3tQIK3CPHsJCYAeLVAoK5cQ-M5yOCkSinzpTILI,5303
67
+ clutterxp/mover/safe_move.py,sha256=usP0ow31Z4JT-W65ssRwPGoxNdQUbbHCe2O4-VwDMqw,27382
68
+ clutterxp/rules/__init__.py,sha256=L-IUtlHyqhkEjASeygbYSTj5NVfzeJzXZYUsBxhM4Lo,519
69
+ clutterxp/rules/engine.py,sha256=_7XzJc7glN08Gdmny2nULjgwlwIGqBHwt3LSTarSQW4,14722
70
+ clutterxp/rules/matcher.py,sha256=GnyRs-mLKTT7U1t4RLER35mnJg_9y5Rg2PIXP412zh4,7160
71
+ clutterxp/rules/template.py,sha256=vvWXFHX9wgUd0azUA6FHfpYW_IiqEwrz_0wY7ZUdBfc,5808
72
+ clutterxp/rules/tokenizer.py,sha256=otlEG2ZBvnIXZMOwmRCdmtQJzxOAFOB8ZaBlHXPx_XY,3879
73
+ clutterxp/watch/__init__.py,sha256=38V9Gjgxurl7PmfQbh9SaaIubMPZNGrkLYilS8h7bkg,848
74
+ clutterxp/watch/debounce.py,sha256=fNEZ1O9pMDdoCp5uEWEoZVqZtphhaXhDEgs0rTVDz1I,2538
75
+ clutterxp/watch/events.py,sha256=H4slrysVVf0J4qOZ-0fdQ2AlJDxL7WIivY2651I8NWA,7462
76
+ clutterxp/watch/ignore.py,sha256=TY_jZAQw_JBJQELFBvO1YWOeUVnKaeQbG3w8Ojj3_54,2195
77
+ clutterxp/watch/service.py,sha256=iL_SB-xNokv-rqk7MIxab2YytghRbHjgFBvxxJjKFr8,26064
78
+ clutterxp/watch/settle.py,sha256=mUFG7veiZTjp9Nfb8_SGdju-ZrWV1lBW8M50QMsjBSU,5647
79
+ clutterxp/config.example.yaml,sha256=oRYl2HKG0S7ZqBSJCsolswMEjGfPxdTLM2xG8Ccgq3E,2523
80
+ clutterxp/packaging/clutterxp.service,sha256=f_7idpR_RHNKtyrUCvaIU9EVBvVVB2tyhQL5D4xTUbs,384
81
+ clutterxp-1.1.0.dist-info/METADATA,sha256=wESJZQdHFfDwX_msK1SVCeACmlPGjyridEi0vQUOe4I,5465
82
+ clutterxp-1.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
83
+ clutterxp-1.1.0.dist-info/entry_points.txt,sha256=xrAi5zTDu-MJW46ubjRlMsL0r7V3TYfxRTdycFCvmeM,136
84
+ clutterxp-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ clutterxp = clutterxp.cli.app:main
3
+ cxp = clutterxp.cli.app:main
4
+
5
+ [gui_scripts]
6
+ clutterxp-gui = clutterxp.gui.app:main