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.
Files changed (214) hide show
  1. {clutterxp-1.1.0 → clutterxp-1.2.0}/PKG-INFO +10 -10
  2. {clutterxp-1.1.0 → clutterxp-1.2.0}/README.md +9 -9
  3. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/__init__.py +1 -1
  4. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/app.py +9 -2
  5. clutterxp-1.2.0/src/clutterxp/gui/theme.py +378 -0
  6. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/dashboard.py +45 -23
  7. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/duplicates.py +23 -5
  8. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/folders.py +18 -3
  9. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/history.py +29 -6
  10. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/preview.py +35 -9
  11. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/rules.py +74 -12
  12. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/settings.py +12 -2
  13. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/workers.py +11 -3
  14. {clutterxp-1.1.0 → clutterxp-1.2.0}/.github/workflows/ci.yml +0 -0
  15. {clutterxp-1.1.0 → clutterxp-1.2.0}/.gitignore +0 -0
  16. {clutterxp-1.1.0 → clutterxp-1.2.0}/CHANGELOG.md +0 -0
  17. {clutterxp-1.1.0 → clutterxp-1.2.0}/config.example.yaml +0 -0
  18. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/00_PROJECT_PLAN.md +0 -0
  19. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/01_ARCHITECTURE.md +0 -0
  20. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/02_RULE_ENGINE.md +0 -0
  21. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/03_CONFIG.md +0 -0
  22. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/04_SAFE_MOVER.md +0 -0
  23. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/05_HISTORY_UNDO.md +0 -0
  24. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/06_WATCHER.md +0 -0
  25. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/07_DUPLICATES.md +0 -0
  26. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/08_CLI.md +0 -0
  27. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/09_SAFETY.md +0 -0
  28. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/10_TESTING.md +0 -0
  29. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/11_PACKAGING.md +0 -0
  30. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/12_GUI.md +0 -0
  31. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/13_ROADMAP.md +0 -0
  32. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/14_DECISIONS.md +0 -0
  33. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/15_ACCEPTANCE.md +0 -0
  34. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/16_RISKS.md +0 -0
  35. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/.gitkeep +0 -0
  36. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/acceptance_matrix.md +0 -0
  37. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/perf_report.md +0 -0
  38. {clutterxp-1.1.0 → clutterxp-1.2.0}/docs/reviews/security_review.md +0 -0
  39. {clutterxp-1.1.0 → clutterxp-1.2.0}/packaging/clutterxp.service +0 -0
  40. {clutterxp-1.1.0 → clutterxp-1.2.0}/pyproject.toml +0 -0
  41. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/.gitkeep +0 -0
  42. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/check_contracts.py +0 -0
  43. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/check_ownership.py +0 -0
  44. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/contracts.lock.json +0 -0
  45. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/e2e.sh +0 -0
  46. {clutterxp-1.1.0 → clutterxp-1.2.0}/scripts/smoke_readme.sh +0 -0
  47. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/__init__.py +0 -0
  48. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/app.py +0 -0
  49. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_config.py +0 -0
  50. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_doctor.py +0 -0
  51. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_duplicates.py +0 -0
  52. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_gui.py +0 -0
  53. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_history.py +0 -0
  54. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_init.py +0 -0
  55. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_organize.py +0 -0
  56. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_rules.py +0 -0
  57. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_scan.py +0 -0
  58. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_service.py +0 -0
  59. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_undo.py +0 -0
  60. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/cmd_watch.py +0 -0
  61. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/errors.py +0 -0
  62. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/cli/render.py +0 -0
  63. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/__init__.py +0 -0
  64. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/jsonschema.py +0 -0
  65. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/loader.py +0 -0
  66. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/placeholders.py +0 -0
  67. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/runtime.py +0 -0
  68. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/schema.py +0 -0
  69. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/validate.py +0 -0
  70. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/config/writer.py +0 -0
  71. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/__init__.py +0 -0
  72. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/detector.py +0 -0
  73. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/hashcache.py +0 -0
  74. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/namenorm.py +0 -0
  75. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/quarantine.py +0 -0
  76. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/duplicates/report.py +0 -0
  77. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/__init__.py +0 -0
  78. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/api.py +0 -0
  79. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/models_qt.py +0 -0
  80. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/gui/views/__init__.py +0 -0
  81. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/__init__.py +0 -0
  82. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/db.py +0 -0
  83. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/pathcodec.py +0 -0
  84. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/recovery.py +0 -0
  85. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/schema.sql +0 -0
  86. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/service.py +0 -0
  87. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/history/undo.py +0 -0
  88. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/interfaces.py +0 -0
  89. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/locking.py +0 -0
  90. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/logging_setup.py +0 -0
  91. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/models.py +0 -0
  92. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/__init__.py +0 -0
  93. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/collision.py +0 -0
  94. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/fsops.py +0 -0
  95. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/mover/safe_move.py +0 -0
  96. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/organizer.py +0 -0
  97. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/paths.py +0 -0
  98. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/planner.py +0 -0
  99. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/py.typed +0 -0
  100. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/__init__.py +0 -0
  101. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/engine.py +0 -0
  102. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/matcher.py +0 -0
  103. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/template.py +0 -0
  104. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/rules/tokenizer.py +0 -0
  105. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/safety.py +0 -0
  106. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/scanner.py +0 -0
  107. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/service_install.py +0 -0
  108. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/units.py +0 -0
  109. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/__init__.py +0 -0
  110. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/debounce.py +0 -0
  111. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/events.py +0 -0
  112. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/ignore.py +0 -0
  113. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/service.py +0 -0
  114. {clutterxp-1.1.0 → clutterxp-1.2.0}/src/clutterxp/watch/settle.py +0 -0
  115. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/.gitkeep +0 -0
  116. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/__init__.py +0 -0
  117. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/__init__.py +0 -0
  118. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/conftest.py +0 -0
  119. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/__init__.py +0 -0
  120. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/doctor.json +0 -0
  121. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/duplicates.json +0 -0
  122. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/history.json +0 -0
  123. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/organize.json +0 -0
  124. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/rules_list.json +0 -0
  125. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/rules_test.json +0 -0
  126. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/schemas/scan.json +0 -0
  127. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_config.py +0 -0
  128. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_doctor.py +0 -0
  129. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_duplicates.py +0 -0
  130. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_general.py +0 -0
  131. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_gui.py +0 -0
  132. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_history.py +0 -0
  133. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_init.py +0 -0
  134. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_organize.py +0 -0
  135. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_rules.py +0 -0
  136. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_scan.py +0 -0
  137. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_service.py +0 -0
  138. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_undo.py +0 -0
  139. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/cli/test_watch.py +0 -0
  140. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/__init__.py +0 -0
  141. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/conftest.py +0 -0
  142. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_jsonschema_and_logging.py +0 -0
  143. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_loader.py +0 -0
  144. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_paths.py +0 -0
  145. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_placeholders.py +0 -0
  146. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_runtime.py +0 -0
  147. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_validate.py +0 -0
  148. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/config/test_writer.py +0 -0
  149. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/conftest.py +0 -0
  150. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/__init__.py +0 -0
  151. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_detector.py +0 -0
  152. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_hashcache.py +0 -0
  153. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_namenorm.py +0 -0
  154. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/duplicates/test_quarantine.py +0 -0
  155. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/__init__.py +0 -0
  156. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/conftest.py +0 -0
  157. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A81_gui_launches_and_cli_never_imports_qt.py +0 -0
  158. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A82_dashboard_shows_watcher_and_recent_operations.py +0 -0
  159. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A83_folders_view_edits_watch_folders_and_settings.py +0 -0
  160. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A84_rules_editor_includes_live_trace.py +0 -0
  161. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A85_preview_data_equals_cli_json_plan.py +0 -0
  162. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A86_history_view_undo_shows_exact_refusal_reasons.py +0 -0
  163. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A87_duplicates_view_progress_and_cancel.py +0 -0
  164. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A88_duplicates_quarantine_gui.py +0 -0
  165. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A89_ui_thread_never_blocks_worker_cancellation.py +0 -0
  166. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_A90_gui_extra_documented.py +0 -0
  167. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_api_extra.py +0 -0
  168. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/gui/test_models_qt.py +0 -0
  169. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/helpers.py +0 -0
  170. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/__init__.py +0 -0
  171. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_db.py +0 -0
  172. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_pathcodec.py +0 -0
  173. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_recovery.py +0 -0
  174. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_service.py +0 -0
  175. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/history/test_undo.py +0 -0
  176. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/__init__.py +0 -0
  177. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_acceptance_sweep.py +0 -0
  178. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_fault_injection.py +0 -0
  179. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_properties.py +0 -0
  180. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/integration/test_scenarios.py +0 -0
  181. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/__init__.py +0 -0
  182. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/conftest.py +0 -0
  183. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_collision.py +0 -0
  184. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_fsops.py +0 -0
  185. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_locking.py +0 -0
  186. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/mover/test_safe_move.py +0 -0
  187. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/__init__.py +0 -0
  188. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/conftest.py +0 -0
  189. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_organizer.py +0 -0
  190. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_planner.py +0 -0
  191. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/organizer/test_scanner.py +0 -0
  192. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/perf/__init__.py +0 -0
  193. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/perf/test_perf_budgets.py +0 -0
  194. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/__init__.py +0 -0
  195. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/conftest.py +0 -0
  196. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_engine.py +0 -0
  197. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_matcher.py +0 -0
  198. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_template.py +0 -0
  199. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_tokenizer.py +0 -0
  200. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/rules/test_vectors.py +0 -0
  201. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/__init__.py +0 -0
  202. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/test_invariants_full.py +0 -0
  203. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/safety/test_safety_module.py +0 -0
  204. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/test_harness.py +0 -0
  205. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/test_service_install.py +0 -0
  206. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/__init__.py +0 -0
  207. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/conftest.py +0 -0
  208. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_acceptance.py +0 -0
  209. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_debounce.py +0 -0
  210. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_events.py +0 -0
  211. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_ignore.py +0 -0
  212. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_invariants.py +0 -0
  213. {clutterxp-1.1.0 → clutterxp-1.2.0}/tests/watch/test_service.py +0 -0
  214. {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.1.0
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
- - 🛡️ **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.
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
- ### 📁 Set it and forget it (Background Watcher)
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
- ### 🔍 Find and clean duplicate downloads
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
- ### ✏️ Customize your rules in seconds
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
- - 🛡️ **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.
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
- ### 📁 Set it and forget it (Background Watcher)
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
- ### 🔍 Find and clean duplicate downloads
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
- ### ✏️ Customize your rules in seconds
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`"*
@@ -1,4 +1,4 @@
1
1
  """ClutterXP - configurable, safe, fast file organizer."""
2
2
 
3
- __version__ = "1.1.0"
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)