clutterxp 1.1.0__tar.gz → 1.2.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.
- {clutterxp-1.1.0 → clutterxp-1.2.0}/PKG-INFO +10 -10
- {clutterxp-1.1.0 → clutterxp-1.2.0}/README.md +9 -9
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/__init__.py +1 -1
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/app.py +9 -2
- clutterxp-1.2.0/src/clutterxp/gui/theme.py +378 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/dashboard.py +45 -23
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/duplicates.py +23 -5
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/folders.py +18 -3
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/history.py +29 -6
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/preview.py +35 -9
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/rules.py +74 -12
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/settings.py +12 -2
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/workers.py +11 -3
- {clutterxp-1.1.0 → clutterxp-1.2.0}/.github/workflows/ci.yml +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/.gitignore +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/CHANGELOG.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/config.example.yaml +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/00_PROJECT_PLAN.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/01_ARCHITECTURE.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/02_RULE_ENGINE.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/03_CONFIG.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/04_SAFE_MOVER.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/05_HISTORY_UNDO.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/06_WATCHER.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/07_DUPLICATES.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/08_CLI.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/09_SAFETY.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/10_TESTING.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/11_PACKAGING.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/12_GUI.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/13_ROADMAP.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/14_DECISIONS.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/15_ACCEPTANCE.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/16_RISKS.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/.gitkeep +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/acceptance_matrix.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/perf_report.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/security_review.md +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/packaging/clutterxp.service +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/pyproject.toml +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/.gitkeep +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/check_contracts.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/check_ownership.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/contracts.lock.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/e2e.sh +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/smoke_readme.sh +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/app.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_config.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_doctor.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_duplicates.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_gui.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_history.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_init.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_organize.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_rules.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_scan.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_undo.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_watch.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/errors.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/render.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/jsonschema.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/loader.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/placeholders.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/runtime.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/schema.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/validate.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/writer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/detector.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/hashcache.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/namenorm.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/quarantine.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/report.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/api.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/models_qt.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/db.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/pathcodec.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/recovery.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/schema.sql +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/undo.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/interfaces.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/locking.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/logging_setup.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/models.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/collision.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/fsops.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/safe_move.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/organizer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/paths.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/planner.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/py.typed +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/engine.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/matcher.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/template.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/tokenizer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/safety.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/scanner.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/service_install.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/units.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/debounce.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/events.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/ignore.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/settle.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/.gitkeep +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/doctor.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/duplicates.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/history.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/organize.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/rules_list.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/rules_test.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/scan.json +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_config.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_doctor.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_duplicates.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_general.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_gui.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_history.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_init.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_organize.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_rules.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_scan.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_undo.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_watch.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_jsonschema_and_logging.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_loader.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_paths.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_placeholders.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_runtime.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_validate.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_writer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_detector.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_hashcache.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_namenorm.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_quarantine.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A81_gui_launches_and_cli_never_imports_qt.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A82_dashboard_shows_watcher_and_recent_operations.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A83_folders_view_edits_watch_folders_and_settings.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A84_rules_editor_includes_live_trace.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A85_preview_data_equals_cli_json_plan.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A86_history_view_undo_shows_exact_refusal_reasons.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A87_duplicates_view_progress_and_cancel.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A88_duplicates_quarantine_gui.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A89_ui_thread_never_blocks_worker_cancellation.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A90_gui_extra_documented.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_api_extra.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_models_qt.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/helpers.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_db.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_pathcodec.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_recovery.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_undo.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_acceptance_sweep.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_fault_injection.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_properties.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_scenarios.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_collision.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_fsops.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_locking.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_safe_move.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_organizer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_planner.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_scanner.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/perf/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/perf/test_perf_budgets.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_engine.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_matcher.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_template.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_tokenizer.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_vectors.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/test_invariants_full.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/test_safety_module.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/test_harness.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/test_service_install.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/__init__.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/conftest.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_acceptance.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_debounce.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_events.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_ignore.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_invariants.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_service.py +0 -0
- {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_settle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: clutterxp
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Configurable, safe, lightweight file organizer: rules, preview, undo, watch, duplicates.
|
|
5
5
|
Keywords: automation,cli,downloads,file-organizer,linux
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -39,18 +39,18 @@ Use the **clean visual desktop app** or run quick commands from your terminal.
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
## Why you can actually trust it
|
|
42
|
+
## 🛡️ Why you can actually trust it
|
|
43
43
|
|
|
44
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
45
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
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
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
53
|
-
## Getting Started
|
|
53
|
+
## ⚡ Getting Started
|
|
54
54
|
|
|
55
55
|
### 1. Installation
|
|
56
56
|
|
|
@@ -101,7 +101,7 @@ clutterxp undo --yes
|
|
|
101
101
|
|
|
102
102
|
## Features that make life easier
|
|
103
103
|
|
|
104
|
-
###
|
|
104
|
+
### Set it and forget it (Background Watcher)
|
|
105
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
106
|
```bash
|
|
107
107
|
clutterxp watch
|
|
@@ -112,13 +112,13 @@ clutterxp service install
|
|
|
112
112
|
clutterxp service status
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
###
|
|
115
|
+
### Find and clean duplicate downloads
|
|
116
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
117
|
```bash
|
|
118
118
|
clutterxp duplicates scan ~/Downloads
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
###
|
|
121
|
+
### Customize your rules in seconds
|
|
122
122
|
Rules are dead simple. Group by keywords, file extensions, size, or age:
|
|
123
123
|
- *"Move anything with `invoice` or `receipt` to `~/Documents/Invoices`"*
|
|
124
124
|
- *"Move `.png` and `.jpg` with `wallpaper` to `~/Pictures/Wallpapers`"*
|
|
@@ -10,18 +10,18 @@ Use the **clean visual desktop app** or run quick commands from your terminal.
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
## Why you can actually trust it
|
|
13
|
+
## 🛡️ Why you can actually trust it
|
|
14
14
|
|
|
15
15
|
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:
|
|
16
16
|
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
17
|
+
- **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.
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **100% private and offline**: Runs completely on your machine. No accounts, no cloud sync, no tracking, and zero internet requests.
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
## Getting Started
|
|
24
|
+
## ⚡ Getting Started
|
|
25
25
|
|
|
26
26
|
### 1. Installation
|
|
27
27
|
|
|
@@ -72,7 +72,7 @@ clutterxp undo --yes
|
|
|
72
72
|
|
|
73
73
|
## Features that make life easier
|
|
74
74
|
|
|
75
|
-
###
|
|
75
|
+
### Set it and forget it (Background Watcher)
|
|
76
76
|
Turn on automatic sorting. Whenever a new download finishes, ClutterXP waits until it's completely written to disk, then quietly sorts it into place.
|
|
77
77
|
```bash
|
|
78
78
|
clutterxp watch
|
|
@@ -83,13 +83,13 @@ clutterxp service install
|
|
|
83
83
|
clutterxp service status
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
###
|
|
86
|
+
### Find and clean duplicate downloads
|
|
87
87
|
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.
|
|
88
88
|
```bash
|
|
89
89
|
clutterxp duplicates scan ~/Downloads
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
###
|
|
92
|
+
### Customize your rules in seconds
|
|
93
93
|
Rules are dead simple. Group by keywords, file extensions, size, or age:
|
|
94
94
|
- *"Move anything with `invoice` or `receipt` to `~/Documents/Invoices`"*
|
|
95
95
|
- *"Move `.png` and `.jpg` with `wallpaper` to `~/Pictures/Wallpapers`"*
|
|
@@ -28,6 +28,7 @@ from PySide6.QtWidgets import (
|
|
|
28
28
|
|
|
29
29
|
from clutterxp import paths
|
|
30
30
|
from clutterxp.gui.api import GuiApi
|
|
31
|
+
from clutterxp.gui.theme import apply_theme
|
|
31
32
|
from clutterxp.gui.views import (
|
|
32
33
|
DashboardView,
|
|
33
34
|
DuplicatesView,
|
|
@@ -45,8 +46,13 @@ class ClutterXPMainWindow(QMainWindow):
|
|
|
45
46
|
def __init__(self, api: GuiApi | None = None, parent: QWidget | None = None) -> None:
|
|
46
47
|
super().__init__(parent)
|
|
47
48
|
self.api = api if api is not None else GuiApi()
|
|
48
|
-
self.setWindowTitle("ClutterXP - Intelligent File Organizer")
|
|
49
|
-
self.resize(
|
|
49
|
+
self.setWindowTitle("ClutterXP - Safe, Intelligent File Organizer")
|
|
50
|
+
self.resize(1080, 720)
|
|
51
|
+
self.setMinimumSize(880, 580)
|
|
52
|
+
|
|
53
|
+
app_instance = QApplication.instance()
|
|
54
|
+
if app_instance is not None:
|
|
55
|
+
apply_theme(app_instance)
|
|
50
56
|
|
|
51
57
|
self._init_ui()
|
|
52
58
|
|
|
@@ -181,6 +187,7 @@ def main() -> None:
|
|
|
181
187
|
if app is None:
|
|
182
188
|
app = QApplication(sys.argv)
|
|
183
189
|
app.setApplicationName("ClutterXP")
|
|
190
|
+
apply_theme(app)
|
|
184
191
|
|
|
185
192
|
# Hook up SIGINT to cleanly exit
|
|
186
193
|
signal.signal(signal.SIGINT, lambda *_: app.quit())
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Modern, minimalist, elegant dark theme for ClutterXP GUI.
|
|
2
|
+
|
|
3
|
+
Provides a unified design system with clean typography, balanced spacing,
|
|
4
|
+
sleek card surfaces, modern button hierarchy, and custom scrollbars.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from PySide6.QtGui import QColor, QPalette
|
|
10
|
+
from PySide6.QtWidgets import QApplication
|
|
11
|
+
|
|
12
|
+
MODERN_STYLESHEET = """
|
|
13
|
+
/* ====================================================================
|
|
14
|
+
ClutterXP Modern Minimalist Theme (Obsidian & Indigo)
|
|
15
|
+
==================================================================== */
|
|
16
|
+
|
|
17
|
+
* {
|
|
18
|
+
outline: none;
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
QMainWindow, QDialog, QWidget {
|
|
23
|
+
background-color: #0f1117;
|
|
24
|
+
color: #f1f5f9;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ==========================================
|
|
29
|
+
Tab Widget & Segmented Tab Bar
|
|
30
|
+
========================================== */
|
|
31
|
+
QTabWidget::pane {
|
|
32
|
+
border: 1px solid #1e2433;
|
|
33
|
+
border-radius: 8px;
|
|
34
|
+
background-color: #121620;
|
|
35
|
+
padding: 6px;
|
|
36
|
+
margin-top: -1px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
QTabBar {
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
qproperty-drawBase: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
QTabBar::tab {
|
|
45
|
+
background-color: #171b26;
|
|
46
|
+
color: #94a3b8;
|
|
47
|
+
border: 1px solid #262d3d;
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
padding: 7px 18px;
|
|
50
|
+
margin: 3px 5px 3px 0px;
|
|
51
|
+
font-size: 13px;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
QTabBar::tab:hover {
|
|
56
|
+
background-color: #1f2637;
|
|
57
|
+
color: #f8fafc;
|
|
58
|
+
border-color: #3b455c;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
QTabBar::tab:selected {
|
|
62
|
+
background-color: #6366f1;
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
border: 1px solid #818cf8;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ==========================================
|
|
69
|
+
Cards & Group Boxes
|
|
70
|
+
========================================== */
|
|
71
|
+
QGroupBox {
|
|
72
|
+
background-color: #161a25;
|
|
73
|
+
border: 1px solid #232a3b;
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
margin-top: 20px;
|
|
76
|
+
padding: 16px 14px 14px 14px;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: #f8fafc;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
QGroupBox::title {
|
|
83
|
+
subcontrol-origin: margin;
|
|
84
|
+
subcontrol-position: top left;
|
|
85
|
+
left: 14px;
|
|
86
|
+
top: 2px;
|
|
87
|
+
padding: 0 6px;
|
|
88
|
+
background-color: #161a25;
|
|
89
|
+
color: #a5b4fc;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
font-weight: 600;
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
letter-spacing: 0.5px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ==========================================
|
|
97
|
+
Buttons
|
|
98
|
+
========================================== */
|
|
99
|
+
QPushButton {
|
|
100
|
+
background-color: #1e2433;
|
|
101
|
+
color: #f1f5f9;
|
|
102
|
+
border: 1px solid #2d374d;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
padding: 6px 14px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
min-height: 22px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
QPushButton:hover {
|
|
111
|
+
background-color: #273044;
|
|
112
|
+
border-color: #404f6d;
|
|
113
|
+
color: #ffffff;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
QPushButton:pressed {
|
|
117
|
+
background-color: #181d29;
|
|
118
|
+
border-color: #6366f1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
QPushButton:disabled {
|
|
122
|
+
background-color: #141720;
|
|
123
|
+
color: #475569;
|
|
124
|
+
border-color: #1d222e;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Specialized Button Variants via Property */
|
|
128
|
+
QPushButton[primary="true"] {
|
|
129
|
+
background-color: #6366f1;
|
|
130
|
+
color: #ffffff;
|
|
131
|
+
border: 1px solid #818cf8;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
QPushButton[primary="true"]:hover {
|
|
136
|
+
background-color: #4f46e5;
|
|
137
|
+
border-color: #a5b4fc;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
QPushButton[primary="true"]:pressed {
|
|
141
|
+
background-color: #4338ca;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
QPushButton[success="true"] {
|
|
145
|
+
background-color: #10b981;
|
|
146
|
+
color: #ffffff;
|
|
147
|
+
border: 1px solid #34d399;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
QPushButton[success="true"]:hover {
|
|
152
|
+
background-color: #059669;
|
|
153
|
+
border-color: #6ee7b7;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
QPushButton[danger="true"] {
|
|
157
|
+
background-color: #e11d48;
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
border: 1px solid #fb7185;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
QPushButton[danger="true"]:hover {
|
|
164
|
+
background-color: #be123c;
|
|
165
|
+
border-color: #fda4af;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ==========================================
|
|
169
|
+
Input Controls
|
|
170
|
+
========================================== */
|
|
171
|
+
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {
|
|
172
|
+
background-color: #0c0f16;
|
|
173
|
+
border: 1px solid #232a3b;
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
padding: 6px 10px;
|
|
176
|
+
color: #f8fafc;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
selection-background-color: #6366f1;
|
|
179
|
+
selection-color: #ffffff;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {
|
|
183
|
+
border: 1px solid #6366f1;
|
|
184
|
+
background-color: #0f131d;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
QComboBox::drop-down {
|
|
188
|
+
border: none;
|
|
189
|
+
width: 24px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
QComboBox QAbstractItemView {
|
|
193
|
+
background-color: #161a25;
|
|
194
|
+
border: 1px solid #232a3b;
|
|
195
|
+
border-radius: 6px;
|
|
196
|
+
selection-background-color: #6366f1;
|
|
197
|
+
selection-color: #ffffff;
|
|
198
|
+
color: #f1f5f9;
|
|
199
|
+
padding: 4px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* ==========================================
|
|
203
|
+
Tables & Lists
|
|
204
|
+
========================================== */
|
|
205
|
+
QTableView, QListWidget, QTextEdit {
|
|
206
|
+
background-color: #0c0f16;
|
|
207
|
+
alternate-background-color: #11151f;
|
|
208
|
+
border: 1px solid #232a3b;
|
|
209
|
+
border-radius: 6px;
|
|
210
|
+
gridline-color: #1a202d;
|
|
211
|
+
color: #f1f5f9;
|
|
212
|
+
selection-background-color: #2e384e;
|
|
213
|
+
selection-color: #ffffff;
|
|
214
|
+
font-size: 13px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
QHeaderView::section {
|
|
218
|
+
background-color: #161a25;
|
|
219
|
+
color: #94a3b8;
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
font-size: 12px;
|
|
222
|
+
padding: 7px 8px;
|
|
223
|
+
border: none;
|
|
224
|
+
border-bottom: 1px solid #232a3b;
|
|
225
|
+
border-right: 1px solid #1a202d;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
QHeaderView::section:last {
|
|
229
|
+
border-right: none;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ==========================================
|
|
233
|
+
Progress Bar
|
|
234
|
+
========================================== */
|
|
235
|
+
QProgressBar {
|
|
236
|
+
border: 1px solid #232a3b;
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
text-align: center;
|
|
239
|
+
background-color: #0c0f16;
|
|
240
|
+
color: #f8fafc;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
font-size: 11px;
|
|
243
|
+
height: 16px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
QProgressBar::chunk {
|
|
247
|
+
background-color: #6366f1;
|
|
248
|
+
border-radius: 5px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ==========================================
|
|
252
|
+
Scroll Bars
|
|
253
|
+
========================================== */
|
|
254
|
+
QScrollBar:vertical {
|
|
255
|
+
background: #0c0f16;
|
|
256
|
+
width: 8px;
|
|
257
|
+
margin: 0;
|
|
258
|
+
border-radius: 4px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
QScrollBar::handle:vertical {
|
|
262
|
+
background: #2b3345;
|
|
263
|
+
min-height: 24px;
|
|
264
|
+
border-radius: 4px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
QScrollBar::handle:vertical:hover {
|
|
268
|
+
background: #475569;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
|
272
|
+
height: 0px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
QScrollBar:horizontal {
|
|
276
|
+
background: #0c0f16;
|
|
277
|
+
height: 8px;
|
|
278
|
+
margin: 0;
|
|
279
|
+
border-radius: 4px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
QScrollBar::handle:horizontal {
|
|
283
|
+
background: #2b3345;
|
|
284
|
+
min-width: 24px;
|
|
285
|
+
border-radius: 4px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
QScrollBar::handle:horizontal:hover {
|
|
289
|
+
background: #475569;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
|
|
293
|
+
width: 0px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* ==========================================
|
|
297
|
+
Status Bar, Menu Bar & Tooltips
|
|
298
|
+
========================================== */
|
|
299
|
+
QStatusBar {
|
|
300
|
+
background-color: #0c0f16;
|
|
301
|
+
border-top: 1px solid #1e2433;
|
|
302
|
+
color: #94a3b8;
|
|
303
|
+
font-size: 12px;
|
|
304
|
+
padding: 4px 8px;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
QMenuBar {
|
|
308
|
+
background-color: #0f1117;
|
|
309
|
+
border-bottom: 1px solid #1e2433;
|
|
310
|
+
color: #cbd5e1;
|
|
311
|
+
padding: 2px 4px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
QMenuBar::item {
|
|
315
|
+
background: transparent;
|
|
316
|
+
padding: 4px 10px;
|
|
317
|
+
border-radius: 4px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
QMenuBar::item:selected {
|
|
321
|
+
background-color: #1e2433;
|
|
322
|
+
color: #f8fafc;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
QMenu {
|
|
326
|
+
background-color: #161a25;
|
|
327
|
+
border: 1px solid #232a3b;
|
|
328
|
+
border-radius: 6px;
|
|
329
|
+
padding: 4px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
QMenu::item {
|
|
333
|
+
padding: 6px 20px;
|
|
334
|
+
border-radius: 4px;
|
|
335
|
+
color: #f1f5f9;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
QMenu::item:selected {
|
|
339
|
+
background-color: #6366f1;
|
|
340
|
+
color: #ffffff;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
QMenu::separator {
|
|
344
|
+
height: 1px;
|
|
345
|
+
background-color: #232a3b;
|
|
346
|
+
margin: 4px 8px;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
QToolTip {
|
|
350
|
+
background-color: #1e2433;
|
|
351
|
+
color: #f8fafc;
|
|
352
|
+
border: 1px solid #334155;
|
|
353
|
+
border-radius: 4px;
|
|
354
|
+
padding: 4px 8px;
|
|
355
|
+
font-size: 12px;
|
|
356
|
+
}
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def apply_theme(app: QApplication) -> None:
|
|
361
|
+
"""Apply the modern dark theme and palette to the QApplication."""
|
|
362
|
+
# Palette configuration
|
|
363
|
+
palette = QPalette()
|
|
364
|
+
palette.setColor(QPalette.ColorRole.Window, QColor("#0f1117"))
|
|
365
|
+
palette.setColor(QPalette.ColorRole.WindowText, QColor("#f1f5f9"))
|
|
366
|
+
palette.setColor(QPalette.ColorRole.Base, QColor("#0c0f16"))
|
|
367
|
+
palette.setColor(QPalette.ColorRole.AlternateBase, QColor("#121620"))
|
|
368
|
+
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor("#1e2433"))
|
|
369
|
+
palette.setColor(QPalette.ColorRole.ToolTipText, QColor("#f8fafc"))
|
|
370
|
+
palette.setColor(QPalette.ColorRole.Text, QColor("#f1f5f9"))
|
|
371
|
+
palette.setColor(QPalette.ColorRole.Button, QColor("#1e2433"))
|
|
372
|
+
palette.setColor(QPalette.ColorRole.ButtonText, QColor("#f1f5f9"))
|
|
373
|
+
palette.setColor(QPalette.ColorRole.BrightText, QColor("#ffffff"))
|
|
374
|
+
palette.setColor(QPalette.ColorRole.Highlight, QColor("#6366f1"))
|
|
375
|
+
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#ffffff"))
|
|
376
|
+
|
|
377
|
+
app.setPalette(palette)
|
|
378
|
+
app.setStyleSheet(MODERN_STYLESHEET)
|
|
@@ -10,7 +10,6 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from PySide6.QtCore import Qt, Signal
|
|
12
12
|
from PySide6.QtWidgets import (
|
|
13
|
-
QFrame,
|
|
14
13
|
QGridLayout,
|
|
15
14
|
QGroupBox,
|
|
16
15
|
QHBoxLayout,
|
|
@@ -45,31 +44,43 @@ class DashboardView(QWidget):
|
|
|
45
44
|
main_layout.setContentsMargins(16, 16, 16, 16)
|
|
46
45
|
main_layout.setSpacing(16)
|
|
47
46
|
|
|
48
|
-
#
|
|
49
|
-
|
|
47
|
+
# Header
|
|
48
|
+
header_layout = QVBoxLayout()
|
|
49
|
+
header_layout.setSpacing(2)
|
|
50
|
+
title_label = QLabel("Dashboard")
|
|
50
51
|
title_font = title_label.font()
|
|
51
|
-
title_font.setPointSize(
|
|
52
|
+
title_font.setPointSize(18)
|
|
52
53
|
title_font.setBold(True)
|
|
53
54
|
title_label.setFont(title_font)
|
|
54
|
-
|
|
55
|
+
title_label.setStyleSheet("color: #f8fafc;")
|
|
56
|
+
header_layout.addWidget(title_label)
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
subtitle_label = QLabel(
|
|
59
|
+
"Overview of file activity, background watcher, and quick controls."
|
|
60
|
+
)
|
|
61
|
+
subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
62
|
+
header_layout.addWidget(subtitle_label)
|
|
63
|
+
main_layout.addLayout(header_layout)
|
|
64
|
+
|
|
65
|
+
# Top row: Watcher Card & Stats Grid & Quick Actions
|
|
57
66
|
top_row = QHBoxLayout()
|
|
58
67
|
top_row.setSpacing(16)
|
|
59
68
|
|
|
60
69
|
# Watcher State Card
|
|
61
70
|
self.watcher_group = QGroupBox("Watcher Daemon")
|
|
62
71
|
watcher_layout = QVBoxLayout(self.watcher_group)
|
|
63
|
-
watcher_layout.setSpacing(
|
|
72
|
+
watcher_layout.setSpacing(10)
|
|
64
73
|
|
|
65
74
|
self.watcher_status_label = QLabel("Status: Checking...")
|
|
66
75
|
status_font = self.watcher_status_label.font()
|
|
67
76
|
status_font.setBold(True)
|
|
68
77
|
self.watcher_status_label.setFont(status_font)
|
|
78
|
+
self.watcher_status_label.setStyleSheet("color: #a5b4fc; font-size: 13px;")
|
|
69
79
|
watcher_layout.addWidget(self.watcher_status_label)
|
|
70
80
|
|
|
71
81
|
self.watcher_folders_label = QLabel("Watched Folders: None")
|
|
72
82
|
self.watcher_folders_label.setWordWrap(True)
|
|
83
|
+
self.watcher_folders_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
|
|
73
84
|
watcher_layout.addWidget(self.watcher_folders_label)
|
|
74
85
|
|
|
75
86
|
self.watcher_toggle_btn = QPushButton("Toggle Watcher")
|
|
@@ -83,19 +94,31 @@ class DashboardView(QWidget):
|
|
|
83
94
|
stats_group = QGroupBox("Quick Stats")
|
|
84
95
|
stats_layout = QGridLayout(stats_group)
|
|
85
96
|
stats_layout.setSpacing(12)
|
|
97
|
+
stats_layout.setContentsMargins(14, 16, 14, 14)
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
lbl_folders = QLabel("Watched Folders:")
|
|
100
|
+
lbl_folders.setStyleSheet("color: #94a3b8;")
|
|
101
|
+
stats_layout.addWidget(lbl_folders, 0, 0)
|
|
88
102
|
self.folders_count_label = QLabel("0")
|
|
103
|
+
self.folders_count_label.setStyleSheet(
|
|
104
|
+
"font-size: 16px; font-weight: bold; color: #818cf8;"
|
|
105
|
+
)
|
|
89
106
|
self.folders_count_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
90
107
|
stats_layout.addWidget(self.folders_count_label, 0, 1)
|
|
91
108
|
|
|
92
|
-
|
|
109
|
+
lbl_today = QLabel("Today's Moves:")
|
|
110
|
+
lbl_today.setStyleSheet("color: #94a3b8;")
|
|
111
|
+
stats_layout.addWidget(lbl_today, 1, 0)
|
|
93
112
|
self.today_moves_label = QLabel("0")
|
|
113
|
+
self.today_moves_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #34d399;")
|
|
94
114
|
self.today_moves_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
95
115
|
stats_layout.addWidget(self.today_moves_label, 1, 1)
|
|
96
116
|
|
|
97
|
-
|
|
117
|
+
lbl_rules = QLabel("Active Rules:")
|
|
118
|
+
lbl_rules.setStyleSheet("color: #94a3b8;")
|
|
119
|
+
stats_layout.addWidget(lbl_rules, 2, 0)
|
|
98
120
|
self.rules_count_label = QLabel("0")
|
|
121
|
+
self.rules_count_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #fbbf24;")
|
|
99
122
|
self.rules_count_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
|
100
123
|
stats_layout.addWidget(self.rules_count_label, 2, 1)
|
|
101
124
|
|
|
@@ -107,15 +130,16 @@ class DashboardView(QWidget):
|
|
|
107
130
|
actions_layout = QVBoxLayout(actions_group)
|
|
108
131
|
actions_layout.setSpacing(8)
|
|
109
132
|
|
|
110
|
-
self.
|
|
111
|
-
self.
|
|
112
|
-
actions_layout.addWidget(self.scan_btn)
|
|
113
|
-
|
|
114
|
-
self.organize_btn = QPushButton("Organize (Preview)")
|
|
133
|
+
self.organize_btn = QPushButton("⚡ Organize (Preview)")
|
|
134
|
+
self.organize_btn.setProperty("primary", True)
|
|
115
135
|
self.organize_btn.clicked.connect(lambda: self.navigate_requested.emit("preview"))
|
|
116
136
|
actions_layout.addWidget(self.organize_btn)
|
|
117
137
|
|
|
118
|
-
self.
|
|
138
|
+
self.scan_btn = QPushButton("🔍 Scan Folders")
|
|
139
|
+
self.scan_btn.clicked.connect(lambda: self.navigate_requested.emit("preview"))
|
|
140
|
+
actions_layout.addWidget(self.scan_btn)
|
|
141
|
+
|
|
142
|
+
self.undo_last_btn = QPushButton("↩️ Undo Last Batch")
|
|
119
143
|
self.undo_last_btn.clicked.connect(self._on_undo_last_batch)
|
|
120
144
|
actions_layout.addWidget(self.undo_last_btn)
|
|
121
145
|
|
|
@@ -124,20 +148,18 @@ class DashboardView(QWidget):
|
|
|
124
148
|
|
|
125
149
|
main_layout.addLayout(top_row)
|
|
126
150
|
|
|
127
|
-
# Separator line
|
|
128
|
-
line = QFrame()
|
|
129
|
-
line.setFrameShape(QFrame.Shape.HLine)
|
|
130
|
-
line.setFrameShadow(QFrame.Shadow.Sunken)
|
|
131
|
-
main_layout.addWidget(line)
|
|
132
|
-
|
|
133
151
|
# Bottom section: Recent Operations
|
|
134
152
|
recent_group = QGroupBox("Recent Operations (Last 5)")
|
|
135
153
|
recent_layout = QVBoxLayout(recent_group)
|
|
154
|
+
recent_layout.setSpacing(10)
|
|
136
155
|
|
|
137
156
|
self.recent_table = QTableView()
|
|
138
157
|
self.recent_table.setModel(self._history_model)
|
|
158
|
+
self.recent_table.setAlternatingRowColors(True)
|
|
159
|
+
self.recent_table.setShowGrid(False)
|
|
139
160
|
self.recent_table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
|
140
161
|
self.recent_table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
|
162
|
+
self.recent_table.verticalHeader().setDefaultSectionSize(32)
|
|
141
163
|
self.recent_table.horizontalHeader().setSectionResizeMode(
|
|
142
164
|
QHeaderView.ResizeMode.ResizeToContents
|
|
143
165
|
)
|
|
@@ -146,7 +168,7 @@ class DashboardView(QWidget):
|
|
|
146
168
|
|
|
147
169
|
btn_row = QHBoxLayout()
|
|
148
170
|
btn_row.addStretch()
|
|
149
|
-
self.view_history_btn = QPushButton("View Full History")
|
|
171
|
+
self.view_history_btn = QPushButton("View Full History →")
|
|
150
172
|
self.view_history_btn.clicked.connect(lambda: self.navigate_requested.emit("history"))
|
|
151
173
|
btn_row.addWidget(self.view_history_btn)
|
|
152
174
|
recent_layout.addLayout(btn_row)
|