code-puppy-core-plugins 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,172 @@
1
+ """Frame data for the catalogue's cli-spinners-style builtins.
2
+
3
+ GENERATED data module -- every non-ASCII char is escape-spelled (repo
4
+ emoji filter), which is also why this file is data-only: hand-editing
5
+ escaped braille is nobody's idea of fun. Intervals are intentionally
6
+ absent; ``spinners.py`` applies one normalized speed to all of these.
7
+ """
8
+
9
+ # Frame rows are packed several-per-line; one-per-line would blow the
10
+ # 600-line cap. The bare marker below must stay bare -- ruff ignores
11
+ # 'fmt: off' comments that have trailing prose.
12
+ # fmt: off
13
+ # name -> (frames, description)
14
+ EXTRA_SPECS = {
15
+ "dotsWide": (
16
+ (
17
+ "\u2809\u2809", "\u2808\u2819", "\u2800\u2839", "\u2800\u28b8", "\u2800\u28f0",
18
+ "\u2880\u28e0", "\u28c0\u28c0", "\u28c4\u2840", "\u28c6\u2800", "\u2847\u2800",
19
+ "\u280f\u2800", "\u280b\u2801",
20
+ ),
21
+ "a two-cell braille orbit",
22
+ ),
23
+ "dots8Bit": (
24
+ (
25
+ "\u2800", "\u2801", "\u2802", "\u2803", "\u2804", "\u2805", "\u2806", "\u2807",
26
+ "\u2840", "\u2841", "\u2842", "\u2843", "\u2844", "\u2845", "\u2846", "\u2847",
27
+ "\u2808", "\u2809", "\u280a", "\u280b", "\u280c", "\u280d", "\u280e", "\u280f",
28
+ "\u2848", "\u2849", "\u284a", "\u284b", "\u284c", "\u284d", "\u284e", "\u284f",
29
+ "\u2810", "\u2811", "\u2812", "\u2813", "\u2814", "\u2815", "\u2816", "\u2817",
30
+ "\u2850", "\u2851", "\u2852", "\u2853", "\u2854", "\u2855", "\u2856", "\u2857",
31
+ "\u2818", "\u2819", "\u281a", "\u281b", "\u281c", "\u281d", "\u281e", "\u281f",
32
+ "\u2858", "\u2859", "\u285a", "\u285b", "\u285c", "\u285d", "\u285e", "\u285f",
33
+ "\u2820", "\u2821", "\u2822", "\u2823", "\u2824", "\u2825", "\u2826", "\u2827",
34
+ "\u2860", "\u2861", "\u2862", "\u2863", "\u2864", "\u2865", "\u2866", "\u2867",
35
+ "\u2828", "\u2829", "\u282a", "\u282b", "\u282c", "\u282d", "\u282e", "\u282f",
36
+ "\u2868", "\u2869", "\u286a", "\u286b", "\u286c", "\u286d", "\u286e", "\u286f",
37
+ "\u2830", "\u2831", "\u2832", "\u2833", "\u2834", "\u2835", "\u2836", "\u2837",
38
+ "\u2870", "\u2871", "\u2872", "\u2873", "\u2874", "\u2875", "\u2876", "\u2877",
39
+ "\u2838", "\u2839", "\u283a", "\u283b", "\u283c", "\u283d", "\u283e", "\u283f",
40
+ "\u2878", "\u2879", "\u287a", "\u287b", "\u287c", "\u287d", "\u287e", "\u287f",
41
+ "\u2880", "\u2881", "\u2882", "\u2883", "\u2884", "\u2885", "\u2886", "\u2887",
42
+ "\u28c0", "\u28c1", "\u28c2", "\u28c3", "\u28c4", "\u28c5", "\u28c6", "\u28c7",
43
+ "\u2888", "\u2889", "\u288a", "\u288b", "\u288c", "\u288d", "\u288e", "\u288f",
44
+ "\u28c8", "\u28c9", "\u28ca", "\u28cb", "\u28cc", "\u28cd", "\u28ce", "\u28cf",
45
+ "\u2890", "\u2891", "\u2892", "\u2893", "\u2894", "\u2895", "\u2896", "\u2897",
46
+ "\u28d0", "\u28d1", "\u28d2", "\u28d3", "\u28d4", "\u28d5", "\u28d6", "\u28d7",
47
+ "\u2898", "\u2899", "\u289a", "\u289b", "\u289c", "\u289d", "\u289e", "\u289f",
48
+ "\u28d8", "\u28d9", "\u28da", "\u28db", "\u28dc", "\u28dd", "\u28de", "\u28df",
49
+ "\u28a0", "\u28a1", "\u28a2", "\u28a3", "\u28a4", "\u28a5", "\u28a6", "\u28a7",
50
+ "\u28e0", "\u28e1", "\u28e2", "\u28e3", "\u28e4", "\u28e5", "\u28e6", "\u28e7",
51
+ "\u28a8", "\u28a9", "\u28aa", "\u28ab", "\u28ac", "\u28ad", "\u28ae", "\u28af",
52
+ "\u28e8", "\u28e9", "\u28ea", "\u28eb", "\u28ec", "\u28ed", "\u28ee", "\u28ef",
53
+ "\u28b0", "\u28b1", "\u28b2", "\u28b3", "\u28b4", "\u28b5", "\u28b6", "\u28b7",
54
+ "\u28f0", "\u28f1", "\u28f2", "\u28f3", "\u28f4", "\u28f5", "\u28f6", "\u28f7",
55
+ "\u28b8", "\u28b9", "\u28ba", "\u28bb", "\u28bc", "\u28bd", "\u28be", "\u28bf",
56
+ "\u28f8", "\u28f9", "\u28fa", "\u28fb", "\u28fc", "\u28fd", "\u28fe", "\u28ff",
57
+ ),
58
+ "all 256 braille patterns",
59
+ ),
60
+ "dotsCircle": (
61
+ (
62
+ "\u288e ", "\u280e\u2801", "\u280a\u2811", "\u2808\u2831", " \u2871",
63
+ "\u2880\u2870", "\u2884\u2860", "\u2886\u2840",
64
+ ),
65
+ "a braille ring chase",
66
+ ),
67
+ "sand": (
68
+ (
69
+ "\u2801", "\u2802", "\u2804", "\u2840", "\u2848", "\u2850", "\u2860", "\u28c0",
70
+ "\u28c1", "\u28c2", "\u28c4", "\u28cc", "\u28d4", "\u28e4", "\u28e5", "\u28e6",
71
+ "\u28ee", "\u28f6", "\u28f7", "\u28ff", "\u287f", "\u283f", "\u289f", "\u281f",
72
+ "\u285b", "\u281b", "\u282b", "\u288b", "\u280b", "\u280d", "\u2849", "\u2809",
73
+ "\u2811", "\u2821", "\u2881",
74
+ ),
75
+ "grains trickling through",
76
+ ),
77
+ "growVertical": (
78
+ (
79
+ "\u2581", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2586", "\u2585",
80
+ "\u2584", "\u2583",
81
+ ),
82
+ "a bar breathing up and down",
83
+ ),
84
+ "growHorizontal": (
85
+ (
86
+ "\u258f", "\u258e", "\u258d", "\u258c", "\u258b", "\u258a", "\u2589", "\u258a",
87
+ "\u258b", "\u258c", "\u258d", "\u258e",
88
+ ),
89
+ "a bar breathing side to side",
90
+ ),
91
+ "noise": (
92
+ (
93
+ "\u2593", "\u2592", "\u2591",
94
+ ),
95
+ "static from an old TV",
96
+ ),
97
+ "binary": (
98
+ (
99
+ "010010", "001100", "100101", "111010", "111101", "010111", "101011", "111000",
100
+ "110011", "110101",
101
+ ),
102
+ "streaming ones and zeros",
103
+ ),
104
+ "chevrons": (
105
+ (
106
+ "\u25b9\u25b9\u25b9\u25b9\u25b9", "\u25b8\u25b9\u25b9\u25b9\u25b9",
107
+ "\u25b9\u25b8\u25b9\u25b9\u25b9", "\u25b9\u25b9\u25b8\u25b9\u25b9",
108
+ "\u25b9\u25b9\u25b9\u25b8\u25b9", "\u25b9\u25b9\u25b9\u25b9\u25b8",
109
+ ),
110
+ "a pulse of chevrons",
111
+ ),
112
+ "bouncingBar": (
113
+ (
114
+ "[ ]", "[= ]", "[== ]", "[=== ]", "[====]", "[ ===]", "[ ==]", "[ =]",
115
+ "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]",
116
+ ),
117
+ "old-school loading-bar ping-pong",
118
+ ),
119
+ "bouncingBall": (
120
+ (
121
+ "( \u25cf )", "( \u25cf )", "( \u25cf )", "( \u25cf )", "( \u25cf)",
122
+ "( \u25cf )", "( \u25cf )", "( \u25cf )", "( \u25cf )", "(\u25cf )",
123
+ ),
124
+ "a ball on kennel patrol",
125
+ ),
126
+ "pong": (
127
+ (
128
+ "\u2590\u2802 \u258c", "\u2590\u2808 \u258c",
129
+ "\u2590 \u2802 \u258c", "\u2590 \u2820 \u258c",
130
+ "\u2590 \u2840 \u258c", "\u2590 \u2820 \u258c",
131
+ "\u2590 \u2802 \u258c", "\u2590 \u2808 \u258c",
132
+ "\u2590 \u2802 \u258c", "\u2590 \u2820 \u258c",
133
+ "\u2590 \u2840 \u258c", "\u2590 \u2820 \u258c",
134
+ "\u2590 \u2802 \u258c", "\u2590 \u2808 \u258c",
135
+ "\u2590 \u2802\u258c", "\u2590 \u2820\u258c",
136
+ "\u2590 \u2840\u258c", "\u2590 \u2820 \u258c",
137
+ "\u2590 \u2802 \u258c", "\u2590 \u2808 \u258c",
138
+ "\u2590 \u2802 \u258c", "\u2590 \u2820 \u258c",
139
+ "\u2590 \u2840 \u258c", "\u2590 \u2820 \u258c",
140
+ "\u2590 \u2802 \u258c", "\u2590 \u2808 \u258c",
141
+ "\u2590 \u2802 \u258c", "\u2590 \u2820 \u258c",
142
+ "\u2590 \u2840 \u258c", "\u2590\u2820 \u258c",
143
+ ),
144
+ "two paddles, one braille ball",
145
+ ),
146
+ "fistBump": (
147
+ (
148
+ "\U0001f91c\u3000\u3000\u3000\u3000\U0001f91b ",
149
+ "\U0001f91c\u3000\u3000\u3000\u3000\U0001f91b ",
150
+ "\U0001f91c\u3000\u3000\u3000\u3000\U0001f91b ",
151
+ "\u3000\U0001f91c\u3000\u3000\U0001f91b\u3000 ",
152
+ "\u3000\u3000\U0001f91c\U0001f91b\u3000\u3000 ",
153
+ "\u3000\U0001f91c\u2728\U0001f91b\u3000\u3000 ",
154
+ "\U0001f91c\u3000\u2728\u3000\U0001f91b\u3000 ",
155
+ ),
156
+ "when the work deserves respect",
157
+ ),
158
+ "aesthetic": (
159
+ (
160
+ "\u25b0\u25b1\u25b1\u25b1\u25b1\u25b1\u25b1",
161
+ "\u25b0\u25b0\u25b1\u25b1\u25b1\u25b1\u25b1",
162
+ "\u25b0\u25b0\u25b0\u25b1\u25b1\u25b1\u25b1",
163
+ "\u25b0\u25b0\u25b0\u25b0\u25b1\u25b1\u25b1",
164
+ "\u25b0\u25b0\u25b0\u25b0\u25b0\u25b1\u25b1",
165
+ "\u25b0\u25b0\u25b0\u25b0\u25b0\u25b0\u25b1",
166
+ "\u25b0\u25b0\u25b0\u25b0\u25b0\u25b0\u25b0",
167
+ "\u25b1\u25b1\u25b1\u25b1\u25b1\u25b1\u25b1",
168
+ ),
169
+ "vaporwave progress vibes",
170
+ ),
171
+ }
172
+ # fmt: on
@@ -0,0 +1,116 @@
1
+ """/spinner -- pick a status-bar spinner style, interactively or by name.
2
+
3
+ UX:
4
+ /spinner -> interactive picker with live animated preview
5
+ (-/+ adjusts speed, i writes a starter
6
+ spinners.json, Enter applies)
7
+ /spinner <name> [seconds] -> apply by name, optionally at a custom speed
8
+ /spinner init -> write a starter spinners.json for custom spinners
9
+
10
+ Custom spinners live in ``spinners.json`` next to ``puppy.cfg``; user
11
+ entries override builtins on name collision. The choice persists in
12
+ ``puppy.cfg``, custom speeds save into spinners.json itself, and both
13
+ take effect on the very next tick -- no restart. Edits to spinners.json
14
+ are picked up automatically (mtime watch), so there's no reload command
15
+ to remember.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import concurrent.futures
22
+
23
+ from code_puppy.messaging import emit_error, emit_info, emit_warning
24
+
25
+ from . import spinners
26
+ from .picker import interactive_spinner_picker
27
+
28
+ _INTERACTIVE_TIMEOUT_SECONDS = 300 # 5 min -- generous; user is browsing
29
+
30
+
31
+ def help_entries():
32
+ return [
33
+ ("spinner", "Pick a status-bar spinner style (interactive: /spinner)"),
34
+ ]
35
+
36
+
37
+ def _run_interactive_picker() -> tuple[str, float | None] | None:
38
+ """Run the async TUI from a sync command handler (theme-plugin pattern)."""
39
+ with concurrent.futures.ThreadPoolExecutor() as executor:
40
+ future = executor.submit(lambda: asyncio.run(interactive_spinner_picker()))
41
+ return future.result(timeout=_INTERACTIVE_TIMEOUT_SECONDS)
42
+
43
+
44
+ def _apply(name: str, interval: float | None = None) -> None:
45
+ # Match case-insensitively; built-in names use camelCase.
46
+ catalogue = spinners.get_catalogue()
47
+ name = next((n for n in catalogue if n.lower() == name.lower()), name)
48
+ try:
49
+ spinner = spinners.set_active(name, interval)
50
+ except KeyError:
51
+ known = ", ".join(sorted(spinners.get_catalogue()))
52
+ emit_warning(
53
+ f"Unknown spinner '{name}'. Try /spinner for the picker, "
54
+ f"or pick one of: {known}."
55
+ )
56
+ return
57
+ speed_note = " -- speed saved to spinners.json" if interval is not None else ""
58
+ emit_info(
59
+ f"Spinner set to '{spinner.name}' "
60
+ f"({len(spinner.frames)} frames @ {spinner.interval:.2f}s{speed_note}). "
61
+ f"Sample: {spinner.frames[0].strip()}"
62
+ )
63
+
64
+
65
+ def _init() -> None:
66
+ try:
67
+ created = spinners.write_template()
68
+ except OSError as exc:
69
+ emit_error(f"Could not write {spinners.USER_SPINNERS_FILE}: {exc}")
70
+ return
71
+ if created:
72
+ emit_info(
73
+ f"Starter file written: {spinners.USER_SPINNERS_FILE}\n"
74
+ "Edit it freely -- changes are picked up automatically."
75
+ )
76
+ else:
77
+ emit_info(
78
+ f"{spinners.USER_SPINNERS_FILE} already exists -- not touching it. "
79
+ "Edit it directly; changes are picked up automatically."
80
+ )
81
+
82
+
83
+ def handle_spinner(command: str, name: str):
84
+ """``custom_command`` callback -- owns ``/spinner`` only."""
85
+ if name != "spinner":
86
+ return None
87
+
88
+ parts = command.split()
89
+ sub = parts[1].lower() if len(parts) > 1 else ""
90
+
91
+ if sub == "":
92
+ try:
93
+ chosen = _run_interactive_picker()
94
+ except Exception as exc: # pragma: no cover -- defensive UX
95
+ emit_error(f"Spinner picker failed: {exc}")
96
+ return True
97
+ if chosen is None:
98
+ emit_info("Spinner unchanged.")
99
+ return True
100
+ name, interval = chosen
101
+ _apply(name, interval)
102
+ elif sub == "init":
103
+ _init()
104
+ else:
105
+ interval = None
106
+ if len(parts) > 2:
107
+ try:
108
+ interval = float(parts[2])
109
+ except ValueError:
110
+ emit_warning(
111
+ f"'{parts[2]}' is not a number of seconds. "
112
+ "Usage: /spinner <name> [seconds]"
113
+ )
114
+ return True
115
+ _apply(sub, interval)
116
+ return True
@@ -0,0 +1,379 @@
1
+ """Interactive spinner picker TUI.
2
+
3
+ Same full-screen prompt_toolkit pattern as the theme plugin's picker,
4
+ but with a preview pane that actually *animates*: a background asyncio
5
+ task invalidates the app ~20x/sec, and the preview control derives the
6
+ current frame from wall-clock time, so every spinner runs at its own
7
+ configured interval.
8
+
9
+ Navigation follows the plugins-menu convention (same split-pane shape,
10
+ same muscle memory): ``j``/``k`` + arrows move the selection (clamped,
11
+ no wraparound), ``pageup``/``pagedown`` page through the list via the
12
+ shared pagination helpers, ``g``/``G``/``home``/``end`` jump to the
13
+ first/last entry, ``enter`` applies, and ``q``/``esc``/``ctrl-c`` bail.
14
+ ``i`` writes the starter spinners.json without leaving; the animator
15
+ task also watches the file's mtime, so external edits reload the menu
16
+ live while the picker is open.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import sys
23
+ import time
24
+ from typing import List, Optional, Tuple
25
+
26
+ from prompt_toolkit import Application
27
+ from prompt_toolkit.formatted_text import FormattedText
28
+ from prompt_toolkit.key_binding import KeyBindings
29
+ from prompt_toolkit.layout import Layout, VSplit, Window
30
+ from prompt_toolkit.layout.controls import FormattedTextControl
31
+ from prompt_toolkit.widgets import Frame
32
+
33
+ from code_puppy.command_line.pagination import (
34
+ ensure_visible_page,
35
+ get_page_bounds,
36
+ get_page_for_index,
37
+ get_total_pages,
38
+ )
39
+
40
+ from . import spinners as sp
41
+ from code_puppy.callbacks import on_prompt_toolkit_style
42
+
43
+ #: Preview invalidation cadence, pinned to MIN_INTERVAL so slowest spinners run
44
+ #: at their true speed.
45
+ _REFRESH_INTERVAL_S = sp.MIN_INTERVAL
46
+ PAGE_SIZE = 20 # one line per entry, same as the plugins menu
47
+ #: Speed-key grid step; matching the clamp floor keeps 0.02..1.0 on-grid.
48
+ _SPEED_STEP_S = 0.02
49
+
50
+
51
+ def _step_interval(current: float, delta: float) -> float:
52
+ """One speed-key step from *current*: snap to the step grid, then clamp.
53
+
54
+ Snapping matters at the edges: clamping alone strands you off-grid
55
+ (0.05 -> floor 0.02 -> 0.07 -> 0.12 ...). Rounding to the nearest
56
+ multiple of the step first means the floor is just a stop -- the
57
+ next nudge up lands back on 0.05. Off-grid starting values (from a
58
+ spinners.json tweak) get folded onto the grid the same way.
59
+ """
60
+ stepped = round((current + delta) / _SPEED_STEP_S) * _SPEED_STEP_S
61
+ return sp.clamp_interval(stepped)
62
+
63
+
64
+ def _format_menu(
65
+ entries: List[sp.Spinner], selected: int, page: int, active: str
66
+ ) -> FormattedText:
67
+ """Left-hand menu, styled to match the plugins menu's list pane.
68
+
69
+ One line per entry: ``" > "`` marks the selection (bold), a green
70
+ ``*`` glyph marks the active spinner (the plugins menu's ``+``/``x``
71
+ slot), names only -- descriptions live in the preview pane, exactly
72
+ like plugin descriptions live in the detail pane.
73
+ """
74
+ total_pages = get_total_pages(len(entries), PAGE_SIZE)
75
+ start, end = get_page_bounds(page, len(entries), PAGE_SIZE)
76
+
77
+ lines: list[tuple[str, str]] = [
78
+ ("class:tui.header", " Spinners"),
79
+ ("", "\n\n"),
80
+ ]
81
+ for i in range(start, end):
82
+ spinner = entries[i]
83
+ is_selected = i == selected
84
+ icon = "*" if spinner.name == active else " "
85
+ prefix = " > " if is_selected else " "
86
+
87
+ if is_selected:
88
+ lines.append(("class:tui.selected", prefix))
89
+ lines.append(("class:tui.selected", icon))
90
+ lines.append(("class:tui.selected", f" {spinner.name}"))
91
+ else:
92
+ lines.append(("class:tui.body", prefix))
93
+ lines.append(("class:tui.success", icon))
94
+ lines.append(("class:tui.muted", f" {spinner.name}"))
95
+ lines.append(("", "\n"))
96
+
97
+ lines.append(("", "\n"))
98
+ lines.append(("class:tui.muted", f" Page {page + 1}/{total_pages}"))
99
+ lines.append(("", "\n"))
100
+
101
+ _render_hints(lines)
102
+ return FormattedText(lines)
103
+
104
+
105
+ #: Hint rows; ``_render_hints`` pads keys to keep action columns aligned.
106
+ _HINTS = [
107
+ ("class:tui.help-key", "up/down or j/k", "Navigate"),
108
+ ("class:tui.help-key", "PgUp/PgDn", "Page"),
109
+ ("class:tui.help-key", "g / G", "First / Last"),
110
+ ("class:tui.help-key", "-/+ or \u2190/\u2192", "Slower / Faster"),
111
+ ("class:tui.help-key", "i", "Init spinners.json"),
112
+ ("class:tui.success", "Enter", "Apply"),
113
+ ("class:tui.error", "q / Esc", "Exit"),
114
+ ]
115
+
116
+
117
+ def _render_hints(lines: list[tuple[str, str]]) -> None:
118
+ """The plugins menu's hint block, with a speed row where their
119
+ detail-scroll row sits. The key column is computed, not hand-padded.
120
+ """
121
+ key_col = max(len(keys) for _, keys, _ in _HINTS) + 1
122
+ lines.append(("", "\n"))
123
+ for i, (style, keys, label) in enumerate(_HINTS):
124
+ lines.append((style, f" {keys.ljust(key_col)}"))
125
+ lines.append(
126
+ ("class:tui.help", label if i == len(_HINTS) - 1 else f"{label}\n")
127
+ )
128
+
129
+
130
+ def _format_preview(
131
+ spinner: sp.Spinner,
132
+ started_at: float,
133
+ interval: Optional[float] = None,
134
+ notice: str = "",
135
+ ) -> FormattedText:
136
+ """Right-hand pane, styled like the plugins menu's detail pane.
137
+
138
+ *interval* is the speed the user dialed with the speed keys; None
139
+ means the spinner's own speed (the usual case). *notice* is a
140
+ one-line status message (e.g. the outcome of pressing ``i``).
141
+ """
142
+ effective = interval if interval is not None else spinner.interval
143
+ elapsed = time.monotonic() - started_at
144
+ frame = spinner.frames[int(elapsed / effective) % len(spinner.frames)]
145
+ lines: list[tuple[str, str]] = [
146
+ ("class:tui.title", " LIVE PREVIEW"),
147
+ ("", "\n\n"),
148
+ ("class:tui.label", f" {spinner.name}"),
149
+ ("", "\n\n"),
150
+ ]
151
+ if spinner.description:
152
+ lines.append(("", f" {spinner.description}"))
153
+ lines.append(("", "\n\n"))
154
+ lines.extend(
155
+ [
156
+ ("class:tui.label", f" {frame}"),
157
+ ("", "\n\n"),
158
+ ("class:tui.label", " Source: "),
159
+ ("class:tui.body", spinner.source),
160
+ ("", "\n"),
161
+ ("class:tui.label", " Frames: "),
162
+ ("class:tui.body", str(len(spinner.frames))),
163
+ ("", "\n"),
164
+ ("class:tui.label", " Interval: "),
165
+ ("class:tui.body", f"{effective:.2f}s"),
166
+ (
167
+ "class:tui.warning",
168
+ " (custom -- Enter saves it)" if interval is not None else "",
169
+ ),
170
+ ("", "\n\n"),
171
+ ("class:tui.label", " Custom spinners:"),
172
+ ("", "\n"),
173
+ ("class:tui.muted", f" {sp.USER_SPINNERS_FILE}"),
174
+ ("", "\n"),
175
+ ("class:tui.muted", " (press i to write a starter file)"),
176
+ ("", "\n"),
177
+ ]
178
+ )
179
+ if notice:
180
+ lines.append(("", "\n"))
181
+ lines.append(("class:tui.warning", f" {notice}"))
182
+ lines.append(("", "\n"))
183
+ return FormattedText(lines)
184
+
185
+
186
+ async def interactive_spinner_picker() -> Optional[Tuple[str, Optional[float]]]:
187
+ """Show the full-screen spinner picker.
188
+
189
+ Returns:
190
+ ``(name, interval)`` -- *interval* is the dialed custom speed or
191
+ None for the spinner's own speed -- or ``None`` if cancelled.
192
+ """
193
+ from code_puppy.tools.command_runner import set_awaiting_user_input
194
+
195
+ entries = list(sp.get_catalogue().values())
196
+ active = sp.get_active_spinner().name
197
+ selected = [next((i for i, s in enumerate(entries) if s.name == active), 0)]
198
+ page = [get_page_for_index(selected[0], PAGE_SIZE)]
199
+ custom_interval: list[Optional[float]] = [None]
200
+ notice = [""]
201
+ result: list[Optional[Tuple[str, Optional[float]]]] = [None]
202
+ started_at = time.monotonic()
203
+
204
+ set_awaiting_user_input(True)
205
+ sys.stdout.write("\033[?1049h\033[2J\033[H")
206
+ sys.stdout.flush()
207
+ await asyncio.sleep(0.05)
208
+
209
+ def _set_selection(new_idx: int) -> None:
210
+ """Clamp selection and keep its page visible (plugins-menu contract).
211
+
212
+ Moving resets the dialed speed -- each entry previews at its own
213
+ speed until you nudge it.
214
+ """
215
+ selected[0] = max(0, min(new_idx, len(entries) - 1))
216
+ page[0] = ensure_visible_page(selected[0], page[0], len(entries), PAGE_SIZE)
217
+ custom_interval[0] = None
218
+
219
+ def _nudge_speed(delta: float) -> None:
220
+ current = (
221
+ custom_interval[0]
222
+ if custom_interval[0] is not None
223
+ else entries[selected[0]].interval
224
+ )
225
+ custom_interval[0] = _step_interval(current, delta)
226
+
227
+ def _refresh_entries() -> None:
228
+ """Re-read the catalogue in place, keeping the selection pinned by
229
+ name. Shared by the ``i`` key and the live file watcher."""
230
+ current_name = entries[selected[0]].name
231
+ entries[:] = list(sp.get_catalogue().values())
232
+ _set_selection(
233
+ next((i for i, s in enumerate(entries) if s.name == current_name), 0)
234
+ )
235
+
236
+ def _change_page(delta: int) -> None:
237
+ """Move the page by *delta* (clamped) and jump selection to its head."""
238
+ total_pages = get_total_pages(len(entries), PAGE_SIZE)
239
+ new_page = max(0, min(page[0] + delta, total_pages - 1))
240
+ if new_page == page[0]:
241
+ return
242
+ page[0] = new_page
243
+ _set_selection(new_page * PAGE_SIZE)
244
+
245
+ kb = KeyBindings()
246
+
247
+ # -- Selection (j = up, k = down -- plugins-menu convention) ------------
248
+ @kb.add("up")
249
+ @kb.add("c-p")
250
+ @kb.add("j")
251
+ def _(event):
252
+ _set_selection(selected[0] - 1)
253
+ event.app.invalidate()
254
+
255
+ @kb.add("down")
256
+ @kb.add("c-n")
257
+ @kb.add("k")
258
+ def _(event):
259
+ _set_selection(selected[0] + 1)
260
+ event.app.invalidate()
261
+
262
+ # -- Page through the list ----------------------------------------------
263
+ @kb.add("pageup")
264
+ def _(event):
265
+ _change_page(-1)
266
+ event.app.invalidate()
267
+
268
+ @kb.add("pagedown")
269
+ def _(event):
270
+ _change_page(+1)
271
+ event.app.invalidate()
272
+
273
+ # -- Jump to first / last -------------------------------------------------
274
+ @kb.add("home")
275
+ @kb.add("g")
276
+ def _(event):
277
+ _set_selection(0)
278
+ event.app.invalidate()
279
+
280
+ @kb.add("end")
281
+ @kb.add("G")
282
+ def _(event):
283
+ _set_selection(len(entries) - 1)
284
+ event.app.invalidate()
285
+
286
+ # -- Speed (left/- slower, right/+ faster: interval moves inversely) ----
287
+ @kb.add("left")
288
+ @kb.add("-")
289
+ def _(event):
290
+ _nudge_speed(+_SPEED_STEP_S)
291
+ event.app.invalidate()
292
+
293
+ @kb.add("right")
294
+ @kb.add("+")
295
+ @kb.add("=") # unshifted + on most layouts
296
+ def _(event):
297
+ _nudge_speed(-_SPEED_STEP_S)
298
+ event.app.invalidate()
299
+
300
+ # -- Init the user spinners file (no need to leave the menu) -----------
301
+ @kb.add("i")
302
+ def _(event):
303
+ try:
304
+ created = sp.write_template()
305
+ notice[0] = (
306
+ "Starter file written -- edit it freely, changes apply live."
307
+ if created
308
+ else "spinners.json already exists -- edit it directly."
309
+ )
310
+ except OSError as exc:
311
+ notice[0] = f"Could not write starter file: {exc}"
312
+ _refresh_entries()
313
+ event.app.invalidate()
314
+
315
+ # -- Actions / exit --------------------------------------------------------
316
+ @kb.add("enter")
317
+ def _(event):
318
+ result[0] = (entries[selected[0]].name, custom_interval[0])
319
+ event.app.exit()
320
+
321
+ @kb.add("q")
322
+ @kb.add("escape")
323
+ @kb.add("c-c")
324
+ def _(event):
325
+ result[0] = None
326
+ event.app.exit()
327
+
328
+ left = Window(
329
+ content=FormattedTextControl(
330
+ lambda: _format_menu(entries, selected[0], page[0], active)
331
+ ),
332
+ width=36, # widest hint row is 35 cells; don't guillotine the json
333
+ )
334
+ right = Window(
335
+ content=FormattedTextControl(
336
+ lambda: _format_preview(
337
+ entries[selected[0]], started_at, custom_interval[0], notice[0]
338
+ )
339
+ ),
340
+ )
341
+
342
+ layout = Layout(
343
+ VSplit([Frame(left, title="Spinners"), Frame(right, title="Live Preview")])
344
+ )
345
+ app = Application(
346
+ layout=layout,
347
+ key_bindings=kb,
348
+ full_screen=False,
349
+ mouse_support=False,
350
+ style=on_prompt_toolkit_style(),
351
+ )
352
+
353
+ async def _animate() -> None:
354
+ """Poke the app awake so the preview frame advances -- and watch
355
+ spinners.json so external edits reload the menu while it's open
356
+ (the same mtime signal the tick loop uses).
357
+ """
358
+ file_stamp = sp.user_file_stamp()
359
+ try:
360
+ while True:
361
+ await asyncio.sleep(_REFRESH_INTERVAL_S)
362
+ stamp = sp.user_file_stamp()
363
+ if stamp != file_stamp:
364
+ file_stamp = stamp
365
+ _refresh_entries()
366
+ app.invalidate()
367
+ except asyncio.CancelledError:
368
+ pass
369
+
370
+ animator = asyncio.get_running_loop().create_task(_animate())
371
+ try:
372
+ await app.run_async()
373
+ finally:
374
+ animator.cancel()
375
+ set_awaiting_user_input(False)
376
+ sys.stdout.write("\033[?1049l")
377
+ sys.stdout.flush()
378
+
379
+ return result[0]