notepad-cleanup 0.2.1__tar.gz → 0.2.3__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.
- notepad_cleanup-0.2.3/PKG-INFO +208 -0
- notepad_cleanup-0.2.3/README.md +165 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/_version.py +2 -2
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/cli.py +99 -6
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/dedup.py +81 -7
- notepad_cleanup-0.2.3/notepad_cleanup/organizer.py +571 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/prompts/organize.md +2 -1
- notepad_cleanup-0.2.3/notepad_cleanup.egg-info/PKG-INFO +208 -0
- notepad_cleanup-0.2.1/PKG-INFO +0 -299
- notepad_cleanup-0.2.1/README.md +0 -256
- notepad_cleanup-0.2.1/notepad_cleanup/organizer.py +0 -281
- notepad_cleanup-0.2.1/notepad_cleanup.egg-info/PKG-INFO +0 -299
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/LICENSE +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/__init__.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/__main__.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/config.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/discovery.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/extractor.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup/saver.py +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup.egg-info/SOURCES.txt +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup.egg-info/dependency_links.txt +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup.egg-info/entry_points.txt +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup.egg-info/requires.txt +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/notepad_cleanup.egg-info/top_level.txt +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/pyproject.toml +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/setup.cfg +0 -0
- {notepad_cleanup-0.2.1 → notepad_cleanup-0.2.3}/setup.py +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notepad-cleanup
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Extract and organize text from Windows 11 Notepad tabs using AI
|
|
5
|
+
Home-page: https://github.com/DazzleTools/notepad-cleanup
|
|
6
|
+
Author: djdarcy
|
|
7
|
+
Author-email: djdarcy <6962246+djdarcy@users.noreply.github.com>
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Project-URL: Homepage, https://github.com/DazzleTools/notepad-cleanup
|
|
10
|
+
Project-URL: Repository, https://github.com/DazzleTools/notepad-cleanup
|
|
11
|
+
Project-URL: Issues, https://github.com/DazzleTools/notepad-cleanup/issues
|
|
12
|
+
Project-URL: Discussions, https://github.com/DazzleTools/notepad-cleanup/discussions
|
|
13
|
+
Project-URL: Changelog, https://github.com/DazzleTools/notepad-cleanup/blob/main/CHANGELOG.md
|
|
14
|
+
Keywords: windows,notepad,extraction,organization,ai,dedup,tabs,clipboard
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
18
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Classifier: Topic :: Text Processing
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: pywinauto>=0.6.9
|
|
32
|
+
Requires-Dist: pywin32>=306
|
|
33
|
+
Requires-Dist: psutil>=5.9.0
|
|
34
|
+
Requires-Dist: click>=8.1.0
|
|
35
|
+
Requires-Dist: rich>=13.0.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
39
|
+
Dynamic: author
|
|
40
|
+
Dynamic: home-page
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
Dynamic: requires-python
|
|
43
|
+
|
|
44
|
+
# notepad-cleanup
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/notepad-cleanup/)
|
|
47
|
+
[](https://github.com/DazzleTools/notepad-cleanup/releases)
|
|
48
|
+
[](https://www.python.org/downloads/)
|
|
49
|
+
[](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
50
|
+
[](https://dazzletools.github.io/notepad-cleanup/stats/#installs)
|
|
51
|
+
[](https://github.com/DazzleTools/notepad-cleanup/discussions)
|
|
52
|
+
[](https://github.com/DazzleTools/notepad-cleanup)
|
|
53
|
+
|
|
54
|
+
Extract and organize text from all open Windows 11 Notepad tabs using AI-powered categorization.
|
|
55
|
+
|
|
56
|
+
## What It Does
|
|
57
|
+
|
|
58
|
+
Windows 11 Notepad supports multiple tabs, making it easy to accumulate dozens of text snippets, code fragments, notes, and temporary data across multiple windows. **notepad-cleanup** extracts all that text in one command, deduplicates it against previous sessions, and organizes it into categorized folders using AI.
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install notepad-cleanup
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For virtual environments, source installs, and Claude Code CLI setup, see [docs/install.md](docs/install.md).
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
notepad-cleanup extract # Extract all Notepad tabs
|
|
70
|
+
notepad-cleanup compare --last --link auto # Find and link duplicates
|
|
71
|
+
notepad-cleanup organize --last # AI categorization (duplicates become symlinks)
|
|
72
|
+
notepad-cleanup links separate --last # Optional: split out linked files to see only new content
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
- **Two-phase extraction** -- Silent `WM_GETTEXT` for loaded tabs, UI Automation for unloaded tabs
|
|
78
|
+
- **Cross-session deduplication** -- Compare against historical sessions with exact and [fuzzy matching](docs/fuzzy-matching.md)
|
|
79
|
+
- **Filesystem linking** -- Replace duplicates with hardlinks, symlinks, or DazzleLink descriptors
|
|
80
|
+
- **Link-aware organization** -- AI categorizes all files, but duplicates become symlinks in `organized/` instead of copies. Preserves the connection network back to canonical sources
|
|
81
|
+
- **Separate/join links** -- Split `organized/` into new files vs linked duplicates, or rejoin them
|
|
82
|
+
- **Configuration system** -- Unified folder registry with [`...` notation](docs/config.md), MRU history, persistent settings
|
|
83
|
+
- **Diff integration** -- Auto-generated scripts for Beyond Compare, WinMerge, VS Code, etc.
|
|
84
|
+
|
|
85
|
+
## Usage
|
|
86
|
+
|
|
87
|
+
### First-time setup
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
notepad-cleanup config add "C:\Users\YourName\Desktop\Notepad Organize"
|
|
91
|
+
notepad-cleanup config set search "...1"
|
|
92
|
+
notepad-cleanup config set diff_tool bcomp
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Daily workflow
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
notepad-cleanup extract # 1. Extract all tabs
|
|
99
|
+
notepad-cleanup compare --last # 2. Find duplicates
|
|
100
|
+
notepad-cleanup diff --last # 3. Spot-check in diff tool
|
|
101
|
+
notepad-cleanup compare --last --link auto # 4. Link duplicates
|
|
102
|
+
notepad-cleanup organize --last # 5. AI categorization (dupes become symlinks)
|
|
103
|
+
notepad-cleanup links separate --last # 6. Optional: see only new files
|
|
104
|
+
notepad-cleanup links join --last # 7. Optional: rejoin everything
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
After setup, `--last` auto-uses the most recent extraction. No path copy-pasting.
|
|
108
|
+
|
|
109
|
+
### Commands
|
|
110
|
+
|
|
111
|
+
| Command | Purpose |
|
|
112
|
+
|---------|---------|
|
|
113
|
+
| `extract` | Extract text from all open Notepad windows/tabs |
|
|
114
|
+
| `compare` | Find duplicates across historical sessions |
|
|
115
|
+
| `organize` | AI-powered categorization (symlinks for duplicates, copies for new) |
|
|
116
|
+
| `links` | Separate linked files from organized/, or rejoin them |
|
|
117
|
+
| `diff` | Launch diff script to spot-check matched pairs |
|
|
118
|
+
| `config` | Manage folders, search dirs, diff tool, settings |
|
|
119
|
+
| `run` | Extract + organize in one step |
|
|
120
|
+
|
|
121
|
+
For full parameter documentation, see [docs/parameters.md](docs/parameters.md).
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
| Doc | Contents |
|
|
126
|
+
|-----|----------|
|
|
127
|
+
| [Parameters](docs/parameters.md) | Full command reference with all options |
|
|
128
|
+
| [Configuration](docs/config.md) | Folder registry, `...` notation, MRU, search dirs |
|
|
129
|
+
| [Fuzzy Matching](docs/fuzzy-matching.md) | Threshold formula, derivation, customization |
|
|
130
|
+
|
|
131
|
+
## Output Structure
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
notepad-cleanup/nc-2026-03-16__08-15-30/
|
|
135
|
+
├── manifest.json # Extraction metadata
|
|
136
|
+
├── window01/
|
|
137
|
+
│ ├── tab01.txt # Raw extracted files
|
|
138
|
+
│ ├── tab02.txt
|
|
139
|
+
│ └── tab03.txt
|
|
140
|
+
├── window02/
|
|
141
|
+
│ └── tab01.txt
|
|
142
|
+
├── organized/ # AI-organized output (after organize step)
|
|
143
|
+
│ ├── code-snippets/
|
|
144
|
+
│ │ ├── process-data.py
|
|
145
|
+
│ │ └── batch-rename.bat
|
|
146
|
+
│ ├── personal-notes/
|
|
147
|
+
│ │ └── grocery-list.txt
|
|
148
|
+
│ └── _summary.md # Organization summary
|
|
149
|
+
├── _compare_results.json # Dedup comparison cache
|
|
150
|
+
├── _compare_diffs.cmd # Diff script for spot-checking
|
|
151
|
+
├── _dedup_links.json # Link manifest (if --link used)
|
|
152
|
+
├── _organize_prompt.md # AI prompt used
|
|
153
|
+
└── _organize_log.txt # Claude CLI output
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## How It Works
|
|
157
|
+
|
|
158
|
+
### Phase 1: Silent Extraction
|
|
159
|
+
|
|
160
|
+
Uses `WM_GETTEXT` message to read text from `RichEditD2DPT` child windows. This is completely silent and invisible -- no focus changes, no window activation, no disruption to your workflow.
|
|
161
|
+
|
|
162
|
+
**Limitation:** Only works for tabs that have been loaded (visited) at least once in the current Notepad session. Unloaded tabs have no `RichEditD2DPT` control yet, so they cannot be read silently.
|
|
163
|
+
|
|
164
|
+
### Phase 2: Tab Switching (Announced)
|
|
165
|
+
|
|
166
|
+
For unloaded tabs, uses UI Automation (`TabItem.Select()`) to activate each tab, which forces Windows to load the `RichEditD2DPT` control. Once loaded, the same `WM_GETTEXT` method reads the content.
|
|
167
|
+
|
|
168
|
+
**Warning:** This steals focus and activates Notepad windows. The tool warns you before Phase 2 starts and waits for confirmation. Do not type or click during Phase 2.
|
|
169
|
+
|
|
170
|
+
### Organization with AI
|
|
171
|
+
|
|
172
|
+
After extraction, Claude Code CLI:
|
|
173
|
+
1. Reads `manifest.json` to understand the collection
|
|
174
|
+
2. Reads each extracted file to determine content type
|
|
175
|
+
3. Returns a JSON plan with categories and renamed filenames
|
|
176
|
+
4. The tool executes the plan locally (copy files to organized folders)
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
180
|
+
- **Windows 11** (uses Windows 11 Notepad tab features)
|
|
181
|
+
- **Python 3.10+**
|
|
182
|
+
- **Claude Code CLI** (optional, for organize step)
|
|
183
|
+
|
|
184
|
+
For detailed installation instructions, see [docs/install.md](docs/install.md).
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
git clone https://github.com/DazzleTools/notepad-cleanup.git
|
|
190
|
+
cd notepad-cleanup
|
|
191
|
+
python -m venv venv
|
|
192
|
+
venv\Scripts\activate
|
|
193
|
+
pip install -e .
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Contributing
|
|
197
|
+
|
|
198
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
199
|
+
|
|
200
|
+
Like the project?
|
|
201
|
+
|
|
202
|
+
[](https://www.buymeacoffee.com/djdarcy)
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
notepad-cleanup, Copyright (C) 2026 Dustin Darcy.
|
|
207
|
+
|
|
208
|
+
This project is licensed under the GNU General Public License v3.0 — see [LICENSE](LICENSE) for full details.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# notepad-cleanup
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/notepad-cleanup/)
|
|
4
|
+
[](https://github.com/DazzleTools/notepad-cleanup/releases)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](https://www.gnu.org/licenses/gpl-3.0.html)
|
|
7
|
+
[](https://dazzletools.github.io/notepad-cleanup/stats/#installs)
|
|
8
|
+
[](https://github.com/DazzleTools/notepad-cleanup/discussions)
|
|
9
|
+
[](https://github.com/DazzleTools/notepad-cleanup)
|
|
10
|
+
|
|
11
|
+
Extract and organize text from all open Windows 11 Notepad tabs using AI-powered categorization.
|
|
12
|
+
|
|
13
|
+
## What It Does
|
|
14
|
+
|
|
15
|
+
Windows 11 Notepad supports multiple tabs, making it easy to accumulate dozens of text snippets, code fragments, notes, and temporary data across multiple windows. **notepad-cleanup** extracts all that text in one command, deduplicates it against previous sessions, and organizes it into categorized folders using AI.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install notepad-cleanup
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For virtual environments, source installs, and Claude Code CLI setup, see [docs/install.md](docs/install.md).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
notepad-cleanup extract # Extract all Notepad tabs
|
|
27
|
+
notepad-cleanup compare --last --link auto # Find and link duplicates
|
|
28
|
+
notepad-cleanup organize --last # AI categorization (duplicates become symlinks)
|
|
29
|
+
notepad-cleanup links separate --last # Optional: split out linked files to see only new content
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Two-phase extraction** -- Silent `WM_GETTEXT` for loaded tabs, UI Automation for unloaded tabs
|
|
35
|
+
- **Cross-session deduplication** -- Compare against historical sessions with exact and [fuzzy matching](docs/fuzzy-matching.md)
|
|
36
|
+
- **Filesystem linking** -- Replace duplicates with hardlinks, symlinks, or DazzleLink descriptors
|
|
37
|
+
- **Link-aware organization** -- AI categorizes all files, but duplicates become symlinks in `organized/` instead of copies. Preserves the connection network back to canonical sources
|
|
38
|
+
- **Separate/join links** -- Split `organized/` into new files vs linked duplicates, or rejoin them
|
|
39
|
+
- **Configuration system** -- Unified folder registry with [`...` notation](docs/config.md), MRU history, persistent settings
|
|
40
|
+
- **Diff integration** -- Auto-generated scripts for Beyond Compare, WinMerge, VS Code, etc.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### First-time setup
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
notepad-cleanup config add "C:\Users\YourName\Desktop\Notepad Organize"
|
|
48
|
+
notepad-cleanup config set search "...1"
|
|
49
|
+
notepad-cleanup config set diff_tool bcomp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Daily workflow
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
notepad-cleanup extract # 1. Extract all tabs
|
|
56
|
+
notepad-cleanup compare --last # 2. Find duplicates
|
|
57
|
+
notepad-cleanup diff --last # 3. Spot-check in diff tool
|
|
58
|
+
notepad-cleanup compare --last --link auto # 4. Link duplicates
|
|
59
|
+
notepad-cleanup organize --last # 5. AI categorization (dupes become symlinks)
|
|
60
|
+
notepad-cleanup links separate --last # 6. Optional: see only new files
|
|
61
|
+
notepad-cleanup links join --last # 7. Optional: rejoin everything
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
After setup, `--last` auto-uses the most recent extraction. No path copy-pasting.
|
|
65
|
+
|
|
66
|
+
### Commands
|
|
67
|
+
|
|
68
|
+
| Command | Purpose |
|
|
69
|
+
|---------|---------|
|
|
70
|
+
| `extract` | Extract text from all open Notepad windows/tabs |
|
|
71
|
+
| `compare` | Find duplicates across historical sessions |
|
|
72
|
+
| `organize` | AI-powered categorization (symlinks for duplicates, copies for new) |
|
|
73
|
+
| `links` | Separate linked files from organized/, or rejoin them |
|
|
74
|
+
| `diff` | Launch diff script to spot-check matched pairs |
|
|
75
|
+
| `config` | Manage folders, search dirs, diff tool, settings |
|
|
76
|
+
| `run` | Extract + organize in one step |
|
|
77
|
+
|
|
78
|
+
For full parameter documentation, see [docs/parameters.md](docs/parameters.md).
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
| Doc | Contents |
|
|
83
|
+
|-----|----------|
|
|
84
|
+
| [Parameters](docs/parameters.md) | Full command reference with all options |
|
|
85
|
+
| [Configuration](docs/config.md) | Folder registry, `...` notation, MRU, search dirs |
|
|
86
|
+
| [Fuzzy Matching](docs/fuzzy-matching.md) | Threshold formula, derivation, customization |
|
|
87
|
+
|
|
88
|
+
## Output Structure
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
notepad-cleanup/nc-2026-03-16__08-15-30/
|
|
92
|
+
├── manifest.json # Extraction metadata
|
|
93
|
+
├── window01/
|
|
94
|
+
│ ├── tab01.txt # Raw extracted files
|
|
95
|
+
│ ├── tab02.txt
|
|
96
|
+
│ └── tab03.txt
|
|
97
|
+
├── window02/
|
|
98
|
+
│ └── tab01.txt
|
|
99
|
+
├── organized/ # AI-organized output (after organize step)
|
|
100
|
+
│ ├── code-snippets/
|
|
101
|
+
│ │ ├── process-data.py
|
|
102
|
+
│ │ └── batch-rename.bat
|
|
103
|
+
│ ├── personal-notes/
|
|
104
|
+
│ │ └── grocery-list.txt
|
|
105
|
+
│ └── _summary.md # Organization summary
|
|
106
|
+
├── _compare_results.json # Dedup comparison cache
|
|
107
|
+
├── _compare_diffs.cmd # Diff script for spot-checking
|
|
108
|
+
├── _dedup_links.json # Link manifest (if --link used)
|
|
109
|
+
├── _organize_prompt.md # AI prompt used
|
|
110
|
+
└── _organize_log.txt # Claude CLI output
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## How It Works
|
|
114
|
+
|
|
115
|
+
### Phase 1: Silent Extraction
|
|
116
|
+
|
|
117
|
+
Uses `WM_GETTEXT` message to read text from `RichEditD2DPT` child windows. This is completely silent and invisible -- no focus changes, no window activation, no disruption to your workflow.
|
|
118
|
+
|
|
119
|
+
**Limitation:** Only works for tabs that have been loaded (visited) at least once in the current Notepad session. Unloaded tabs have no `RichEditD2DPT` control yet, so they cannot be read silently.
|
|
120
|
+
|
|
121
|
+
### Phase 2: Tab Switching (Announced)
|
|
122
|
+
|
|
123
|
+
For unloaded tabs, uses UI Automation (`TabItem.Select()`) to activate each tab, which forces Windows to load the `RichEditD2DPT` control. Once loaded, the same `WM_GETTEXT` method reads the content.
|
|
124
|
+
|
|
125
|
+
**Warning:** This steals focus and activates Notepad windows. The tool warns you before Phase 2 starts and waits for confirmation. Do not type or click during Phase 2.
|
|
126
|
+
|
|
127
|
+
### Organization with AI
|
|
128
|
+
|
|
129
|
+
After extraction, Claude Code CLI:
|
|
130
|
+
1. Reads `manifest.json` to understand the collection
|
|
131
|
+
2. Reads each extracted file to determine content type
|
|
132
|
+
3. Returns a JSON plan with categories and renamed filenames
|
|
133
|
+
4. The tool executes the plan locally (copy files to organized folders)
|
|
134
|
+
|
|
135
|
+
## Requirements
|
|
136
|
+
|
|
137
|
+
- **Windows 11** (uses Windows 11 Notepad tab features)
|
|
138
|
+
- **Python 3.10+**
|
|
139
|
+
- **Claude Code CLI** (optional, for organize step)
|
|
140
|
+
|
|
141
|
+
For detailed installation instructions, see [docs/install.md](docs/install.md).
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git clone https://github.com/DazzleTools/notepad-cleanup.git
|
|
147
|
+
cd notepad-cleanup
|
|
148
|
+
python -m venv venv
|
|
149
|
+
venv\Scripts\activate
|
|
150
|
+
pip install -e .
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Contributing
|
|
154
|
+
|
|
155
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
156
|
+
|
|
157
|
+
Like the project?
|
|
158
|
+
|
|
159
|
+
[](https://www.buymeacoffee.com/djdarcy)
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
notepad-cleanup, Copyright (C) 2026 Dustin Darcy.
|
|
164
|
+
|
|
165
|
+
This project is licensed under the GNU General Public License v3.0 — see [LICENSE](LICENSE) for full details.
|
|
@@ -21,13 +21,13 @@ Version levels:
|
|
|
21
21
|
# Version components - edit these for version bumps
|
|
22
22
|
MAJOR = 0
|
|
23
23
|
MINOR = 2
|
|
24
|
-
PATCH =
|
|
24
|
+
PATCH = 3
|
|
25
25
|
PHASE = None # Per-MINOR feature set: None, "alpha", "beta", "rc1", etc.
|
|
26
26
|
PRE_RELEASE_NUM = 0 # PEP 440 pre-release number (e.g., a1, b2)
|
|
27
27
|
PROJECT_PHASE = "prealpha" # Project-wide: "prealpha", "alpha", "beta", "stable"
|
|
28
28
|
|
|
29
29
|
# Auto-updated by git hooks - do not edit manually
|
|
30
|
-
__version__ = "0.2.
|
|
30
|
+
__version__ = "0.2.3_main_16-20260317-af4f15c0"
|
|
31
31
|
__app_name__ = "notepad-cleanup"
|
|
32
32
|
|
|
33
33
|
|
|
@@ -13,13 +13,15 @@ from .discovery import find_notepad_windows, get_richedit_children, get_tab_coun
|
|
|
13
13
|
from .extractor import extract_phase1, extract_phase2, merge_results
|
|
14
14
|
from .saver import save_extraction
|
|
15
15
|
from .organizer import (generate_prompt, invoke_claude_cli, save_prompt_to_file,
|
|
16
|
-
find_claude_cli, parse_plan, execute_plan
|
|
16
|
+
find_claude_cli, parse_plan, execute_plan,
|
|
17
|
+
separate_links, join_links)
|
|
17
18
|
from .dedup import (find_session_dirs, build_hash_index, find_duplicates,
|
|
18
19
|
find_content_files,
|
|
19
20
|
generate_unified_diff, near_match_threshold,
|
|
20
21
|
resolve_diff_tool, launch_diff_tool, create_links,
|
|
21
22
|
write_link_manifest, generate_diff_script,
|
|
22
23
|
save_compare_results, load_compare_results,
|
|
24
|
+
get_linked_paths,
|
|
23
25
|
LINK_STRATEGIES, CACHE_FILENAME)
|
|
24
26
|
from .config import (
|
|
25
27
|
load_config, save_config, config_get, config_set, config_unset,
|
|
@@ -546,8 +548,14 @@ def organize(folder, last, backend, dry_run, verbose):
|
|
|
546
548
|
|
|
547
549
|
console.print("\n[bold]Notepad Cleanup -- Organize[/bold]\n")
|
|
548
550
|
|
|
549
|
-
#
|
|
550
|
-
|
|
551
|
+
# Load dedup context if available (for link-aware organize)
|
|
552
|
+
linked_paths = get_linked_paths(folder)
|
|
553
|
+
if linked_paths:
|
|
554
|
+
console.print(f" [dim]Found {len(linked_paths)} dedup-linked files "
|
|
555
|
+
f"(will symlink, not copy)[/dim]")
|
|
556
|
+
|
|
557
|
+
# Generate prompt (linked files excluded from AI task)
|
|
558
|
+
prompt = generate_prompt(manifest_path, linked_paths=linked_paths)
|
|
551
559
|
|
|
552
560
|
if backend == "prompt-only" or dry_run:
|
|
553
561
|
prompt_file = save_prompt_to_file(prompt, folder)
|
|
@@ -606,12 +614,14 @@ def organize(folder, last, backend, dry_run, verbose):
|
|
|
606
614
|
|
|
607
615
|
console.print(f" Claude proposed a plan for [bold]{len(plan)}[/bold] files\n")
|
|
608
616
|
|
|
609
|
-
# Step 3: Execute the plan locally
|
|
617
|
+
# Step 3: Execute the plan locally (link-aware)
|
|
610
618
|
with console.status("Organizing files..."):
|
|
611
|
-
summary, stats = execute_plan(plan, folder)
|
|
619
|
+
summary, stats = execute_plan(plan, folder, linked_paths=linked_paths)
|
|
612
620
|
|
|
613
621
|
console.print("[bold green]Organization complete![/bold green]\n")
|
|
614
|
-
console.print(f" Files
|
|
622
|
+
console.print(f" Files copied: {stats['copied']}")
|
|
623
|
+
if stats.get('linked', 0):
|
|
624
|
+
console.print(f" Files linked: {stats['linked']} (symlink/hardlink to canonical)")
|
|
615
625
|
if stats['errors']:
|
|
616
626
|
console.print(f" [red]Errors: {stats['errors']}[/red]")
|
|
617
627
|
|
|
@@ -1011,6 +1021,89 @@ def diff_cmd(folder, last):
|
|
|
1011
1021
|
console.print(f" You can run it manually: {script_path}\n")
|
|
1012
1022
|
|
|
1013
1023
|
|
|
1024
|
+
@main.command("links")
|
|
1025
|
+
@click.argument("action", type=click.Choice(["separate", "join"]))
|
|
1026
|
+
@click.argument("folder", type=click.Path(), required=False, default=None)
|
|
1027
|
+
@click.option("--last", is_flag=True, help="Use the most recent extraction")
|
|
1028
|
+
@click.option("--dir-name", default="organized-links",
|
|
1029
|
+
help="Name for the links directory (default: organized-links)")
|
|
1030
|
+
@click.option("--dry-run", is_flag=True, help="Preview without moving files")
|
|
1031
|
+
def links_cmd(action, folder, last, dir_name, dry_run):
|
|
1032
|
+
"""Separate or rejoin linked files in organized/.
|
|
1033
|
+
|
|
1034
|
+
\b
|
|
1035
|
+
separate: Move linked files from organized/ into a parallel tree,
|
|
1036
|
+
preserving category structure. Shows only new files in organized/.
|
|
1037
|
+
join: Move linked files back from the parallel tree into organized/,
|
|
1038
|
+
restoring the full collection.
|
|
1039
|
+
|
|
1040
|
+
\b
|
|
1041
|
+
Examples:
|
|
1042
|
+
notepad-cleanup links separate --last Split links out
|
|
1043
|
+
notepad-cleanup links separate --last --dry-run Preview the split
|
|
1044
|
+
notepad-cleanup links join --last Rejoin links
|
|
1045
|
+
notepad-cleanup links join --last --dry-run Preview the rejoin
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
folder = resolve_folder(folder, use_last=last)
|
|
1049
|
+
if folder is None:
|
|
1050
|
+
console.print("[red]No extraction folder specified.[/red]")
|
|
1051
|
+
console.print(" Provide a FOLDER argument or use --last.")
|
|
1052
|
+
return
|
|
1053
|
+
folder = Path(folder)
|
|
1054
|
+
if not folder.exists():
|
|
1055
|
+
console.print(f"[red]Folder does not exist: {folder}[/red]")
|
|
1056
|
+
return
|
|
1057
|
+
|
|
1058
|
+
if last:
|
|
1059
|
+
console.print(f"\n [dim]Using last extraction: {folder}[/dim]")
|
|
1060
|
+
|
|
1061
|
+
organized_dir = folder / "organized"
|
|
1062
|
+
links_dir = folder / dir_name
|
|
1063
|
+
|
|
1064
|
+
if action == "separate":
|
|
1065
|
+
if not organized_dir.exists():
|
|
1066
|
+
console.print(f"\n [yellow]No organized/ folder found in {folder}[/yellow]")
|
|
1067
|
+
console.print(" Run 'notepad-cleanup organize' first.\n")
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
label = "Separate Links -- Dry Run" if dry_run else "Separate Links"
|
|
1071
|
+
console.print(f"\n[bold]{label}[/bold]\n")
|
|
1072
|
+
console.print(f" From: {organized_dir}")
|
|
1073
|
+
console.print(f" To: {links_dir}\n")
|
|
1074
|
+
|
|
1075
|
+
stats, details = separate_links(organized_dir, links_dir_name=dir_name, dry_run=dry_run)
|
|
1076
|
+
|
|
1077
|
+
console.print(f" Links moved: {stats['moved']}")
|
|
1078
|
+
console.print(f" Real files: {stats['real_kept']}")
|
|
1079
|
+
if stats['errors']:
|
|
1080
|
+
console.print(f" [red]Errors: {stats['errors']}[/red]")
|
|
1081
|
+
|
|
1082
|
+
elif action == "join":
|
|
1083
|
+
if not links_dir.exists():
|
|
1084
|
+
console.print(f"\n [yellow]No {dir_name}/ folder found in {folder}[/yellow]")
|
|
1085
|
+
console.print(" Run 'notepad-cleanup links separate' first.\n")
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
label = "Join Links -- Dry Run" if dry_run else "Join Links"
|
|
1089
|
+
console.print(f"\n[bold]{label}[/bold]\n")
|
|
1090
|
+
console.print(f" From: {links_dir}")
|
|
1091
|
+
console.print(f" To: {organized_dir}\n")
|
|
1092
|
+
|
|
1093
|
+
stats, details = join_links(organized_dir, links_dir_name=dir_name, dry_run=dry_run)
|
|
1094
|
+
|
|
1095
|
+
console.print(f" Links restored: {stats['moved']}")
|
|
1096
|
+
if stats['errors']:
|
|
1097
|
+
console.print(f" [red]Errors: {stats['errors']}[/red]")
|
|
1098
|
+
|
|
1099
|
+
if details:
|
|
1100
|
+
console.print()
|
|
1101
|
+
for d in details:
|
|
1102
|
+
console.print(d)
|
|
1103
|
+
|
|
1104
|
+
console.print()
|
|
1105
|
+
|
|
1106
|
+
|
|
1014
1107
|
@main.command()
|
|
1015
1108
|
@click.option("--output-dir", "-o", type=click.Path(), default=None,
|
|
1016
1109
|
help="Output directory")
|
|
@@ -1062,24 +1062,59 @@ def _create_single_link(match: DedupMatch, strategy: str, backup: bool) -> LinkR
|
|
|
1062
1062
|
def _create_dazzlelink_file(link_path: Path, target_path: Path):
|
|
1063
1063
|
"""Create a .dazzlelink JSON descriptor file.
|
|
1064
1064
|
|
|
1065
|
-
This is a
|
|
1066
|
-
|
|
1065
|
+
This is a cross-platform alternative when native symlinks aren't
|
|
1066
|
+
available. The file is a JSON descriptor compatible with the DazzleLink
|
|
1067
|
+
tool (https://github.com/DazzleTools/dazzlelink). Default mode is "open"
|
|
1068
|
+
so double-clicking opens the target in its native application.
|
|
1067
1069
|
"""
|
|
1068
1070
|
from datetime import datetime
|
|
1071
|
+
import time
|
|
1072
|
+
|
|
1073
|
+
resolved_target = target_path.resolve()
|
|
1074
|
+
now = datetime.now()
|
|
1075
|
+
now_ts = time.time()
|
|
1076
|
+
|
|
1077
|
+
# Get target file info
|
|
1078
|
+
target_exists = resolved_target.exists()
|
|
1079
|
+
target_size = 0
|
|
1080
|
+
target_ext = resolved_target.suffix
|
|
1081
|
+
target_timestamps = {}
|
|
1082
|
+
if target_exists:
|
|
1083
|
+
try:
|
|
1084
|
+
stat = resolved_target.stat()
|
|
1085
|
+
target_size = stat.st_size
|
|
1086
|
+
target_timestamps = {
|
|
1087
|
+
"created": stat.st_ctime,
|
|
1088
|
+
"modified": stat.st_mtime,
|
|
1089
|
+
"accessed": stat.st_atime,
|
|
1090
|
+
"created_iso": datetime.fromtimestamp(stat.st_ctime).isoformat(),
|
|
1091
|
+
"modified_iso": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
|
1092
|
+
"accessed_iso": datetime.fromtimestamp(stat.st_atime).isoformat(),
|
|
1093
|
+
}
|
|
1094
|
+
except OSError:
|
|
1095
|
+
pass
|
|
1069
1096
|
|
|
1070
1097
|
dazzlelink_data = {
|
|
1071
1098
|
"schema_version": 1,
|
|
1072
|
-
"created_by": "notepad-cleanup
|
|
1073
|
-
"
|
|
1099
|
+
"created_by": "notepad-cleanup",
|
|
1100
|
+
"creation_timestamp": now_ts,
|
|
1101
|
+
"creation_date": now.isoformat(),
|
|
1074
1102
|
"link": {
|
|
1075
1103
|
"original_path": str(link_path),
|
|
1076
|
-
"target_path": str(
|
|
1104
|
+
"target_path": str(resolved_target),
|
|
1077
1105
|
"type": "dazzlelink",
|
|
1078
1106
|
"relative_path": False,
|
|
1079
1107
|
},
|
|
1080
1108
|
"target": {
|
|
1081
|
-
"exists":
|
|
1082
|
-
"
|
|
1109
|
+
"exists": target_exists,
|
|
1110
|
+
"type": "file",
|
|
1111
|
+
"size": target_size,
|
|
1112
|
+
"extension": target_ext,
|
|
1113
|
+
"timestamps": target_timestamps,
|
|
1114
|
+
},
|
|
1115
|
+
"config": {
|
|
1116
|
+
"default_mode": "open",
|
|
1117
|
+
"platform": _os.name,
|
|
1083
1118
|
},
|
|
1084
1119
|
"context": {
|
|
1085
1120
|
"reason": "dedup_exact_match",
|
|
@@ -1127,6 +1162,45 @@ def write_link_manifest(results: list, output_dir: Path):
|
|
|
1127
1162
|
return manifest_path
|
|
1128
1163
|
|
|
1129
1164
|
|
|
1165
|
+
LINK_MANIFEST_FILENAME = "_dedup_links.json"
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def load_link_manifest(session_dir: Path) -> dict:
|
|
1169
|
+
"""Load the dedup link manifest from a session directory.
|
|
1170
|
+
|
|
1171
|
+
Returns the parsed manifest dict, or an empty structure if no manifest exists.
|
|
1172
|
+
The returned dict always has a "links" key (list of link entries).
|
|
1173
|
+
"""
|
|
1174
|
+
manifest_path = Path(session_dir) / LINK_MANIFEST_FILENAME
|
|
1175
|
+
if not manifest_path.exists():
|
|
1176
|
+
return {"links": []}
|
|
1177
|
+
try:
|
|
1178
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
1179
|
+
if "links" not in data:
|
|
1180
|
+
data["links"] = []
|
|
1181
|
+
return data
|
|
1182
|
+
except (json.JSONDecodeError, OSError):
|
|
1183
|
+
return {"links": []}
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def get_linked_paths(session_dir: Path) -> dict:
|
|
1187
|
+
"""Return a mapping of linked new_path -> canonical_path for successful links.
|
|
1188
|
+
|
|
1189
|
+
Keys are resolved Path objects (the window*/tab*.txt files that were linked).
|
|
1190
|
+
Values are resolved Path objects pointing to the canonical (provenance root) file.
|
|
1191
|
+
Returns empty dict if no manifest or no successful links.
|
|
1192
|
+
"""
|
|
1193
|
+
manifest = load_link_manifest(session_dir)
|
|
1194
|
+
linked = {}
|
|
1195
|
+
for entry in manifest.get("links", []):
|
|
1196
|
+
if not entry.get("success"):
|
|
1197
|
+
continue
|
|
1198
|
+
new_path = Path(entry["new_path"]).resolve()
|
|
1199
|
+
canonical_path = Path(entry["canonical_path"]).resolve()
|
|
1200
|
+
linked[new_path] = canonical_path
|
|
1201
|
+
return linked
|
|
1202
|
+
|
|
1203
|
+
|
|
1130
1204
|
# --- Diff tools ---
|
|
1131
1205
|
|
|
1132
1206
|
# Known diff tools in preference order (name, command pattern).
|