sortmeout 1.0.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.
- sortmeout/__init__.py +23 -0
- sortmeout/app.py +618 -0
- sortmeout/cli.py +550 -0
- sortmeout/config/__init__.py +11 -0
- sortmeout/config/manager.py +313 -0
- sortmeout/config/settings.py +201 -0
- sortmeout/core/__init__.py +21 -0
- sortmeout/core/action.py +889 -0
- sortmeout/core/condition.py +672 -0
- sortmeout/core/engine.py +421 -0
- sortmeout/core/rule.py +254 -0
- sortmeout/core/watcher.py +471 -0
- sortmeout/gui/__init__.py +10 -0
- sortmeout/gui/app.py +325 -0
- sortmeout/macos/__init__.py +19 -0
- sortmeout/macos/spotlight.py +337 -0
- sortmeout/macos/tags.py +308 -0
- sortmeout/macos/trash.py +449 -0
- sortmeout/utils/__init__.py +12 -0
- sortmeout/utils/file_info.py +363 -0
- sortmeout/utils/logger.py +214 -0
- sortmeout-1.0.0.dist-info/METADATA +302 -0
- sortmeout-1.0.0.dist-info/RECORD +27 -0
- sortmeout-1.0.0.dist-info/WHEEL +5 -0
- sortmeout-1.0.0.dist-info/entry_points.txt +3 -0
- sortmeout-1.0.0.dist-info/licenses/LICENSE +21 -0
- sortmeout-1.0.0.dist-info/top_level.txt +1 -0
sortmeout/cli.py
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for SortMeOut.
|
|
3
|
+
|
|
4
|
+
Provides a full-featured CLI for managing file automation rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.syntax import Syntax
|
|
20
|
+
from rich import print as rprint
|
|
21
|
+
|
|
22
|
+
from sortmeout import SortMeOut, Rule, Condition, Action, __version__
|
|
23
|
+
from sortmeout.config.manager import ConfigManager
|
|
24
|
+
from sortmeout.utils.logger import setup_logging
|
|
25
|
+
from sortmeout.macos.trash import get_trash_info, empty_trash, TrashManager
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.group()
|
|
31
|
+
@click.version_option(__version__, prog_name="SortMeOut")
|
|
32
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
|
|
33
|
+
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def main(ctx, verbose: bool, config: Optional[str]):
|
|
36
|
+
"""
|
|
37
|
+
SortMeOut - Automated file organization for macOS.
|
|
38
|
+
|
|
39
|
+
Watch folders and organize files automatically based on rules you define.
|
|
40
|
+
"""
|
|
41
|
+
ctx.ensure_object(dict)
|
|
42
|
+
ctx.obj["verbose"] = verbose
|
|
43
|
+
ctx.obj["config"] = config
|
|
44
|
+
|
|
45
|
+
if verbose:
|
|
46
|
+
setup_logging(level="DEBUG")
|
|
47
|
+
else:
|
|
48
|
+
setup_logging(level="INFO")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@main.command()
|
|
52
|
+
@click.option("--preview", "-p", is_flag=True, help="Preview mode (don't execute actions)")
|
|
53
|
+
@click.option("--foreground", "-f", is_flag=True, help="Run in foreground")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def start(ctx, preview: bool, foreground: bool):
|
|
56
|
+
"""Start watching configured folders."""
|
|
57
|
+
app = SortMeOut(
|
|
58
|
+
config_path=ctx.obj.get("config"),
|
|
59
|
+
preview_mode=preview,
|
|
60
|
+
verbose=ctx.obj.get("verbose", False),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
folders = app.get_folders()
|
|
64
|
+
if not folders:
|
|
65
|
+
console.print("[yellow]No folders configured. Use 'sortmeout folder add' to add folders.[/yellow]")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
console.print(f"[green]Starting SortMeOut...[/green]")
|
|
69
|
+
console.print(f"Watching {len(folders)} folder(s)")
|
|
70
|
+
|
|
71
|
+
if preview:
|
|
72
|
+
console.print("[yellow]Preview mode: Actions will not be executed[/yellow]")
|
|
73
|
+
|
|
74
|
+
for folder in folders:
|
|
75
|
+
rules = app.get_rules(folder)
|
|
76
|
+
console.print(f" • {folder} ({len(rules)} rules)")
|
|
77
|
+
|
|
78
|
+
if foreground:
|
|
79
|
+
console.print("\nPress Ctrl+C to stop...")
|
|
80
|
+
try:
|
|
81
|
+
app.start()
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
console.print("\n[yellow]Stopping...[/yellow]")
|
|
84
|
+
app.stop()
|
|
85
|
+
else:
|
|
86
|
+
thread = app.start_background()
|
|
87
|
+
console.print("[green]SortMeOut running in background[/green]")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@main.command()
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def stop(ctx):
|
|
93
|
+
"""Stop watching folders."""
|
|
94
|
+
console.print("[yellow]Stopping SortMeOut...[/yellow]")
|
|
95
|
+
# In a real implementation, this would communicate with a running daemon
|
|
96
|
+
console.print("[green]Stopped[/green]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@main.command()
|
|
100
|
+
@click.pass_context
|
|
101
|
+
def status(ctx):
|
|
102
|
+
"""Show current status."""
|
|
103
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
104
|
+
|
|
105
|
+
folders = app.get_folders()
|
|
106
|
+
stats = app.get_stats()
|
|
107
|
+
|
|
108
|
+
# Status panel
|
|
109
|
+
console.print(Panel.fit(
|
|
110
|
+
f"[bold]SortMeOut Status[/bold]\n\n"
|
|
111
|
+
f"Folders: {len(folders)}\n"
|
|
112
|
+
f"Running: {'Yes' if app.is_running else 'No'}\n"
|
|
113
|
+
f"Preview Mode: {'Yes' if app.preview_mode else 'No'}",
|
|
114
|
+
title="Status",
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
# Folders table
|
|
118
|
+
if folders:
|
|
119
|
+
table = Table(title="Watched Folders")
|
|
120
|
+
table.add_column("Folder", style="cyan")
|
|
121
|
+
table.add_column("Rules", justify="right")
|
|
122
|
+
|
|
123
|
+
for folder in folders:
|
|
124
|
+
rules = app.get_rules(folder)
|
|
125
|
+
table.add_row(folder, str(len(rules)))
|
|
126
|
+
|
|
127
|
+
console.print(table)
|
|
128
|
+
else:
|
|
129
|
+
console.print("[yellow]No folders configured[/yellow]")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Folder management commands
|
|
133
|
+
@main.group()
|
|
134
|
+
def folder():
|
|
135
|
+
"""Manage watched folders."""
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@folder.command("add")
|
|
140
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
141
|
+
@click.option("--recursive", "-r", is_flag=True, help="Watch subdirectories")
|
|
142
|
+
@click.pass_context
|
|
143
|
+
def folder_add(ctx, path: str, recursive: bool):
|
|
144
|
+
"""Add a folder to watch."""
|
|
145
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
if app.add_folder(path, recursive=recursive):
|
|
149
|
+
console.print(f"[green]Added folder: {path}[/green]")
|
|
150
|
+
if recursive:
|
|
151
|
+
console.print(" (watching subdirectories)")
|
|
152
|
+
else:
|
|
153
|
+
console.print(f"[yellow]Folder already being watched: {path}[/yellow]")
|
|
154
|
+
except ValueError as e:
|
|
155
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@folder.command("remove")
|
|
159
|
+
@click.argument("path")
|
|
160
|
+
@click.pass_context
|
|
161
|
+
def folder_remove(ctx, path: str):
|
|
162
|
+
"""Remove a folder from watching."""
|
|
163
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
164
|
+
|
|
165
|
+
if app.remove_folder(path):
|
|
166
|
+
console.print(f"[green]Removed folder: {path}[/green]")
|
|
167
|
+
else:
|
|
168
|
+
console.print(f"[red]Folder not found: {path}[/red]")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@folder.command("list")
|
|
172
|
+
@click.pass_context
|
|
173
|
+
def folder_list(ctx):
|
|
174
|
+
"""List all watched folders."""
|
|
175
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
176
|
+
|
|
177
|
+
folders = app.get_folders()
|
|
178
|
+
|
|
179
|
+
if not folders:
|
|
180
|
+
console.print("[yellow]No folders configured[/yellow]")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
table = Table(title="Watched Folders")
|
|
184
|
+
table.add_column("Path", style="cyan")
|
|
185
|
+
table.add_column("Rules", justify="right")
|
|
186
|
+
|
|
187
|
+
for folder in folders:
|
|
188
|
+
rules = app.get_rules(folder)
|
|
189
|
+
table.add_row(folder, str(len(rules)))
|
|
190
|
+
|
|
191
|
+
console.print(table)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@folder.command("process")
|
|
195
|
+
@click.argument("path")
|
|
196
|
+
@click.option("--preview", "-p", is_flag=True, help="Preview only")
|
|
197
|
+
@click.pass_context
|
|
198
|
+
def folder_process(ctx, path: str, preview: bool):
|
|
199
|
+
"""Process all existing files in a folder."""
|
|
200
|
+
app = SortMeOut(config_path=ctx.obj.get("config"), preview_mode=preview)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
result = app.process_folder(path)
|
|
204
|
+
console.print(f"[green]Processed {result['processed']} files[/green]")
|
|
205
|
+
except ValueError as e:
|
|
206
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Rule management commands
|
|
210
|
+
@main.group()
|
|
211
|
+
def rule():
|
|
212
|
+
"""Manage rules."""
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@rule.command("add")
|
|
217
|
+
@click.argument("folder")
|
|
218
|
+
@click.argument("name")
|
|
219
|
+
@click.option("--condition", "-c", multiple=True, help="Condition (attribute:operator:value)")
|
|
220
|
+
@click.option("--action", "-a", multiple=True, help="Action (type:param=value)")
|
|
221
|
+
@click.pass_context
|
|
222
|
+
def rule_add(ctx, folder: str, name: str, condition: tuple, action: tuple):
|
|
223
|
+
"""Add a rule to a folder."""
|
|
224
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
225
|
+
|
|
226
|
+
# Parse conditions
|
|
227
|
+
conditions = []
|
|
228
|
+
for cond_str in condition:
|
|
229
|
+
parts = cond_str.split(":", 2)
|
|
230
|
+
if len(parts) >= 2:
|
|
231
|
+
attr, op = parts[0], parts[1]
|
|
232
|
+
value = parts[2] if len(parts) > 2 else ""
|
|
233
|
+
conditions.append(Condition(attr, op, value))
|
|
234
|
+
|
|
235
|
+
# Parse actions
|
|
236
|
+
actions = []
|
|
237
|
+
for action_str in action:
|
|
238
|
+
if ":" in action_str:
|
|
239
|
+
action_type, params_str = action_str.split(":", 1)
|
|
240
|
+
params = {}
|
|
241
|
+
for param in params_str.split(","):
|
|
242
|
+
if "=" in param:
|
|
243
|
+
key, value = param.split("=", 1)
|
|
244
|
+
params[key.strip()] = value.strip()
|
|
245
|
+
actions.append(Action(action_type, **params))
|
|
246
|
+
else:
|
|
247
|
+
actions.append(Action(action_str))
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
rule = Rule(name=name, conditions=conditions, actions=actions)
|
|
251
|
+
|
|
252
|
+
if app.add_rule(folder, rule):
|
|
253
|
+
console.print(f"[green]Added rule '{name}' to {folder}[/green]")
|
|
254
|
+
else:
|
|
255
|
+
console.print(f"[yellow]Rule '{name}' already exists[/yellow]")
|
|
256
|
+
except ValueError as e:
|
|
257
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@rule.command("remove")
|
|
261
|
+
@click.argument("folder")
|
|
262
|
+
@click.argument("name")
|
|
263
|
+
@click.pass_context
|
|
264
|
+
def rule_remove(ctx, folder: str, name: str):
|
|
265
|
+
"""Remove a rule from a folder."""
|
|
266
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
267
|
+
|
|
268
|
+
if app.remove_rule(folder, name):
|
|
269
|
+
console.print(f"[green]Removed rule '{name}' from {folder}[/green]")
|
|
270
|
+
else:
|
|
271
|
+
console.print(f"[red]Rule not found: {name}[/red]")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@rule.command("list")
|
|
275
|
+
@click.argument("folder")
|
|
276
|
+
@click.pass_context
|
|
277
|
+
def rule_list(ctx, folder: str):
|
|
278
|
+
"""List rules for a folder."""
|
|
279
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
280
|
+
|
|
281
|
+
rules = app.get_rules(folder)
|
|
282
|
+
|
|
283
|
+
if not rules:
|
|
284
|
+
console.print(f"[yellow]No rules for folder: {folder}[/yellow]")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
table = Table(title=f"Rules for {folder}")
|
|
288
|
+
table.add_column("#", justify="right", style="dim")
|
|
289
|
+
table.add_column("Name", style="cyan")
|
|
290
|
+
table.add_column("Enabled", justify="center")
|
|
291
|
+
table.add_column("Conditions")
|
|
292
|
+
table.add_column("Actions")
|
|
293
|
+
|
|
294
|
+
for i, rule in enumerate(rules, 1):
|
|
295
|
+
enabled = "✓" if rule.enabled else "✗"
|
|
296
|
+
conditions = str(len(rule.conditions))
|
|
297
|
+
actions = str(len(rule.actions))
|
|
298
|
+
table.add_row(str(i), rule.name, enabled, conditions, actions)
|
|
299
|
+
|
|
300
|
+
console.print(table)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@rule.command("show")
|
|
304
|
+
@click.argument("folder")
|
|
305
|
+
@click.argument("name")
|
|
306
|
+
@click.pass_context
|
|
307
|
+
def rule_show(ctx, folder: str, name: str):
|
|
308
|
+
"""Show details of a rule."""
|
|
309
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
310
|
+
|
|
311
|
+
rules = app.get_rules(folder)
|
|
312
|
+
rule = next((r for r in rules if r.name == name), None)
|
|
313
|
+
|
|
314
|
+
if not rule:
|
|
315
|
+
console.print(f"[red]Rule not found: {name}[/red]")
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Rule details
|
|
319
|
+
console.print(Panel.fit(
|
|
320
|
+
f"[bold]{rule.name}[/bold]\n\n"
|
|
321
|
+
f"Enabled: {'Yes' if rule.enabled else 'No'}\n"
|
|
322
|
+
f"Match Mode: {rule.match_mode.value}\n"
|
|
323
|
+
f"Continue Processing: {'Yes' if rule.continue_processing else 'No'}\n"
|
|
324
|
+
f"Description: {rule.description or 'None'}",
|
|
325
|
+
title="Rule Details",
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
# Conditions
|
|
329
|
+
if rule.conditions:
|
|
330
|
+
console.print("\n[bold]Conditions:[/bold]")
|
|
331
|
+
for i, cond in enumerate(rule.conditions, 1):
|
|
332
|
+
console.print(f" {i}. {cond}")
|
|
333
|
+
|
|
334
|
+
# Actions
|
|
335
|
+
if rule.actions:
|
|
336
|
+
console.print("\n[bold]Actions:[/bold]")
|
|
337
|
+
for i, action in enumerate(rule.actions, 1):
|
|
338
|
+
console.print(f" {i}. {action}")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@rule.command("enable")
|
|
342
|
+
@click.argument("folder")
|
|
343
|
+
@click.argument("name")
|
|
344
|
+
@click.pass_context
|
|
345
|
+
def rule_enable(ctx, folder: str, name: str):
|
|
346
|
+
"""Enable a rule."""
|
|
347
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
348
|
+
|
|
349
|
+
rules = app.get_rules(folder)
|
|
350
|
+
rule = next((r for r in rules if r.name == name), None)
|
|
351
|
+
|
|
352
|
+
if not rule:
|
|
353
|
+
console.print(f"[red]Rule not found: {name}[/red]")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
rule.enabled = True
|
|
357
|
+
app.update_rule(folder, name, rule)
|
|
358
|
+
console.print(f"[green]Enabled rule: {name}[/green]")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@rule.command("disable")
|
|
362
|
+
@click.argument("folder")
|
|
363
|
+
@click.argument("name")
|
|
364
|
+
@click.pass_context
|
|
365
|
+
def rule_disable(ctx, folder: str, name: str):
|
|
366
|
+
"""Disable a rule."""
|
|
367
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
368
|
+
|
|
369
|
+
rules = app.get_rules(folder)
|
|
370
|
+
rule = next((r for r in rules if r.name == name), None)
|
|
371
|
+
|
|
372
|
+
if not rule:
|
|
373
|
+
console.print(f"[red]Rule not found: {name}[/red]")
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
rule.enabled = False
|
|
377
|
+
app.update_rule(folder, name, rule)
|
|
378
|
+
console.print(f"[yellow]Disabled rule: {name}[/yellow]")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@rule.command("export")
|
|
382
|
+
@click.argument("folder")
|
|
383
|
+
@click.argument("output", type=click.Path())
|
|
384
|
+
@click.pass_context
|
|
385
|
+
def rule_export(ctx, folder: str, output: str):
|
|
386
|
+
"""Export rules to a file."""
|
|
387
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
388
|
+
|
|
389
|
+
if app.export_rules(folder, output):
|
|
390
|
+
console.print(f"[green]Exported rules to: {output}[/green]")
|
|
391
|
+
else:
|
|
392
|
+
console.print(f"[red]Failed to export rules[/red]")
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@rule.command("import")
|
|
396
|
+
@click.argument("folder")
|
|
397
|
+
@click.argument("input", type=click.Path(exists=True))
|
|
398
|
+
@click.pass_context
|
|
399
|
+
def rule_import(ctx, folder: str, input: str):
|
|
400
|
+
"""Import rules from a file."""
|
|
401
|
+
app = SortMeOut(config_path=ctx.obj.get("config"))
|
|
402
|
+
|
|
403
|
+
count = app.import_rules(folder, input)
|
|
404
|
+
console.print(f"[green]Imported {count} rules[/green]")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Trash management commands
|
|
408
|
+
@main.group()
|
|
409
|
+
def trash():
|
|
410
|
+
"""Manage Trash."""
|
|
411
|
+
pass
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@trash.command("status")
|
|
415
|
+
def trash_status():
|
|
416
|
+
"""Show Trash status."""
|
|
417
|
+
info = get_trash_info()
|
|
418
|
+
|
|
419
|
+
console.print(Panel.fit(
|
|
420
|
+
f"[bold]Trash Status[/bold]\n\n"
|
|
421
|
+
f"Items: {info.item_count}\n"
|
|
422
|
+
f"Size: {info.size_human}\n"
|
|
423
|
+
f"Oldest: {info.oldest_item_date.strftime('%Y-%m-%d') if info.oldest_item_date else 'N/A'}\n"
|
|
424
|
+
f"Newest: {info.newest_item_date.strftime('%Y-%m-%d') if info.newest_item_date else 'N/A'}",
|
|
425
|
+
title="Trash",
|
|
426
|
+
))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@trash.command("empty")
|
|
430
|
+
@click.option("--secure", "-s", is_flag=True, help="Secure empty")
|
|
431
|
+
@click.confirmation_option(prompt="Are you sure you want to empty the Trash?")
|
|
432
|
+
def trash_empty(secure: bool):
|
|
433
|
+
"""Empty the Trash."""
|
|
434
|
+
if empty_trash(secure=secure):
|
|
435
|
+
console.print("[green]Trash emptied[/green]")
|
|
436
|
+
else:
|
|
437
|
+
console.print("[red]Failed to empty Trash[/red]")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@trash.command("clean")
|
|
441
|
+
@click.option("--days", "-d", default=30, help="Delete items older than days")
|
|
442
|
+
@click.option("--size", "-s", default=10.0, help="Maximum size in GB")
|
|
443
|
+
def trash_clean(days: int, size: float):
|
|
444
|
+
"""Clean Trash based on age/size limits."""
|
|
445
|
+
manager = TrashManager(max_age_days=days, max_size_gb=size)
|
|
446
|
+
result = manager.run_cleanup()
|
|
447
|
+
|
|
448
|
+
deleted_count = len(result["deleted_by_age"]) + len(result["deleted_by_size"])
|
|
449
|
+
console.print(f"[green]Deleted {deleted_count} items[/green]")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# Config management commands
|
|
453
|
+
@main.group()
|
|
454
|
+
def config():
|
|
455
|
+
"""Manage configuration."""
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@config.command("show")
|
|
460
|
+
@click.pass_context
|
|
461
|
+
def config_show(ctx):
|
|
462
|
+
"""Show current configuration."""
|
|
463
|
+
manager = ConfigManager(ctx.obj.get("config"))
|
|
464
|
+
cfg = manager.load_config()
|
|
465
|
+
|
|
466
|
+
syntax = Syntax(json.dumps(cfg, indent=2, default=str), "json")
|
|
467
|
+
console.print(syntax)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@config.command("export")
|
|
471
|
+
@click.argument("output", type=click.Path())
|
|
472
|
+
@click.pass_context
|
|
473
|
+
def config_export(ctx, output: str):
|
|
474
|
+
"""Export configuration to file."""
|
|
475
|
+
manager = ConfigManager(ctx.obj.get("config"))
|
|
476
|
+
|
|
477
|
+
if manager.export_config(output):
|
|
478
|
+
console.print(f"[green]Configuration exported to: {output}[/green]")
|
|
479
|
+
else:
|
|
480
|
+
console.print("[red]Failed to export configuration[/red]")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@config.command("import")
|
|
484
|
+
@click.argument("input", type=click.Path(exists=True))
|
|
485
|
+
@click.option("--merge", "-m", is_flag=True, help="Merge with existing config")
|
|
486
|
+
@click.pass_context
|
|
487
|
+
def config_import(ctx, input: str, merge: bool):
|
|
488
|
+
"""Import configuration from file."""
|
|
489
|
+
manager = ConfigManager(ctx.obj.get("config"))
|
|
490
|
+
|
|
491
|
+
if manager.import_config(input, merge=merge):
|
|
492
|
+
console.print(f"[green]Configuration imported from: {input}[/green]")
|
|
493
|
+
else:
|
|
494
|
+
console.print("[red]Failed to import configuration[/red]")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@config.command("reset")
|
|
498
|
+
@click.confirmation_option(prompt="Are you sure you want to reset configuration?")
|
|
499
|
+
@click.pass_context
|
|
500
|
+
def config_reset(ctx):
|
|
501
|
+
"""Reset configuration to defaults."""
|
|
502
|
+
manager = ConfigManager(ctx.obj.get("config"))
|
|
503
|
+
|
|
504
|
+
if manager.reset_config():
|
|
505
|
+
console.print("[green]Configuration reset to defaults[/green]")
|
|
506
|
+
else:
|
|
507
|
+
console.print("[red]Failed to reset configuration[/red]")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Test command
|
|
511
|
+
@main.command("test")
|
|
512
|
+
@click.argument("file", type=click.Path(exists=True))
|
|
513
|
+
@click.argument("folder")
|
|
514
|
+
@click.option("--preview", "-p", is_flag=True, default=True, help="Preview mode")
|
|
515
|
+
@click.pass_context
|
|
516
|
+
def test_file(ctx, file: str, folder: str, preview: bool):
|
|
517
|
+
"""Test rules against a file."""
|
|
518
|
+
app = SortMeOut(config_path=ctx.obj.get("config"), preview_mode=preview)
|
|
519
|
+
|
|
520
|
+
from sortmeout.utils.file_info import get_file_info
|
|
521
|
+
from sortmeout.core.engine import RuleEngine
|
|
522
|
+
|
|
523
|
+
# Get file info
|
|
524
|
+
file_info = get_file_info(file)
|
|
525
|
+
console.print(Panel.fit(
|
|
526
|
+
f"[bold]File: {os.path.basename(file)}[/bold]\n\n"
|
|
527
|
+
f"Size: {file_info.get('size_human', 'N/A')}\n"
|
|
528
|
+
f"Extension: {file_info.get('extension', 'N/A')}\n"
|
|
529
|
+
f"Kind: {file_info.get('kind', 'N/A')}",
|
|
530
|
+
title="File Info",
|
|
531
|
+
))
|
|
532
|
+
|
|
533
|
+
# Test rules
|
|
534
|
+
rules = app.get_rules(folder)
|
|
535
|
+
engine = RuleEngine(preview_mode=True)
|
|
536
|
+
|
|
537
|
+
console.print("\n[bold]Rule Matching:[/bold]")
|
|
538
|
+
for rule in rules:
|
|
539
|
+
matches = engine.evaluate_rule(rule, file, file_info)
|
|
540
|
+
status = "[green]✓ MATCH[/green]" if matches else "[dim]✗ No match[/dim]"
|
|
541
|
+
console.print(f" {rule.name}: {status}")
|
|
542
|
+
|
|
543
|
+
if matches:
|
|
544
|
+
console.print(" [bold]Actions would execute:[/bold]")
|
|
545
|
+
for action in rule.actions:
|
|
546
|
+
console.print(f" • {action}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
if __name__ == "__main__":
|
|
550
|
+
main()
|