clutterxp 1.1.1__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.
Files changed (214) hide show
  1. {clutterxp-1.1.1 → clutterxp-1.2.0}/PKG-INFO +1 -1
  2. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/__init__.py +1 -1
  3. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/app.py +9 -2
  4. clutterxp-1.2.0/src/clutterxp/gui/theme.py +378 -0
  5. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/dashboard.py +45 -23
  6. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/duplicates.py +23 -5
  7. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/folders.py +18 -3
  8. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/history.py +29 -6
  9. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/preview.py +35 -9
  10. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/rules.py +74 -12
  11. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/settings.py +12 -2
  12. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/workers.py +11 -3
  13. {clutterxp-1.1.1 → clutterxp-1.2.0}/.github/workflows/ci.yml +0 -0
  14. {clutterxp-1.1.1 → clutterxp-1.2.0}/.gitignore +0 -0
  15. {clutterxp-1.1.1 → clutterxp-1.2.0}/CHANGELOG.md +0 -0
  16. {clutterxp-1.1.1 → clutterxp-1.2.0}/README.md +0 -0
  17. {clutterxp-1.1.1 → clutterxp-1.2.0}/config.example.yaml +0 -0
  18. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/00_PROJECT_PLAN.md +0 -0
  19. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/01_ARCHITECTURE.md +0 -0
  20. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/02_RULE_ENGINE.md +0 -0
  21. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/03_CONFIG.md +0 -0
  22. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/04_SAFE_MOVER.md +0 -0
  23. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/05_HISTORY_UNDO.md +0 -0
  24. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/06_WATCHER.md +0 -0
  25. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/07_DUPLICATES.md +0 -0
  26. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/08_CLI.md +0 -0
  27. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/09_SAFETY.md +0 -0
  28. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/10_TESTING.md +0 -0
  29. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/11_PACKAGING.md +0 -0
  30. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/12_GUI.md +0 -0
  31. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/13_ROADMAP.md +0 -0
  32. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/14_DECISIONS.md +0 -0
  33. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/15_ACCEPTANCE.md +0 -0
  34. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/16_RISKS.md +0 -0
  35. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/.gitkeep +0 -0
  36. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/acceptance_matrix.md +0 -0
  37. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/perf_report.md +0 -0
  38. {clutterxp-1.1.1 → clutterxp-1.2.0}/docs/reviews/security_review.md +0 -0
  39. {clutterxp-1.1.1 → clutterxp-1.2.0}/packaging/clutterxp.service +0 -0
  40. {clutterxp-1.1.1 → clutterxp-1.2.0}/pyproject.toml +0 -0
  41. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/.gitkeep +0 -0
  42. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/check_contracts.py +0 -0
  43. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/check_ownership.py +0 -0
  44. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/contracts.lock.json +0 -0
  45. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/e2e.sh +0 -0
  46. {clutterxp-1.1.1 → clutterxp-1.2.0}/scripts/smoke_readme.sh +0 -0
  47. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/__init__.py +0 -0
  48. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/app.py +0 -0
  49. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_config.py +0 -0
  50. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_doctor.py +0 -0
  51. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_duplicates.py +0 -0
  52. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_gui.py +0 -0
  53. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_history.py +0 -0
  54. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_init.py +0 -0
  55. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_organize.py +0 -0
  56. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_rules.py +0 -0
  57. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_scan.py +0 -0
  58. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_service.py +0 -0
  59. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_undo.py +0 -0
  60. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_watch.py +0 -0
  61. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/errors.py +0 -0
  62. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/cli/render.py +0 -0
  63. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/__init__.py +0 -0
  64. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/jsonschema.py +0 -0
  65. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/loader.py +0 -0
  66. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/placeholders.py +0 -0
  67. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/runtime.py +0 -0
  68. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/schema.py +0 -0
  69. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/validate.py +0 -0
  70. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/config/writer.py +0 -0
  71. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/__init__.py +0 -0
  72. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/detector.py +0 -0
  73. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/hashcache.py +0 -0
  74. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/namenorm.py +0 -0
  75. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/quarantine.py +0 -0
  76. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/duplicates/report.py +0 -0
  77. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/__init__.py +0 -0
  78. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/api.py +0 -0
  79. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/models_qt.py +0 -0
  80. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/gui/views/__init__.py +0 -0
  81. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/__init__.py +0 -0
  82. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/db.py +0 -0
  83. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/pathcodec.py +0 -0
  84. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/recovery.py +0 -0
  85. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/schema.sql +0 -0
  86. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/service.py +0 -0
  87. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/history/undo.py +0 -0
  88. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/interfaces.py +0 -0
  89. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/locking.py +0 -0
  90. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/logging_setup.py +0 -0
  91. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/models.py +0 -0
  92. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/__init__.py +0 -0
  93. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/collision.py +0 -0
  94. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/fsops.py +0 -0
  95. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/mover/safe_move.py +0 -0
  96. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/organizer.py +0 -0
  97. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/paths.py +0 -0
  98. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/planner.py +0 -0
  99. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/py.typed +0 -0
  100. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/__init__.py +0 -0
  101. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/engine.py +0 -0
  102. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/matcher.py +0 -0
  103. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/template.py +0 -0
  104. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/rules/tokenizer.py +0 -0
  105. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/safety.py +0 -0
  106. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/scanner.py +0 -0
  107. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/service_install.py +0 -0
  108. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/units.py +0 -0
  109. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/__init__.py +0 -0
  110. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/debounce.py +0 -0
  111. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/events.py +0 -0
  112. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/ignore.py +0 -0
  113. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/service.py +0 -0
  114. {clutterxp-1.1.1 → clutterxp-1.2.0}/src/clutterxp/watch/settle.py +0 -0
  115. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/.gitkeep +0 -0
  116. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/__init__.py +0 -0
  117. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/__init__.py +0 -0
  118. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/conftest.py +0 -0
  119. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/__init__.py +0 -0
  120. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/doctor.json +0 -0
  121. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/duplicates.json +0 -0
  122. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/history.json +0 -0
  123. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/organize.json +0 -0
  124. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/rules_list.json +0 -0
  125. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/rules_test.json +0 -0
  126. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/schemas/scan.json +0 -0
  127. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_config.py +0 -0
  128. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_doctor.py +0 -0
  129. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_duplicates.py +0 -0
  130. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_general.py +0 -0
  131. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_gui.py +0 -0
  132. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_history.py +0 -0
  133. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_init.py +0 -0
  134. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_organize.py +0 -0
  135. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_rules.py +0 -0
  136. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_scan.py +0 -0
  137. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_service.py +0 -0
  138. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_undo.py +0 -0
  139. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/cli/test_watch.py +0 -0
  140. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/__init__.py +0 -0
  141. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/conftest.py +0 -0
  142. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_jsonschema_and_logging.py +0 -0
  143. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_loader.py +0 -0
  144. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_paths.py +0 -0
  145. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_placeholders.py +0 -0
  146. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_runtime.py +0 -0
  147. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_validate.py +0 -0
  148. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/config/test_writer.py +0 -0
  149. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/conftest.py +0 -0
  150. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/__init__.py +0 -0
  151. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_detector.py +0 -0
  152. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_hashcache.py +0 -0
  153. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_namenorm.py +0 -0
  154. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/duplicates/test_quarantine.py +0 -0
  155. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/__init__.py +0 -0
  156. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/conftest.py +0 -0
  157. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A81_gui_launches_and_cli_never_imports_qt.py +0 -0
  158. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A82_dashboard_shows_watcher_and_recent_operations.py +0 -0
  159. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A83_folders_view_edits_watch_folders_and_settings.py +0 -0
  160. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A84_rules_editor_includes_live_trace.py +0 -0
  161. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A85_preview_data_equals_cli_json_plan.py +0 -0
  162. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A86_history_view_undo_shows_exact_refusal_reasons.py +0 -0
  163. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A87_duplicates_view_progress_and_cancel.py +0 -0
  164. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A88_duplicates_quarantine_gui.py +0 -0
  165. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A89_ui_thread_never_blocks_worker_cancellation.py +0 -0
  166. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_A90_gui_extra_documented.py +0 -0
  167. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_api_extra.py +0 -0
  168. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/gui/test_models_qt.py +0 -0
  169. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/helpers.py +0 -0
  170. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/__init__.py +0 -0
  171. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_db.py +0 -0
  172. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_pathcodec.py +0 -0
  173. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_recovery.py +0 -0
  174. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_service.py +0 -0
  175. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/history/test_undo.py +0 -0
  176. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/__init__.py +0 -0
  177. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_acceptance_sweep.py +0 -0
  178. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_fault_injection.py +0 -0
  179. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_properties.py +0 -0
  180. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/integration/test_scenarios.py +0 -0
  181. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/__init__.py +0 -0
  182. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/conftest.py +0 -0
  183. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_collision.py +0 -0
  184. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_fsops.py +0 -0
  185. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_locking.py +0 -0
  186. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/mover/test_safe_move.py +0 -0
  187. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/__init__.py +0 -0
  188. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/conftest.py +0 -0
  189. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_organizer.py +0 -0
  190. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_planner.py +0 -0
  191. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/organizer/test_scanner.py +0 -0
  192. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/perf/__init__.py +0 -0
  193. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/perf/test_perf_budgets.py +0 -0
  194. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/__init__.py +0 -0
  195. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/conftest.py +0 -0
  196. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_engine.py +0 -0
  197. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_matcher.py +0 -0
  198. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_template.py +0 -0
  199. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_tokenizer.py +0 -0
  200. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/rules/test_vectors.py +0 -0
  201. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/__init__.py +0 -0
  202. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/test_invariants_full.py +0 -0
  203. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/safety/test_safety_module.py +0 -0
  204. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/test_harness.py +0 -0
  205. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/test_service_install.py +0 -0
  206. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/__init__.py +0 -0
  207. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/conftest.py +0 -0
  208. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_acceptance.py +0 -0
  209. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_debounce.py +0 -0
  210. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_events.py +0 -0
  211. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_ignore.py +0 -0
  212. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_invariants.py +0 -0
  213. {clutterxp-1.1.1 → clutterxp-1.2.0}/tests/watch/test_service.py +0 -0
  214. {clutterxp-1.1.1 → 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.1.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
@@ -1,4 +1,4 @@
1
1
  """ClutterXP - configurable, safe, fast file organizer."""
2
2
 
3
- __version__ = "1.1.1"
3
+ __version__ = "1.2.0"
4
4
 
@@ -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(1000, 700)
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
- # Title
49
- title_label = QLabel("ClutterXP Dashboard")
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(16)
52
+ title_font.setPointSize(18)
52
53
  title_font.setBold(True)
53
54
  title_label.setFont(title_font)
54
- main_layout.addWidget(title_label)
55
+ title_label.setStyleSheet("color: #f8fafc;")
56
+ header_layout.addWidget(title_label)
55
57
 
56
- # Top row: Watcher Card & Stats Grid
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(8)
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
- stats_layout.addWidget(QLabel("Configured Folders:"), 0, 0)
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
- stats_layout.addWidget(QLabel("Today's Moves:"), 1, 0)
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
- stats_layout.addWidget(QLabel("Active Rules:"), 2, 0)
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.scan_btn = QPushButton("Scan Folders")
111
- self.scan_btn.clicked.connect(lambda: self.navigate_requested.emit("preview"))
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.undo_last_btn = QPushButton("Undo Last Batch")
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)
@@ -106,16 +106,27 @@ class DuplicatesView(QWidget):
106
106
  main_layout.setSpacing(12)
107
107
 
108
108
  # Header
109
+ header_layout = QVBoxLayout()
110
+ header_layout.setSpacing(2)
109
111
  title_label = QLabel("Duplicate File Finder & Quarantine")
110
112
  title_font = title_label.font()
111
- title_font.setPointSize(16)
113
+ title_font.setPointSize(18)
112
114
  title_font.setBold(True)
113
115
  title_label.setFont(title_font)
114
- main_layout.addWidget(title_label)
116
+ title_label.setStyleSheet("color: #f8fafc;")
117
+ header_layout.addWidget(title_label)
118
+
119
+ subtitle_label = QLabel(
120
+ "Find duplicate downloads taking up storage and safely isolate them without deleting."
121
+ )
122
+ subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
123
+ header_layout.addWidget(subtitle_label)
124
+ main_layout.addLayout(header_layout)
115
125
 
116
126
  # Folder Picker & Scan Controls Box
117
127
  scan_box = QGroupBox("Scan Targets & Parameters")
118
128
  scan_layout = QVBoxLayout(scan_box)
129
+ scan_layout.setSpacing(10)
119
130
 
120
131
  folders_row = QHBoxLayout()
121
132
  self.folders_list = QListWidget()
@@ -140,6 +151,7 @@ class DuplicatesView(QWidget):
140
151
  scan_layout.addLayout(folders_row)
141
152
 
142
153
  options_row = QHBoxLayout()
154
+ options_row.setSpacing(10)
143
155
  self.recursive_cb = QCheckBox("Recursive")
144
156
  self.recursive_cb.setChecked(True)
145
157
  options_row.addWidget(self.recursive_cb)
@@ -155,8 +167,8 @@ class DuplicatesView(QWidget):
155
167
 
156
168
  options_row.addStretch()
157
169
 
158
- self.scan_btn = QPushButton("Start Scan")
159
- self.scan_btn.setStyleSheet("font-weight: bold; padding: 6px 14px;")
170
+ self.scan_btn = QPushButton("🔍 Start Scan")
171
+ self.scan_btn.setProperty("primary", True)
160
172
  self.scan_btn.clicked.connect(self.start_scan)
161
173
  options_row.addWidget(self.scan_btn)
162
174
 
@@ -179,9 +191,12 @@ class DuplicatesView(QWidget):
179
191
  groups_container = QWidget()
180
192
  groups_layout = QVBoxLayout(groups_container)
181
193
  groups_layout.setContentsMargins(0, 0, 0, 0)
194
+ groups_layout.setSpacing(8)
182
195
 
183
196
  filter_row = QHBoxLayout()
184
- filter_row.addWidget(QLabel("Show Groups:"))
197
+ lbl_show = QLabel("Show Groups:")
198
+ lbl_show.setStyleSheet("color: #94a3b8;")
199
+ filter_row.addWidget(lbl_show)
185
200
  self.kind_combo = QComboBox()
186
201
  self.kind_combo.addItem("All Duplicate Groups", "all")
187
202
  self.kind_combo.addItem("Exact Duplicates Only", "exact")
@@ -194,8 +209,11 @@ class DuplicatesView(QWidget):
194
209
 
195
210
  self.table = QTableView()
196
211
  self.table.setModel(self._model)
212
+ self.table.setAlternatingRowColors(True)
213
+ self.table.setShowGrid(False)
197
214
  self.table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
198
215
  self.table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
216
+ self.table.verticalHeader().setDefaultSectionSize(32)
199
217
  self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
200
218
  self.table.horizontalHeader().setStretchLastSection(True)
201
219
  self.table.selectionModel().selectionChanged.connect(self._on_group_selected)
@@ -191,16 +191,27 @@ class FoldersView(QWidget):
191
191
 
192
192
  # Header with Title and Watcher Control
193
193
  top_bar = QHBoxLayout()
194
+ header_layout = QVBoxLayout()
195
+ header_layout.setSpacing(2)
194
196
  title_label = QLabel("Watch Folders")
195
197
  title_font = title_label.font()
196
- title_font.setPointSize(16)
198
+ title_font.setPointSize(18)
197
199
  title_font.setBold(True)
198
200
  title_label.setFont(title_font)
199
- top_bar.addWidget(title_label)
201
+ title_label.setStyleSheet("color: #f8fafc;")
202
+ header_layout.addWidget(title_label)
203
+
204
+ subtitle_label = QLabel("Directories monitored by ClutterXP for automatic, quiet sorting.")
205
+ subtitle_label.setStyleSheet("color: #94a3b8; font-size: 12px;")
206
+ header_layout.addWidget(subtitle_label)
207
+ top_bar.addLayout(header_layout)
200
208
 
201
209
  top_bar.addStretch()
202
210
 
203
211
  self.watcher_status_label = QLabel("Watcher: Checking...")
212
+ self.watcher_status_label.setStyleSheet(
213
+ "color: #a5b4fc; font-weight: 500; font-size: 13px; margin-right: 8px;"
214
+ )
204
215
  top_bar.addWidget(self.watcher_status_label)
205
216
 
206
217
  self.watcher_toggle_btn = QPushButton("Start Watcher")
@@ -212,15 +223,19 @@ class FoldersView(QWidget):
212
223
  # Table of folders
213
224
  self.table = QTableView()
214
225
  self.table.setModel(self._model)
226
+ self.table.setAlternatingRowColors(True)
227
+ self.table.setShowGrid(False)
215
228
  self.table.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
216
229
  self.table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
230
+ self.table.verticalHeader().setDefaultSectionSize(32)
217
231
  self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
218
232
  self.table.horizontalHeader().setStretchLastSection(True)
219
233
  main_layout.addWidget(self.table, stretch=1)
220
234
 
221
235
  # Buttons row
222
236
  btn_row = QHBoxLayout()
223
- self.add_btn = QPushButton("Add Folder...")
237
+ self.add_btn = QPushButton("+ Add Folder...")
238
+ self.add_btn.setProperty("primary", True)
224
239
  self.add_btn.clicked.connect(self._on_add_folder)
225
240
  btn_row.addWidget(self.add_btn)
226
241