usage-cli 0.31.0__tar.gz → 0.31.1__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 (224) hide show
  1. {usage_cli-0.31.0/usage_cli.egg-info → usage_cli-0.31.1}/PKG-INFO +4 -4
  2. {usage_cli-0.31.0 → usage_cli-0.31.1}/README.md +3 -3
  3. {usage_cli-0.31.0 → usage_cli-0.31.1}/i18n.json +5 -0
  4. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/__init__.py +14 -0
  5. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/dynamic_height.py +11 -2
  6. {usage_cli-0.31.0 → usage_cli-0.31.1}/pyproject.toml +1 -1
  7. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_analyzer_pipeline.py +1 -0
  8. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_dynamic_height.py +13 -0
  9. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_panels.py +3 -0
  10. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_wintray.py +86 -1
  11. {usage_cli-0.31.0 → usage_cli-0.31.1/usage_cli.egg-info}/PKG-INFO +4 -4
  12. {usage_cli-0.31.0 → usage_cli-0.31.1}/wintray/app.py +51 -14
  13. {usage_cli-0.31.0 → usage_cli-0.31.1}/LICENSE +0 -0
  14. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/__init__.py +0 -0
  15. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/agy.py +0 -0
  16. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/claude.py +0 -0
  17. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/codex.py +0 -0
  18. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/grok.py +0 -0
  19. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/muse.py +0 -0
  20. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/rate_limits.py +0 -0
  21. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/registry.py +0 -0
  22. {usage_cli-0.31.0 → usage_cli-0.31.1}/adapters/types.py +0 -0
  23. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/__init__.py +0 -0
  24. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/aggregator.py +0 -0
  25. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/blocks.py +0 -0
  26. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/diagnoser.py +0 -0
  27. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/dispatch_ledger.py +0 -0
  28. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/insights.py +0 -0
  29. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/persona_loader.py +0 -0
  30. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/reporter.py +0 -0
  31. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/subscription.py +0 -0
  32. {usage_cli-0.31.0 → usage_cli-0.31.1}/analyzer/usage_snapshot.py +0 -0
  33. {usage_cli-0.31.0 → usage_cli-0.31.1}/doctor.py +0 -0
  34. {usage_cli-0.31.0 → usage_cli-0.31.1}/fsevents_watch.py +0 -0
  35. {usage_cli-0.31.0 → usage_cli-0.31.1}/i18n.py +0 -0
  36. {usage_cli-0.31.0 → usage_cli-0.31.1}/installer/__init__.py +0 -0
  37. {usage_cli-0.31.0 → usage_cli-0.31.1}/installer/login_item.py +0 -0
  38. {usage_cli-0.31.0 → usage_cli-0.31.1}/installer/session_hooks.py +0 -0
  39. {usage_cli-0.31.0 → usage_cli-0.31.1}/installer/setup_hook.py +0 -0
  40. {usage_cli-0.31.0 → usage_cli-0.31.1}/installer/statusline_settings.py +0 -0
  41. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/__init__.py +0 -0
  42. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/agy_disk_cache.py +0 -0
  43. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/agy_loader.py +0 -0
  44. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/agy_quota_probe.py +0 -0
  45. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/cache_quarantine.py +0 -0
  46. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/claude_paths.py +0 -0
  47. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/codex_disk_cache.py +0 -0
  48. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/codex_events.py +0 -0
  49. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/codex_fork_replay.py +0 -0
  50. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/codex_loader.py +0 -0
  51. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/codex_paths.py +0 -0
  52. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/disk_cache_common.py +0 -0
  53. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/disk_cache_lifecycle.py +0 -0
  54. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/grok_loader.py +0 -0
  55. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/grok_quota_probe.py +0 -0
  56. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/history_disk_cache.py +0 -0
  57. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/history_loader.py +0 -0
  58. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/jsonl_limits.py +0 -0
  59. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/jsonl_utils.py +0 -0
  60. {usage_cli-0.31.0 → usage_cli-0.31.1}/loaders/muse_loader.py +0 -0
  61. {usage_cli-0.31.0 → usage_cli-0.31.1}/main.py +0 -0
  62. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/__init__.py +0 -0
  63. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/actions.py +0 -0
  64. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/agy.py +0 -0
  65. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/app.py +0 -0
  66. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/chrome.py +0 -0
  67. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/grok.py +0 -0
  68. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/menu.py +0 -0
  69. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/notify.py +0 -0
  70. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/popover.py +0 -0
  71. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/prefs.py +0 -0
  72. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/refresh.py +0 -0
  73. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/state.py +0 -0
  74. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/title.py +0 -0
  75. {usage_cli-0.31.0 → usage_cli-0.31.1}/menubar/update.py +0 -0
  76. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/base.py +0 -0
  77. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/panel_scale.py +0 -0
  78. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/panel_window.py +0 -0
  79. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/panel_window_state.py +0 -0
  80. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/payload.py +0 -0
  81. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/web_panel.py +0 -0
  82. {usage_cli-0.31.0 → usage_cli-0.31.1}/panels/window_drag.py +0 -0
  83. {usage_cli-0.31.0 → usage_cli-0.31.1}/prefs.py +0 -0
  84. {usage_cli-0.31.0 → usage_cli-0.31.1}/pricing.py +0 -0
  85. {usage_cli-0.31.0 → usage_cli-0.31.1}/project_resolver.py +0 -0
  86. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/__init__.py +0 -0
  87. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/agy_window_keeper.py +0 -0
  88. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/burn_rate.py +0 -0
  89. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/codex_window_keeper.py +0 -0
  90. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/usage_rate.py +0 -0
  91. {usage_cli-0.31.0 → usage_cli-0.31.1}/quota/window_keeper.py +0 -0
  92. {usage_cli-0.31.0 → usage_cli-0.31.1}/service_status.py +0 -0
  93. {usage_cli-0.31.0 → usage_cli-0.31.1}/setup.cfg +0 -0
  94. {usage_cli-0.31.0 → usage_cli-0.31.1}/setup_app.py +0 -0
  95. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_adapters_claude.py +0 -0
  96. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_adapters_codex.py +0 -0
  97. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_adapters_rate_limits.py +0 -0
  98. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_aggregator.py +0 -0
  99. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_agy_adapter.py +0 -0
  100. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_agy_disk_cache.py +0 -0
  101. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_agy_loader.py +0 -0
  102. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_agy_quota_probe.py +0 -0
  103. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_agy_window_keeper.py +0 -0
  104. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_analyzer_blocks.py +0 -0
  105. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_burn_rate.py +0 -0
  106. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_cache_quarantine.py +0 -0
  107. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_claude_paths.py +0 -0
  108. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_codex_consistency.py +0 -0
  109. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_codex_loader.py +0 -0
  110. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_codex_window_keeper.py +0 -0
  111. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_diagnoser.py +0 -0
  112. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_disk_cache_shards.py +0 -0
  113. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_dispatch_ledger.py +0 -0
  114. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_doctor.py +0 -0
  115. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_fsevents_watch.py +0 -0
  116. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_grok_loader.py +0 -0
  117. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_grok_quota_probe.py +0 -0
  118. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_history_loader.py +0 -0
  119. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_hook_scripts_are_standalone.py +0 -0
  120. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_html_report_snapshot.py +0 -0
  121. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_i18n_format_fallback.py +0 -0
  122. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_i18n_key_parity.py +0 -0
  123. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_i18n_packaged_path.py +0 -0
  124. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_insights.py +0 -0
  125. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_jsonl_utils.py +0 -0
  126. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_login_item.py +0 -0
  127. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_main.py +0 -0
  128. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_menubar.py +0 -0
  129. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_menubar_agy.py +0 -0
  130. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_menubar_grok.py +0 -0
  131. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_menubar_prefs.py +0 -0
  132. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_menubar_state.py +0 -0
  133. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_muse_loader.py +0 -0
  134. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_packaged_resources.py +0 -0
  135. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_packaging.py +0 -0
  136. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_panel_footer_layout.py +0 -0
  137. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_panel_scale.py +0 -0
  138. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_panel_window_state.py +0 -0
  139. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_panels_lazy_import.py +0 -0
  140. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_persona_loader.py +0 -0
  141. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_pricing.py +0 -0
  142. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_project_resolver.py +0 -0
  143. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_report_daily_chart.py +0 -0
  144. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_report_filter_js.py +0 -0
  145. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_reporter_build_data.py +0 -0
  146. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_service_status.py +0 -0
  147. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_session_resume.py +0 -0
  148. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_setup_app.py +0 -0
  149. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_setup_hook.py +0 -0
  150. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_setup_hook_coexistence.py +0 -0
  151. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_setup_hook_resume.py +0 -0
  152. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_setup_hook_terse.py +0 -0
  153. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_subprocess_utils.py +0 -0
  154. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_subscription.py +0 -0
  155. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_time_utils.py +0 -0
  156. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_tui.py +0 -0
  157. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_tui_sprite.py +0 -0
  158. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_ui_tables.py +0 -0
  159. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_update_checker.py +0 -0
  160. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_update_gate.py +0 -0
  161. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_update_release_notes.py +0 -0
  162. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_cli.py +0 -0
  163. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_client.py +0 -0
  164. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_diagnosis_snapshot.py +0 -0
  165. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_dir_sweeper.py +0 -0
  166. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_lang.py +0 -0
  167. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_logging.py +0 -0
  168. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_notifications.py +0 -0
  169. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_rate.py +0 -0
  170. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_snapshot.py +0 -0
  171. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_snapshot_replay.py +0 -0
  172. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_statusline.py +0 -0
  173. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_statusline_agy.py +0 -0
  174. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_statusline_forwarder.py +0 -0
  175. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_statusline_grok.py +0 -0
  176. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_terse_mode.py +0 -0
  177. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_usage_terse_reminder.py +0 -0
  178. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_web_panel_payload.py +0 -0
  179. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_wheel_packaging.py +0 -0
  180. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_win_login_item.py +0 -0
  181. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_window_keeper.py +0 -0
  182. {usage_cli-0.31.0 → usage_cli-0.31.1}/tests/test_windows_manifest.py +0 -0
  183. {usage_cli-0.31.0 → usage_cli-0.31.1}/tui/__init__.py +0 -0
  184. {usage_cli-0.31.0 → usage_cli-0.31.1}/tui/app.py +0 -0
  185. {usage_cli-0.31.0 → usage_cli-0.31.1}/tui/sprite.py +0 -0
  186. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/__init__.py +0 -0
  187. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/html_report.py +0 -0
  188. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_charts.py +0 -0
  189. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_daily_chart.py +0 -0
  190. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_filter.py +0 -0
  191. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_ledger.py +0 -0
  192. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_scripts.py +0 -0
  193. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/report_styles.py +0 -0
  194. {usage_cli-0.31.0 → usage_cli-0.31.1}/ui/tables.py +0 -0
  195. {usage_cli-0.31.0 → usage_cli-0.31.1}/updates/__init__.py +0 -0
  196. {usage_cli-0.31.0 → usage_cli-0.31.1}/updates/checker.py +0 -0
  197. {usage_cli-0.31.0 → usage_cli-0.31.1}/updates/gate.py +0 -0
  198. {usage_cli-0.31.0 → usage_cli-0.31.1}/updates/release_notes.py +0 -0
  199. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.egg-info/SOURCES.txt +0 -0
  200. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.egg-info/dependency_links.txt +0 -0
  201. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.egg-info/entry_points.txt +0 -0
  202. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.egg-info/requires.txt +0 -0
  203. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.egg-info/top_level.txt +0 -0
  204. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_cli.py +0 -0
  205. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_client.py +0 -0
  206. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/__init__.py +0 -0
  207. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/subprocess_utils.py +0 -0
  208. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/time_utils.py +0 -0
  209. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/usage_dir_sweeper.py +0 -0
  210. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/usage_lang.py +0 -0
  211. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_common/usage_logging.py +0 -0
  212. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_diagnosis_snapshot.py +0 -0
  213. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_notifications.py +0 -0
  214. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_session_resume.py +0 -0
  215. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_statusline.py +0 -0
  216. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_statusline_agy.py +0 -0
  217. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_statusline_forwarder.py +0 -0
  218. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_statusline_grok.py +0 -0
  219. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_terse_mode.py +0 -0
  220. {usage_cli-0.31.0 → usage_cli-0.31.1}/usage_terse_reminder.py +0 -0
  221. {usage_cli-0.31.0 → usage_cli-0.31.1}/wintray/__init__.py +0 -0
  222. {usage_cli-0.31.0 → usage_cli-0.31.1}/wintray/login_item.py +0 -0
  223. {usage_cli-0.31.0 → usage_cli-0.31.1}/wintray/menu.py +0 -0
  224. {usage_cli-0.31.0 → usage_cli-0.31.1}/wintray/watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.31.0
3
+ Version: 0.31.1
4
4
  Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
5
5
  License-Expression: AGPL-3.0-only
6
6
  Requires-Python: >=3.13
@@ -87,7 +87,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
87
87
 
88
88
  ### Experience & Customization
89
89
 
90
- - **15 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, and Catppuccin (official palette, all four flavors).
90
+ - **16 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, Heart Monitor, and Catppuccin (official palette, all four flavors).
91
91
  - **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
92
92
  - **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
93
93
  - **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.
@@ -147,7 +147,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
147
147
 
148
148
  Windows has the full core experience: the system-tray UI, Claude Code status-line hook, and Codex history parsing all work natively. Download `usage-windows.zip` from the [latest GitHub Release](https://github.com/aqua5230/usage/releases/latest), unzip it, then run `usage.exe`—no installer is needed. If SmartScreen shows **Windows protected your PC** on first launch, click **More info** → **Run anyway**. The tray UI requires Microsoft Edge WebView2 Runtime, which is normally included with Windows 10 and 11.
149
149
 
150
- The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 15 quota themes available on macOS (Default plus the other fourteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
150
+ The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 16 quota themes available on macOS (Default plus the other fifteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
151
151
 
152
152
  Windows differences: the panel opens at the bottom-right of the working area rather than next to the tray icon; update prompts use a system Yes/No dialog.
153
153
 
@@ -164,7 +164,7 @@ Privacy policy: this program will not transfer any information to other networke
164
164
 
165
165
  ## Theme Gallery
166
166
 
167
- Switch between **15 visual themes** directly from the UI:
167
+ Switch between **16 visual themes** directly from the UI:
168
168
 
169
169
  <p align="center">
170
170
  <img src="docs/classic.en.png" width="32%" alt="Classic theme" />
@@ -67,7 +67,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
67
67
 
68
68
  ### Experience & Customization
69
69
 
70
- - **15 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, and Catppuccin (official palette, all four flavors).
70
+ - **16 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, Heart Monitor, and Catppuccin (official palette, all four flavors).
71
71
  - **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
72
72
  - **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
73
73
  - **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.
@@ -127,7 +127,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
127
127
 
128
128
  Windows has the full core experience: the system-tray UI, Claude Code status-line hook, and Codex history parsing all work natively. Download `usage-windows.zip` from the [latest GitHub Release](https://github.com/aqua5230/usage/releases/latest), unzip it, then run `usage.exe`—no installer is needed. If SmartScreen shows **Windows protected your PC** on first launch, click **More info** → **Run anyway**. The tray UI requires Microsoft Edge WebView2 Runtime, which is normally included with Windows 10 and 11.
129
129
 
130
- The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 15 quota themes available on macOS (Default plus the other fourteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
130
+ The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 16 quota themes available on macOS (Default plus the other fifteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
131
131
 
132
132
  Windows differences: the panel opens at the bottom-right of the working area rather than next to the tray icon; update prompts use a system Yes/No dialog.
133
133
 
@@ -144,7 +144,7 @@ Privacy policy: this program will not transfer any information to other networke
144
144
 
145
145
  ## Theme Gallery
146
146
 
147
- Switch between **15 visual themes** directly from the UI:
147
+ Switch between **16 visual themes** directly from the UI:
148
148
 
149
149
  <p align="center">
150
150
  <img src="docs/classic.en.png" width="32%" alt="Classic theme" />
@@ -11,6 +11,7 @@
11
11
  "panel_stained_glass": "彩繪玻璃",
12
12
  "panel_migration": "候鳥遷徙",
13
13
  "panel_sketchbook": "手繪筆記",
14
+ "panel_heart_monitor": "心電圖",
14
15
  "panel_origami": "摺紙",
15
16
  "panel_catppuccin": "Catppuccin",
16
17
  "panel_black_hole": "黑洞視界",
@@ -437,6 +438,7 @@
437
438
  "panel_stained_glass": "Stained Glass",
438
439
  "panel_migration": "Migration",
439
440
  "panel_sketchbook": "Sketchbook",
441
+ "panel_heart_monitor": "Heart Monitor",
440
442
  "panel_origami": "Origami",
441
443
  "panel_catppuccin": "Catppuccin",
442
444
  "panel_black_hole": "Black Hole",
@@ -863,6 +865,7 @@
863
865
  "panel_stained_glass": "彩绘玻璃",
864
866
  "panel_migration": "候鸟迁徙",
865
867
  "panel_sketchbook": "手绘笔记",
868
+ "panel_heart_monitor": "心电图",
866
869
  "panel_origami": "折纸",
867
870
  "panel_catppuccin": "Catppuccin",
868
871
  "panel_black_hole": "黑洞视界",
@@ -1289,6 +1292,7 @@
1289
1292
  "panel_stained_glass": "ステンドグラス",
1290
1293
  "panel_migration": "渡り鳥",
1291
1294
  "panel_sketchbook": "手描きノート",
1295
+ "panel_heart_monitor": "心電図モニター",
1292
1296
  "panel_origami": "折り紙",
1293
1297
  "panel_catppuccin": "Catppuccin",
1294
1298
  "panel_black_hole": "ブラックホール",
@@ -1715,6 +1719,7 @@
1715
1719
  "panel_stained_glass": "스테인드글라스",
1716
1720
  "panel_migration": "철새 이동",
1717
1721
  "panel_sketchbook": "손그림 노트",
1722
+ "panel_heart_monitor": "심전도 모니터",
1718
1723
  "panel_origami": "종이접기",
1719
1724
  "panel_catppuccin": "Catppuccin",
1720
1725
  "panel_black_hole": "블랙홀",
@@ -144,6 +144,20 @@ def all_panels() -> tuple[Panel, ...]:
144
144
  status_wrap_extra_height=30.0,
145
145
  service_alert_height=32.0,
146
146
  ),
147
+ # Use classic's measured values as initial estimates for the same DOM;
148
+ # dynamic_height.py measures heart_monitor's actual rendered height.
149
+ HTMLPanel(
150
+ "heart_monitor",
151
+ "panel_heart_monitor",
152
+ "heart_monitor.html",
153
+ height=1132.0,
154
+ claude_card_height=192.0,
155
+ codex_card_height=192.0,
156
+ agy_card_height=192.0,
157
+ grok_card_height=128.0,
158
+ status_wrap_extra_height=30.0,
159
+ service_alert_height=32.0,
160
+ ),
147
161
  # Reuse classic's measured values because the DOM structure is identical;
148
162
  # remeasure if a future render shows clipping.
149
163
  HTMLPanel(
@@ -15,6 +15,7 @@ CONTENT_HEIGHT_SCRIPT = """
15
15
  if (typeof applyState !== "function") return;
16
16
  var scheduled = false;
17
17
  var lastPostedHeight = null;
18
+ var lastPostedDpr = null;
18
19
  var fittingHeight = 0;
19
20
  var contentChanged = false;
20
21
  var lastMeasuredHeight = null;
@@ -132,10 +133,17 @@ CONTENT_HEIGHT_SCRIPT = """
132
133
  }
133
134
  var bridge = window.webkit && window.webkit.messageHandlers
134
135
  && window.webkit.messageHandlers.usage;
135
- if (Number.isFinite(height) && height > 0 && height !== lastPostedHeight && bridge
136
+ // The native side sizes its window from this height, so it also needs the
137
+ // ratio the page really renders at. On Windows that is the display scale
138
+ // times the "Text size" accessibility setting (125% x 109% = 1.3625), which
139
+ // GetDpiForWindow does not include.
140
+ var dpr = window.devicePixelRatio;
141
+ if (Number.isFinite(height) && height > 0
142
+ && (height !== lastPostedHeight || dpr !== lastPostedDpr) && bridge
136
143
  && typeof bridge.postMessage === "function") {
137
144
  lastPostedHeight = height;
138
- bridge.postMessage(JSON.stringify({ action: "content_height", height: height }));
145
+ lastPostedDpr = dpr;
146
+ bridge.postMessage(JSON.stringify({ action: "content_height", height: height, dpr: dpr }));
139
147
  }
140
148
  }
141
149
  function requestContentHeight() {
@@ -151,6 +159,7 @@ CONTENT_HEIGHT_SCRIPT = """
151
159
  window.usageRequestContentHeight = requestContentHeight;
152
160
  window.usageInvalidateContentHeight = function() {
153
161
  lastPostedHeight = null;
162
+ lastPostedDpr = null;
154
163
  fittingHeight = 0;
155
164
  requestContentHeight();
156
165
  };
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.31.0"
3
+ version = "0.31.1"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -253,6 +253,7 @@ def test_html_panels_expose_analyze_action() -> None:
253
253
  "matrix.html",
254
254
  "migration.html",
255
255
  "sketchbook.html",
256
+ "heart_monitor.html",
256
257
  "newspaper.html",
257
258
  "origami.html",
258
259
  "prism_arcade.html",
@@ -59,6 +59,19 @@ def test_script_wraps_state_application_and_measures_without_height_constraints(
59
59
  assert "body.style.height = bodyHeight" in CONTENT_HEIGHT_SCRIPT
60
60
 
61
61
 
62
+ def test_script_reports_the_rendered_device_pixel_ratio_with_the_height() -> None:
63
+ # The window is sized from the reported height, so the native side needs the
64
+ # ratio the page really renders at: on Windows it includes the "Text size"
65
+ # setting that GetDpiForWindow leaves out.
66
+ assert "var dpr = window.devicePixelRatio;" in CONTENT_HEIGHT_SCRIPT
67
+ assert 'action: "content_height", height: height, dpr: dpr' in CONTENT_HEIGHT_SCRIPT
68
+ # A ratio change alone is worth reporting again, and invalidating the
69
+ # height must not leave a stale ratio behind.
70
+ assert "dpr !== lastPostedDpr" in CONTENT_HEIGHT_SCRIPT
71
+ assert "lastPostedDpr = dpr;" in CONTENT_HEIGHT_SCRIPT
72
+ assert "lastPostedDpr = null;" in CONTENT_HEIGHT_SCRIPT
73
+
74
+
62
75
  def test_world_cup_declares_its_pitch_height_floor() -> None:
63
76
  html = (PANEL_ASSETS / "world_cup.html").read_text(encoding="utf-8")
64
77
 
@@ -33,6 +33,7 @@ CARD_PANEL_FILENAMES = (
33
33
  "matrix.html",
34
34
  "migration.html",
35
35
  "sketchbook.html",
36
+ "heart_monitor.html",
36
37
  "newspaper.html",
37
38
  "origami.html",
38
39
  "prism_arcade.html",
@@ -70,6 +71,7 @@ def test_registered_panel_ids_are_unique() -> None:
70
71
  "stained_glass",
71
72
  "migration",
72
73
  "sketchbook",
74
+ "heart_monitor",
73
75
  "origami",
74
76
  "black_hole",
75
77
  "lepidoptera",
@@ -93,6 +95,7 @@ def test_registered_panel_i18n_keys() -> None:
93
95
  "panel_stained_glass",
94
96
  "panel_migration",
95
97
  "panel_sketchbook",
98
+ "panel_heart_monitor",
96
99
  "panel_origami",
97
100
  "panel_black_hole",
98
101
  "panel_lepidoptera",
@@ -915,8 +915,92 @@ def test_short_high_dpi_panel_uses_native_work_area_and_keeps_footer_visible(
915
915
  assert round((geometry["y"] + geometry["height"]) * 1.25) <= 1032
916
916
 
917
917
 
918
+ def test_content_height_message_records_the_page_render_scale(
919
+ monkeypatch: pytest.MonkeyPatch,
920
+ ) -> None:
921
+ controller = wintray._WindowsTrayController(mock=True, interval=60)
922
+ monkeypatch.setattr(controller, "_window_dpi_scale", lambda: 1.25)
923
+ monkeypatch.setattr(controller, "_working_area", lambda: (0, 0, 1000, 800))
924
+
925
+ assert controller._content_scale() == 1.25
926
+
927
+ controller.handle_panel_message(
928
+ json.dumps({"action": "content_height", "height": 700, "dpr": 1.3625})
929
+ )
930
+ assert controller._content_scale() == 1.3625
931
+
932
+ # A malformed or absurd ratio from the page never replaces a good one.
933
+ for invalid in (True, "1.5", 0, -1, 0.1, 99, None, float("nan")):
934
+ controller.handle_panel_message(
935
+ json.dumps({"action": "content_height", "height": 701, "dpr": invalid})
936
+ )
937
+ assert controller._content_scale() == 1.3625
938
+
939
+
940
+ def test_text_size_scaling_sizes_the_window_by_the_rendered_scale(
941
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
942
+ ) -> None:
943
+ # Windows "Text size" at 109% on a 125% display: WebView2 renders 1.3625
944
+ # physical pixels per CSS pixel while GetDpiForWindow still reports 1.25.
945
+ # Sized with 1.25 alone, the window is ~8% shorter than its content and
946
+ # shows a scrollbar even though the screen has room to spare.
947
+ monkeypatch.setattr(prefs, "PREFERENCES_FILE", tmp_path / "usage-preferences.json")
948
+ geometry: dict[str, int] = {}
949
+ controller = wintray._WindowsTrayController(mock=True, interval=60)
950
+ controller.active_panel_id = "newspaper"
951
+ controller.window = SimpleNamespace(
952
+ x=0,
953
+ y=0,
954
+ evaluate_js=lambda _code: True,
955
+ resize=lambda width, height: geometry.update(width=width, height=height),
956
+ move=lambda x, y: geometry.update(x=x, y=y),
957
+ )
958
+ monkeypatch.setattr(controller, "_window_dpi_scale", lambda: 1.25)
959
+ # A 1920x1020 physical work area, expressed in CSS pixels at 1.3625.
960
+ monkeypatch.setattr(controller, "_native_work_area_for_point", lambda _point: (0, 0, 1409, 749))
961
+ controller.handle_panel_message(
962
+ json.dumps({"action": "content_height", "height": 600, "dpr": 1.3625})
963
+ )
964
+
965
+ controller._place_window()
966
+
967
+ # pywebview multiplies what it is given by the display scale on its own.
968
+ assert abs(geometry["height"] * 1.25 - 600 * 1.3625) <= 1
969
+ assert abs(geometry["width"] * 1.25 - wintray.PANEL_WIDTH * 1.3625) <= 1
970
+
971
+
972
+ def test_pywebview_geometry_is_unchanged_without_a_page_render_scale(
973
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
974
+ ) -> None:
975
+ monkeypatch.setattr(prefs, "PREFERENCES_FILE", tmp_path / "usage-preferences.json")
976
+ geometry: dict[str, int] = {}
977
+ controller = wintray._WindowsTrayController(mock=True, interval=60)
978
+ controller.active_panel_id = "newspaper"
979
+ controller.window = SimpleNamespace(
980
+ x=0,
981
+ y=0,
982
+ evaluate_js=lambda _code: True,
983
+ resize=lambda width, height: geometry.update(width=width, height=height),
984
+ move=lambda x, y: geometry.update(x=x, y=y),
985
+ )
986
+ monkeypatch.setattr(controller, "_window_dpi_scale", lambda: 1.25)
987
+ monkeypatch.setattr(controller, "_native_work_area_for_point", lambda _point: (0, 0, 1536, 816))
988
+ controller.handle_panel_message(json.dumps({"action": "content_height", "height": 600}))
989
+
990
+ controller._place_window()
991
+
992
+ assert geometry["height"] == 600
993
+ assert geometry["width"] == wintray.PANEL_WIDTH
994
+
995
+
996
+ @pytest.mark.parametrize(
997
+ ("page_scale", "expected"),
998
+ [(None, (0, 0, 1536, 826)), (1.3625, (0, 0, 1409, 757))],
999
+ )
918
1000
  def test_native_work_area_converts_physical_pixels_to_logical(
919
1001
  monkeypatch: pytest.MonkeyPatch,
1002
+ page_scale: float | None,
1003
+ expected: tuple[int, int, int, int],
920
1004
  ) -> None:
921
1005
  class FakeFunction:
922
1006
  def __init__(self, result: object) -> None:
@@ -945,8 +1029,9 @@ def test_native_work_area_converts_physical_pixels_to_logical(
945
1029
  controller = wintray._WindowsTrayController(mock=True, interval=60)
946
1030
  controller.window = SimpleNamespace(native=SimpleNamespace(Handle=123))
947
1031
  monkeypatch.setattr(controller, "_window_dpi_scale", lambda: 1.25)
1032
+ controller._apply_page_scale(page_scale)
948
1033
 
949
- assert _REAL_NATIVE_WORK_AREA(controller, (1169, 2)) == (0, 0, 1536, 826)
1034
+ assert _REAL_NATIVE_WORK_AREA(controller, (1169, 2)) == expected
950
1035
 
951
1036
 
952
1037
  def test_panel_rechecks_native_work_area_before_final_move(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.31.0
3
+ Version: 0.31.1
4
4
  Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
5
5
  License-Expression: AGPL-3.0-only
6
6
  Requires-Python: >=3.13
@@ -87,7 +87,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
87
87
 
88
88
  ### Experience & Customization
89
89
 
90
- - **15 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, and Catppuccin (official palette, all four flavors).
90
+ - **16 Visual Themes:** Switch between panel styles including Default, Matrix, Windows 95, Vintage Newspaper, Cloud Observation, Midnight Aquarium, Prism Arcade, Black Hole, World Cup 2026, Lepidoptera, Migration, Stained Glass, Origami, Sketchbook, Heart Monitor, and Catppuccin (official palette, all four flavors).
91
91
  - **Place the Panel Anywhere:** The panel is no longer pinned under the menu bar icon. Drag it from any empty spot to wherever you want it, and it reopens there next time. It stays put when another app takes focus — a second click on the menu bar icon, or Escape, closes it.
92
92
  - **Drag to Reorder:** Grab any quota card and drag it up or down to swap the order — the arrangement is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
93
93
  - **Automatic Localization:** UI text is available in Traditional Chinese, Simplified Chinese, English, Japanese, and Korean, automatically matching your system settings.
@@ -147,7 +147,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
147
147
 
148
148
  Windows has the full core experience: the system-tray UI, Claude Code status-line hook, and Codex history parsing all work natively. Download `usage-windows.zip` from the [latest GitHub Release](https://github.com/aqua5230/usage/releases/latest), unzip it, then run `usage.exe`—no installer is needed. If SmartScreen shows **Windows protected your PC** on first launch, click **More info** → **Run anyway**. The tray UI requires Microsoft Edge WebView2 Runtime, which is normally included with Windows 10 and 11.
149
149
 
150
- The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 15 quota themes available on macOS (Default plus the other fourteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
150
+ The system-tray icon updates with your Claude quota percentage; its tooltip summarizes the Claude and Codex windows. Left-click opens the same 16 quota themes available on macOS (Default plus the other fifteen) in WebView2. Right-click provides Reset Panel Position and Quit; panel switching, refresh, launch at login, and update checks are in the panel menu.
151
151
 
152
152
  Windows differences: the panel opens at the bottom-right of the working area rather than next to the tray icon; update prompts use a system Yes/No dialog.
153
153
 
@@ -164,7 +164,7 @@ Privacy policy: this program will not transfer any information to other networke
164
164
 
165
165
  ## Theme Gallery
166
166
 
167
- Switch between **15 visual themes** directly from the UI:
167
+ Switch between **16 visual themes** directly from the UI:
168
168
 
169
169
  <p align="center">
170
170
  <img src="docs/classic.en.png" width="32%" alt="Classic theme" />
@@ -8,6 +8,7 @@ import ctypes
8
8
  import importlib
9
9
  import json
10
10
  import logging
11
+ import math
11
12
  import os
12
13
  import threading
13
14
  import time
@@ -97,6 +98,7 @@ WINDOWS_PANELS = (
97
98
  ("stained_glass", "panel_stained_glass", "stained_glass.html"),
98
99
  ("migration", "panel_migration", "migration.html"),
99
100
  ("sketchbook", "panel_sketchbook", "sketchbook.html"),
101
+ ("heart_monitor", "panel_heart_monitor", "heart_monitor.html"),
100
102
  ("origami", "panel_origami", "origami.html"),
101
103
  ("catppuccin", "panel_catppuccin", "catppuccin.html"),
102
104
  )
@@ -119,6 +121,7 @@ PANEL_HEIGHTS = {
119
121
  "stained_glass": 1132,
120
122
  "migration": 1132,
121
123
  "sketchbook": 1132,
124
+ "heart_monitor": 1132,
122
125
  "origami": 1132,
123
126
  "catppuccin": 1166,
124
127
  }
@@ -755,6 +758,7 @@ class _WindowsTrayController:
755
758
  self._history_scan: menubar_state.HistorySourceScan | None = None
756
759
  self._history_scan_at: float | None = None
757
760
  self._content_height: int | None = None
761
+ self._page_scale: float | None = None
758
762
  self._window_mutations: deque[Callable[[], None]] = deque()
759
763
  self._window_mutation_lock = threading.Lock()
760
764
  self._window_mutation_scheduled = False
@@ -930,7 +934,7 @@ class _WindowsTrayController:
930
934
  ),
931
935
  result[0] if result else None,
932
936
  )
933
- window_scale = self._window_dpi_scale()
937
+ window_scale = self._content_scale()
934
938
  if (
935
939
  primary is not None
936
940
  and primary[2] == 1.0
@@ -976,10 +980,37 @@ class _WindowsTrayController:
976
980
  return None
977
981
  return dpi / 96.0 if dpi > 0 else None
978
982
 
983
+ def _content_scale(self) -> float | None:
984
+ """Physical pixels per CSS pixel that the panel page is really rendered at.
985
+
986
+ Windows' "Text size" accessibility setting multiplies WebView2's device
987
+ pixel ratio on top of the display scale (125% x 109% = 1.3625), and
988
+ GetDpiForWindow does not include it. A window sized with the display
989
+ scale alone ends up shorter than its content and shows a scrollbar, so
990
+ every CSS <-> physical conversion uses the ratio the page reports and
991
+ falls back to the display scale until it has reported one.
992
+ """
993
+ return self._page_scale or self._window_dpi_scale()
994
+
995
+ def _apply_page_scale(self, value: object) -> None:
996
+ if isinstance(value, bool) or not isinstance(value, int | float):
997
+ return
998
+ scale = float(value)
999
+ if math.isfinite(scale) and 0.5 <= scale <= 8.0:
1000
+ self._page_scale = scale
1001
+
1002
+ def _pywebview_scale_ratio(self) -> float:
1003
+ """Correction for pywebview's resize()/move(), which apply the display scale."""
1004
+ content = self._content_scale()
1005
+ display = self._window_dpi_scale()
1006
+ if content is None or display is None or display <= 0:
1007
+ return 1.0
1008
+ return content / display
1009
+
979
1010
  def _apply_geometry_without_showing(self, width: int, height: int, x: int, y: int) -> bool:
980
1011
  if os.name != "nt" or self.window is None:
981
1012
  return False
982
- scale = self._window_dpi_scale()
1013
+ scale = self._content_scale()
983
1014
  if scale is None:
984
1015
  return False
985
1016
  try:
@@ -1018,7 +1049,7 @@ class _WindowsTrayController:
1018
1049
  """Read the target monitor's physical work area independently of pywebview."""
1019
1050
  if os.name != "nt" or self.window is None:
1020
1051
  return None
1021
- scale = self._window_dpi_scale()
1052
+ scale = self._content_scale()
1022
1053
  if scale is None:
1023
1054
  return None
1024
1055
 
@@ -1104,7 +1135,7 @@ class _WindowsTrayController:
1104
1135
  return None
1105
1136
  if not isinstance(x, (int, float)) or not isinstance(y, (int, float)):
1106
1137
  return None
1107
- scale = self._window_dpi_scale() or 1.0
1138
+ scale = self._content_scale() or 1.0
1108
1139
  return (int(round(x / scale)), int(round(y / scale)))
1109
1140
 
1110
1141
  def _current_window_position(self) -> tuple[int, int] | None:
@@ -1138,7 +1169,7 @@ class _WindowsTrayController:
1138
1169
  position = self._current_window_position()
1139
1170
  if position is None:
1140
1171
  return None
1141
- scale = self._window_dpi_scale() or 1.0
1172
+ scale = self._content_scale() or 1.0
1142
1173
  return (int(round(position[0] * scale)), int(round(position[1] * scale)))
1143
1174
 
1144
1175
  @staticmethod
@@ -1158,9 +1189,9 @@ class _WindowsTrayController:
1158
1189
  left, top, right, bottom = work_area
1159
1190
  return (max(left + 12, right - width - 12), max(top + 12, bottom - height - 12))
1160
1191
 
1161
- # The legibility floor uses physical pixels: CSS zoom times window DPI scale.
1192
+ # The legibility floor uses physical pixels: CSS zoom times the rendered scale.
1162
1193
  def _panel_minimum_scale(self) -> float:
1163
- return MIN_PANEL_SCALE / (self._window_dpi_scale() or 1.0)
1194
+ return MIN_PANEL_SCALE / (self._content_scale() or 1.0)
1164
1195
 
1165
1196
  def _placement_target(
1166
1197
  self, force_default: bool
@@ -1239,9 +1270,10 @@ class _WindowsTrayController:
1239
1270
  )
1240
1271
  x, y = self._clamp_window_position(position, work_area, width, height)
1241
1272
  logger.debug(
1242
- "Panel placement work_area=%s dpi_scale=%s natural_height=%s fit_scale=%s "
1243
- "x=%s y=%s width=%s height=%s",
1273
+ "Panel placement work_area=%s content_scale=%s display_scale=%s "
1274
+ "natural_height=%s fit_scale=%s x=%s y=%s width=%s height=%s",
1244
1275
  work_area,
1276
+ self._content_scale(),
1245
1277
  self._window_dpi_scale(),
1246
1278
  natural_height,
1247
1279
  scale,
@@ -1259,10 +1291,10 @@ class _WindowsTrayController:
1259
1291
  not isinstance(value, bool) and isinstance(value, int | float)
1260
1292
  for value in current_geometry
1261
1293
  ):
1262
- dpi_scale = self._window_dpi_scale()
1263
- if dpi_scale is not None:
1294
+ content_scale = self._content_scale()
1295
+ if content_scale is not None:
1264
1296
  target_geometry = tuple(
1265
- int(round(value * dpi_scale)) for value in (x, y, width, height)
1297
+ int(round(value * content_scale)) for value in (x, y, width, height)
1266
1298
  )
1267
1299
  if tuple(int(round(value)) for value in current_geometry) == target_geometry:
1268
1300
  self._positioned_this_show = True
@@ -1271,8 +1303,11 @@ class _WindowsTrayController:
1271
1303
  self._apply_geometry_without_showing(width, height, x, y)
1272
1304
  )
1273
1305
  if not geometry_applied:
1274
- self.window.resize(width, height)
1275
- self.window.move(x, y)
1306
+ # pywebview multiplies by the display scale itself; pass values that
1307
+ # land on the same physical pixels the page is rendered at.
1308
+ ratio = self._pywebview_scale_ratio()
1309
+ self.window.resize(int(round(width * ratio)), int(round(height * ratio)))
1310
+ self.window.move(int(round(x * ratio)), int(round(y * ratio)))
1276
1311
  self._positioned_this_show = True
1277
1312
 
1278
1313
  def _dispatch_window_mutation(self, mutation: Callable[[], None]) -> None:
@@ -2023,6 +2058,8 @@ class _WindowsTrayController:
2023
2058
  if action == "open_menu":
2024
2059
  return self._panel_menu_data()
2025
2060
  if action == "content_height":
2061
+ # The scale first: applying the height re-places the window.
2062
+ self._apply_page_scale(payload.get("dpr"))
2026
2063
  self._apply_content_height(payload.get("height"))
2027
2064
  return None
2028
2065
  if action == "set_card_order":
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes