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.
- code_puppy_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Register callbacks for the ``puppy_spinner`` plugin.
|
|
2
|
+
|
|
3
|
+
Resurrects the classic bouncing-puppy spinner on the persistent bottom
|
|
4
|
+
bar. The old Rich ``Live`` spinner died in the bottom-bar rewrite (its
|
|
5
|
+
compat shim in ``messaging.spinner`` forwards context text only); this
|
|
6
|
+
plugin animates the bar's dedicated status-PREFIX slot instead, so the
|
|
7
|
+
token-context info written via ``BottomBar.set_status`` is never
|
|
8
|
+
stomped -- two writers, two slots, one row.
|
|
9
|
+
|
|
10
|
+
Lifecycle:
|
|
11
|
+
|
|
12
|
+
* ``agent_run_start`` -- refcount +1; first active run starts a ~5 fps
|
|
13
|
+
asyncio ticker task (same zero-thread pattern as ``subagent_panel``).
|
|
14
|
+
Sub-agent runs fire these hooks too; the refcount naturally keeps the
|
|
15
|
+
puppy running until the LAST run finishes.
|
|
16
|
+
* ``agent_run_end`` -- refcount -1 (fired from ``_runtime``'s ``finally``,
|
|
17
|
+
so cancels/exceptions never leak a spin); at zero the ticker stops and
|
|
18
|
+
the prefix slot is cleared.
|
|
19
|
+
|
|
20
|
+
Headless (``-p`` / non-TTY) runs never start the ticker: the bar is
|
|
21
|
+
inactive and output must stay byte-identical.
|
|
22
|
+
|
|
23
|
+
Customization (``/spinner``): the tick loop reads the active spinner
|
|
24
|
+
from ``spinners`` each iteration, so picking a new style in the TUI
|
|
25
|
+
(or simply editing ``spinners.json`` -- its mtime is watched) takes
|
|
26
|
+
effect on the very next frame, even mid-run.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import logging
|
|
33
|
+
import threading
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
from code_puppy.callbacks import register_callback
|
|
37
|
+
|
|
38
|
+
from . import commands, spinners
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_PUPPY = spinners.PUPPY # DOG FACE emoji, escape-spelled (repo emoji filter)
|
|
43
|
+
|
|
44
|
+
#: The classic kennel-bounce (the ``puppy`` entry in the spinner catalogue).
|
|
45
|
+
FRAMES = spinners.BUILTIN_SPINNERS[spinners.DEFAULT_SPINNER].frames
|
|
46
|
+
|
|
47
|
+
#: Sourced from the catalogue so the stock puppy's speed has exactly one
|
|
48
|
+
#: home; kept as a module constant so tests can monkeypatch the tempo.
|
|
49
|
+
_TICK_INTERVAL_S = spinners.BUILTIN_SPINNERS[spinners.DEFAULT_SPINNER].interval
|
|
50
|
+
|
|
51
|
+
_lock = threading.Lock()
|
|
52
|
+
_active_runs = 0
|
|
53
|
+
_ticker_task: Optional["asyncio.Task"] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _bar():
|
|
57
|
+
from code_puppy.messaging.bottom_bar import get_bottom_bar
|
|
58
|
+
|
|
59
|
+
return get_bottom_bar()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Dispatcher passes hook args positionally (up to seven for agent_run_end); this
|
|
63
|
+
# plugin ignores them, so swallow all args to stay signature-proof.
|
|
64
|
+
async def _on_run_start(*_args, **_kw):
|
|
65
|
+
global _active_runs
|
|
66
|
+
with _lock:
|
|
67
|
+
_active_runs += 1
|
|
68
|
+
_start_ticker()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _on_run_end(*_args, **_kw):
|
|
72
|
+
global _active_runs
|
|
73
|
+
with _lock:
|
|
74
|
+
_active_runs = max(0, _active_runs - 1)
|
|
75
|
+
last_one_out = _active_runs == 0
|
|
76
|
+
if last_one_out:
|
|
77
|
+
_stop_ticker()
|
|
78
|
+
_clear_prefix()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _start_ticker() -> None:
|
|
82
|
+
"""Start the ticker task (idempotent; no loop or no bar = no puppy)."""
|
|
83
|
+
global _ticker_task
|
|
84
|
+
try:
|
|
85
|
+
loop = asyncio.get_running_loop()
|
|
86
|
+
except RuntimeError:
|
|
87
|
+
return # sync/odd context: skip the animation entirely
|
|
88
|
+
try:
|
|
89
|
+
if not _bar().is_active():
|
|
90
|
+
return # headless / non-TTY: nothing to paint on
|
|
91
|
+
except Exception:
|
|
92
|
+
return
|
|
93
|
+
with _lock:
|
|
94
|
+
if _ticker_task is not None and not _ticker_task.done():
|
|
95
|
+
return
|
|
96
|
+
_ticker_task = loop.create_task(_tick_loop())
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _stop_ticker() -> None:
|
|
100
|
+
"""Cancel the ticker task if it's running. Idempotent."""
|
|
101
|
+
global _ticker_task
|
|
102
|
+
with _lock:
|
|
103
|
+
task = _ticker_task
|
|
104
|
+
_ticker_task = None
|
|
105
|
+
if task is not None and not task.done():
|
|
106
|
+
task.cancel()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _tick_loop() -> None:
|
|
110
|
+
"""Advance the puppy one cell per tick until no run is active.
|
|
111
|
+
|
|
112
|
+
Just the animation -- no "<puppy> is thinking..." chatter. The
|
|
113
|
+
status row is prime real estate; the token summary needs it more.
|
|
114
|
+
"""
|
|
115
|
+
global _ticker_task
|
|
116
|
+
frame = 0
|
|
117
|
+
try:
|
|
118
|
+
while True:
|
|
119
|
+
with _lock:
|
|
120
|
+
if _active_runs <= 0:
|
|
121
|
+
break # belt-and-braces: never outlive the run
|
|
122
|
+
frames, interval = _current_frames_and_interval()
|
|
123
|
+
_paint_prefix(frames[frame % len(frames)])
|
|
124
|
+
frame += 1
|
|
125
|
+
await asyncio.sleep(interval)
|
|
126
|
+
except asyncio.CancelledError:
|
|
127
|
+
pass
|
|
128
|
+
finally:
|
|
129
|
+
_clear_prefix()
|
|
130
|
+
with _lock:
|
|
131
|
+
if _ticker_task is asyncio.current_task():
|
|
132
|
+
_ticker_task = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _current_frames_and_interval():
|
|
136
|
+
"""The active spinner's frames + interval, re-read every tick.
|
|
137
|
+
|
|
138
|
+
The stock builtin puppy routes through the module constants so tests
|
|
139
|
+
(and nostalgic monkeypatchers) can tweak ``_TICK_INTERVAL_S``; any
|
|
140
|
+
other choice -- including a user-file override of "puppy" -- uses
|
|
141
|
+
the catalogue values. A broken catalogue degrades to the classic.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
active = spinners.get_active_spinner()
|
|
145
|
+
is_stock_puppy = (
|
|
146
|
+
active.name == spinners.DEFAULT_SPINNER
|
|
147
|
+
and active.source == "builtin"
|
|
148
|
+
and active.interval
|
|
149
|
+
== spinners.BUILTIN_SPINNERS[
|
|
150
|
+
spinners.DEFAULT_SPINNER
|
|
151
|
+
].interval # a speed override on the puppy still counts as custom
|
|
152
|
+
)
|
|
153
|
+
if not is_stock_puppy:
|
|
154
|
+
return active.frames, active.interval
|
|
155
|
+
except Exception:
|
|
156
|
+
logger.debug("active spinner lookup failed", exc_info=True)
|
|
157
|
+
return FRAMES, _TICK_INTERVAL_S
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
#: Paint-time gap after every spinner frame, including user-authored frames.
|
|
161
|
+
_PREFIX_GAP = " "
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _paint_prefix(text: str) -> None:
|
|
165
|
+
"""Best-effort paint -- a broken bar must never kill the ticker.
|
|
166
|
+
|
|
167
|
+
Non-empty frames get ``_PREFIX_GAP`` appended; clearing stays a
|
|
168
|
+
true empty string so nothing lingers in the prefix slot.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
_bar().set_status_prefix(text + _PREFIX_GAP if text else "")
|
|
172
|
+
except Exception:
|
|
173
|
+
logger.debug("puppy spinner paint failed", exc_info=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _clear_prefix() -> None:
|
|
177
|
+
_paint_prefix("")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
register_callback("agent_run_start", _on_run_start)
|
|
181
|
+
register_callback("agent_run_end", _on_run_end)
|
|
182
|
+
register_callback("custom_command", commands.handle_spinner)
|
|
183
|
+
register_callback("custom_command_help", commands.help_entries)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
__all__ = [
|
|
187
|
+
"FRAMES",
|
|
188
|
+
"_on_run_end",
|
|
189
|
+
"_on_run_start",
|
|
190
|
+
"_start_ticker",
|
|
191
|
+
"_stop_ticker",
|
|
192
|
+
"_tick_loop",
|
|
193
|
+
]
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Spinner catalogue + persistence for the ``puppy_spinner`` plugin.
|
|
2
|
+
|
|
3
|
+
Built-in spinners live here. Users add their own by dropping a
|
|
4
|
+
``spinners.json`` next to ``puppy.cfg`` (see ``USER_SPINNERS_FILE``)::
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"my-spinner": {
|
|
8
|
+
"frames": ["( . ) ", "( . ) ", "( . ) "],
|
|
9
|
+
"interval": 0.1,
|
|
10
|
+
"description": "a very minimalist puppy"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
User spinners win on name collision (so you *can* redefine "puppy",
|
|
15
|
+
you monster). An entry that names a builtin and omits ``frames`` is a
|
|
16
|
+
*tweak*: it inherits the builtin's frames and just overrides the
|
|
17
|
+
interval (and/or description) -- the cheap way to re-speed a builtin
|
|
18
|
+
without copying its art::
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
"zoomies": {"interval": 0.1}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
The active choice persists under the ``spinner_style`` key in
|
|
25
|
+
``puppy.cfg`` and is cached in-process; ``invalidate_cache()`` forces a
|
|
26
|
+
re-read after external edits. Speed has one rule: a spinner runs at its
|
|
27
|
+
spinners.json value when present, else the builtin default. Dialing a
|
|
28
|
+
speed in the picker (or ``/spinner <name> <secs>``) simply *saves* that
|
|
29
|
+
value into spinners.json -- user land keeps its own numbers, no
|
|
30
|
+
shadow-override layer to reason about.
|
|
31
|
+
|
|
32
|
+
Everything here fails gracefully: a broken JSON file or a vanished
|
|
33
|
+
spinner name always degrades to the classic kennel-bounce puppy.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
from dataclasses import dataclass, replace
|
|
42
|
+
from typing import Dict, Optional, Tuple
|
|
43
|
+
|
|
44
|
+
from rich.cells import cell_len
|
|
45
|
+
|
|
46
|
+
from code_puppy import atomic_io, atomic_json
|
|
47
|
+
from code_puppy.config import CONFIG_DIR, get_value, set_value
|
|
48
|
+
|
|
49
|
+
from .builtin_frames import EXTRA_SPECS
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
CONFIG_KEY = "spinner_style"
|
|
54
|
+
DEFAULT_SPINNER = "puppy"
|
|
55
|
+
USER_SPINNERS_FILE = os.path.join(CONFIG_DIR, "spinners.json")
|
|
56
|
+
|
|
57
|
+
MIN_INTERVAL = 0.02 # 50 fps -- any faster is just terminal abuse
|
|
58
|
+
MAX_INTERVAL = 1.0
|
|
59
|
+
#: One speed to rule them all: every builtin ships at this, and it's the
|
|
60
|
+
#: fallback when spinners.json omits (or fumbles) an interval.
|
|
61
|
+
_DEFAULT_INTERVAL = 0.2
|
|
62
|
+
#: ...except the classic. The kennel bounce was tuned for a quicker trot
|
|
63
|
+
#: (0.05s originally) and looks sluggish at the pack default.
|
|
64
|
+
_PUPPY_INTERVAL = 0.06
|
|
65
|
+
_MAX_FRAME_LEN = 40 # the status-prefix slot is prime real estate
|
|
66
|
+
|
|
67
|
+
# Escape-spelled glyphs (repo emoji filter).
|
|
68
|
+
PUPPY = "\U0001f436" # dog face
|
|
69
|
+
_DOG = "\U0001f415" # dog (side view, runs left)
|
|
70
|
+
_DASH = "\U0001f4a8" # dust cloud
|
|
71
|
+
_PAW = "\U0001f43e" # paw prints
|
|
72
|
+
_BONE = "\U0001f9b4" # bone
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Spinner:
|
|
77
|
+
"""One spinner: a frame cycle, a tick interval, and provenance."""
|
|
78
|
+
|
|
79
|
+
name: str
|
|
80
|
+
frames: Tuple[str, ...]
|
|
81
|
+
interval: float
|
|
82
|
+
description: str = ""
|
|
83
|
+
source: str = "builtin" # "builtin" | "user"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _pad(frames) -> Tuple[str, ...]:
|
|
87
|
+
"""Right-pad every frame to a common *cell* width so text after the
|
|
88
|
+
spinner never jumps.
|
|
89
|
+
|
|
90
|
+
``len()`` is a dirty liar for emoji: a paw print is one code point
|
|
91
|
+
but TWO terminal cells. Padding by ``rich.cells.cell_len`` keeps the
|
|
92
|
+
rendered width constant even when frames mix emoji and ASCII.
|
|
93
|
+
"""
|
|
94
|
+
frames = tuple(frames) # materialize: generators only pour out once
|
|
95
|
+
width = max(cell_len(f) for f in frames)
|
|
96
|
+
return tuple(f + " " * (width - cell_len(f)) for f in frames)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _kennel_bounce(critter: str) -> Tuple[str, ...]:
|
|
100
|
+
"""The classic 5-cell kennel bounce: positions 0..4 and back again."""
|
|
101
|
+
return tuple(
|
|
102
|
+
f"({' ' * i}{critter}{' ' * (4 - i)}) " for i in (0, 1, 2, 3, 4, 3, 2, 1)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# name -> (frames, interval, description)
|
|
107
|
+
_BUILTIN_SPECS = {
|
|
108
|
+
"puppy": (
|
|
109
|
+
_kennel_bounce(PUPPY),
|
|
110
|
+
_PUPPY_INTERVAL,
|
|
111
|
+
"the classic kennel bounce (default)",
|
|
112
|
+
),
|
|
113
|
+
"bone": (
|
|
114
|
+
_kennel_bounce(_BONE),
|
|
115
|
+
_DEFAULT_INTERVAL,
|
|
116
|
+
"same kennel, chewier occupant",
|
|
117
|
+
),
|
|
118
|
+
"zoomies": (
|
|
119
|
+
tuple(f"{' ' * i}{_DOG}{_DASH}{' ' * (5 - i)}" for i in (5, 4, 3, 2, 1, 0)),
|
|
120
|
+
_DEFAULT_INTERVAL,
|
|
121
|
+
"full-speed dog, dust trailing",
|
|
122
|
+
),
|
|
123
|
+
"paws": (
|
|
124
|
+
tuple(_PAW * n for n in (1, 2, 3, 4)), # _pad cell-aligns the trail
|
|
125
|
+
_DEFAULT_INTERVAL,
|
|
126
|
+
"a trail of paw prints",
|
|
127
|
+
),
|
|
128
|
+
"dots": (
|
|
129
|
+
tuple(
|
|
130
|
+
chr(cp)
|
|
131
|
+
for cp in (
|
|
132
|
+
0x280B,
|
|
133
|
+
0x2819,
|
|
134
|
+
0x2839,
|
|
135
|
+
0x2838,
|
|
136
|
+
0x283C,
|
|
137
|
+
0x2834,
|
|
138
|
+
0x2826,
|
|
139
|
+
0x2827,
|
|
140
|
+
0x2807,
|
|
141
|
+
0x280F,
|
|
142
|
+
)
|
|
143
|
+
),
|
|
144
|
+
_DEFAULT_INTERVAL,
|
|
145
|
+
"classic braille dots",
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# The cli-spinners-style pack (frames + blurbs live in ``builtin_frames``,
|
|
150
|
+
# a generated data-only module). Same default speed as everything else.
|
|
151
|
+
_BUILTIN_SPECS.update(
|
|
152
|
+
{
|
|
153
|
+
name: (frames, _DEFAULT_INTERVAL, desc)
|
|
154
|
+
for name, (frames, desc) in EXTRA_SPECS.items()
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
BUILTIN_SPINNERS: Dict[str, Spinner] = {
|
|
159
|
+
name: Spinner(name=name, frames=_pad(frames), interval=interval, description=desc)
|
|
160
|
+
for name, (frames, interval, desc) in _BUILTIN_SPECS.items()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- User spinners (external JSON file) --------------------------------------
|
|
165
|
+
def _parse_interval(name: str, spec: dict, default: float) -> float:
|
|
166
|
+
"""Validate + clamp a spec's 'interval', falling back to *default*."""
|
|
167
|
+
interval = spec.get("interval", default)
|
|
168
|
+
# bool check first: JSON `true` is an int subclass, and float(True)
|
|
169
|
+
# is a surprise 1-second spinner. No booleans, thanks.
|
|
170
|
+
if (
|
|
171
|
+
isinstance(interval, bool)
|
|
172
|
+
or not isinstance(interval, (int, float))
|
|
173
|
+
or interval <= 0
|
|
174
|
+
):
|
|
175
|
+
logger.warning(
|
|
176
|
+
"spinners.json: %r has a bad 'interval'; using %s", name, default
|
|
177
|
+
)
|
|
178
|
+
interval = default
|
|
179
|
+
return clamp_interval(float(interval))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _parse_builtin_tweak(name: str, spec: dict) -> Optional[Spinner]:
|
|
183
|
+
"""A frameless entry naming a builtin: inherit its frames, override
|
|
184
|
+
the rest. The cheap way to re-speed a builtin without copying art.
|
|
185
|
+
"""
|
|
186
|
+
if "interval" not in spec and "description" not in spec:
|
|
187
|
+
logger.warning(
|
|
188
|
+
"spinners.json: %r tweaks builtin %r but overrides nothing "
|
|
189
|
+
"(add 'interval' or 'description'); skipping",
|
|
190
|
+
name,
|
|
191
|
+
name,
|
|
192
|
+
)
|
|
193
|
+
return None
|
|
194
|
+
base = BUILTIN_SPINNERS[name]
|
|
195
|
+
description = spec.get("description", base.description)
|
|
196
|
+
if not isinstance(description, str):
|
|
197
|
+
description = base.description
|
|
198
|
+
return Spinner(
|
|
199
|
+
name=name,
|
|
200
|
+
frames=base.frames,
|
|
201
|
+
interval=_parse_interval(name, spec, base.interval),
|
|
202
|
+
description=description,
|
|
203
|
+
source="builtin+user",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_user_spinner(name: str, spec: object) -> Optional[Spinner]:
|
|
208
|
+
"""Validate one user entry. Returns None (and logs) on any problem."""
|
|
209
|
+
if not isinstance(name, str) or not name.strip():
|
|
210
|
+
logger.warning("spinners.json: skipping entry with a blank name")
|
|
211
|
+
return None
|
|
212
|
+
if not isinstance(spec, dict):
|
|
213
|
+
logger.warning("spinners.json: %r is not an object; skipping", name)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
raw_frames = spec.get("frames")
|
|
217
|
+
if raw_frames is None and name in BUILTIN_SPINNERS:
|
|
218
|
+
return _parse_builtin_tweak(name, spec)
|
|
219
|
+
if (
|
|
220
|
+
not isinstance(raw_frames, list)
|
|
221
|
+
or not raw_frames
|
|
222
|
+
or not all(isinstance(f, str) and f for f in raw_frames)
|
|
223
|
+
):
|
|
224
|
+
logger.warning(
|
|
225
|
+
"spinners.json: %r needs a non-empty 'frames' list of strings; skipping",
|
|
226
|
+
name,
|
|
227
|
+
)
|
|
228
|
+
return None
|
|
229
|
+
frames = _pad(f[:_MAX_FRAME_LEN] for f in raw_frames)
|
|
230
|
+
|
|
231
|
+
description = spec.get("description", "")
|
|
232
|
+
if not isinstance(description, str):
|
|
233
|
+
description = ""
|
|
234
|
+
|
|
235
|
+
return Spinner(
|
|
236
|
+
name=name.strip(),
|
|
237
|
+
frames=frames,
|
|
238
|
+
interval=_parse_interval(name, spec, _DEFAULT_INTERVAL),
|
|
239
|
+
description=description,
|
|
240
|
+
source="user",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class _SpinnersNotADict(Exception):
|
|
245
|
+
"""Sentinel: spinners.json parsed to something other than a dict."""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_user_spinners() -> Dict[str, Spinner]:
|
|
249
|
+
"""Read + validate the user spinner file. Missing/broken file = {}.
|
|
250
|
+
|
|
251
|
+
Bounded and lock-aware via :mod:`code_puppy.atomic_json` -- a large or
|
|
252
|
+
concurrently-being-written file can no longer balloon memory or read a
|
|
253
|
+
torn write.
|
|
254
|
+
"""
|
|
255
|
+
try:
|
|
256
|
+
data = atomic_json.load_json(USER_SPINNERS_FILE, default={})
|
|
257
|
+
except (OSError, atomic_json.JsonFileCorrupt) as exc:
|
|
258
|
+
logger.warning("Could not read %s: %s", USER_SPINNERS_FILE, exc)
|
|
259
|
+
return {}
|
|
260
|
+
if not isinstance(data, dict):
|
|
261
|
+
logger.warning("%s must be a JSON object; ignoring", USER_SPINNERS_FILE)
|
|
262
|
+
return {}
|
|
263
|
+
|
|
264
|
+
spinners: Dict[str, Spinner] = {}
|
|
265
|
+
for name, spec in data.items():
|
|
266
|
+
spinner = _parse_user_spinner(name, spec)
|
|
267
|
+
if spinner is not None:
|
|
268
|
+
spinners[spinner.name] = spinner
|
|
269
|
+
return spinners
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def get_catalogue() -> Dict[str, Spinner]:
|
|
273
|
+
"""Builtin + user spinners, user winning on name collision.
|
|
274
|
+
|
|
275
|
+
Sorted alphabetically (case-insensitive) -- every consumer iterates
|
|
276
|
+
this, so ordering lives here once.
|
|
277
|
+
"""
|
|
278
|
+
catalogue = dict(BUILTIN_SPINNERS)
|
|
279
|
+
for name, spinner in load_user_spinners().items():
|
|
280
|
+
if name in catalogue:
|
|
281
|
+
logger.info("user spinner %r overrides the builtin one", name)
|
|
282
|
+
catalogue[name] = spinner
|
|
283
|
+
return dict(sorted(catalogue.items(), key=lambda kv: kv[0].lower()))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# --- Saving a speed back into user land ---------------------------------------
|
|
287
|
+
def clamp_interval(value: float) -> float:
|
|
288
|
+
"""Clamp *value* into the sane range and round to whole centiseconds."""
|
|
289
|
+
return round(min(max(value, MIN_INTERVAL), MAX_INTERVAL), 2)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def save_interval_tweak(name: str, interval: float) -> bool:
|
|
293
|
+
"""Persist *interval* for *name* into spinners.json (creating the
|
|
294
|
+
file if needed).
|
|
295
|
+
|
|
296
|
+
In user land the file *is* the truth: a dialed speed is just a saved
|
|
297
|
+
value, not a separate override layer. Existing entry keys (frames,
|
|
298
|
+
description) are preserved; only ``interval`` changes.
|
|
299
|
+
|
|
300
|
+
Returns False (and logs) when the file can't be read or written --
|
|
301
|
+
we never overwrite a file we couldn't parse. Uses
|
|
302
|
+
:func:`code_puppy.atomic_json.mutate_json` for a bounded, locked,
|
|
303
|
+
atomically-written read-modify-write.
|
|
304
|
+
"""
|
|
305
|
+
interval = clamp_interval(interval)
|
|
306
|
+
|
|
307
|
+
def _mutate(data):
|
|
308
|
+
if not isinstance(data, dict):
|
|
309
|
+
raise _SpinnersNotADict()
|
|
310
|
+
entry = data.get(name)
|
|
311
|
+
if not isinstance(entry, dict):
|
|
312
|
+
entry = {}
|
|
313
|
+
entry["interval"] = interval
|
|
314
|
+
data[name] = entry
|
|
315
|
+
return data
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
atomic_json.mutate_json(USER_SPINNERS_FILE, _mutate, default={})
|
|
319
|
+
except _SpinnersNotADict:
|
|
320
|
+
logger.warning("Not saving speed: %s is not a JSON object", USER_SPINNERS_FILE)
|
|
321
|
+
return False
|
|
322
|
+
except (OSError, atomic_json.JsonFileCorrupt) as exc:
|
|
323
|
+
logger.warning("Could not save speed to %s: %s", USER_SPINNERS_FILE, exc)
|
|
324
|
+
return False
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Active spinner cache avoids parsing config/JSON every tick and invalidates on
|
|
329
|
+
# spinners.json mtime changes, so external edits apply without a reload.
|
|
330
|
+
_active_cache: Optional[Spinner] = None
|
|
331
|
+
_active_cache_stamp: Optional[float] = None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def user_file_stamp() -> Optional[float]:
|
|
335
|
+
"""spinners.json's mtime, or None when the file doesn't exist.
|
|
336
|
+
|
|
337
|
+
Public because the picker watches it too: both the tick loop and the
|
|
338
|
+
open TUI use the same signal to notice external edits.
|
|
339
|
+
"""
|
|
340
|
+
try:
|
|
341
|
+
return os.path.getmtime(USER_SPINNERS_FILE)
|
|
342
|
+
except OSError:
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _load_active() -> Spinner:
|
|
347
|
+
try:
|
|
348
|
+
name = get_value(CONFIG_KEY)
|
|
349
|
+
except Exception: # config layer must never break the spinner
|
|
350
|
+
name = None
|
|
351
|
+
name = (name or DEFAULT_SPINNER).strip()
|
|
352
|
+
catalogue = get_catalogue()
|
|
353
|
+
if name not in catalogue:
|
|
354
|
+
if name != DEFAULT_SPINNER:
|
|
355
|
+
logger.warning(
|
|
356
|
+
"Configured spinner %r not found; falling back to %r",
|
|
357
|
+
name,
|
|
358
|
+
DEFAULT_SPINNER,
|
|
359
|
+
)
|
|
360
|
+
spinner = BUILTIN_SPINNERS[DEFAULT_SPINNER]
|
|
361
|
+
else:
|
|
362
|
+
spinner = catalogue[name]
|
|
363
|
+
return spinner
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def get_active_spinner() -> Spinner:
|
|
367
|
+
"""The currently selected spinner.
|
|
368
|
+
|
|
369
|
+
Cached, but re-read whenever spinners.json changes on disk (an
|
|
370
|
+
os.stat per call -- cheap enough for the ~5 Hz tick loop).
|
|
371
|
+
"""
|
|
372
|
+
global _active_cache, _active_cache_stamp
|
|
373
|
+
stamp = user_file_stamp()
|
|
374
|
+
if _active_cache is None or stamp != _active_cache_stamp:
|
|
375
|
+
_active_cache = _load_active()
|
|
376
|
+
_active_cache_stamp = stamp
|
|
377
|
+
return _active_cache
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def set_active(name: str, interval: Optional[float] = None) -> Spinner:
|
|
381
|
+
"""Persist + activate *name*, optionally saving a custom speed.
|
|
382
|
+
|
|
383
|
+
A given *interval* is written straight into spinners.json as that
|
|
384
|
+
spinner's speed -- the file is the single source of truth, so the
|
|
385
|
+
value sticks until the user changes it again (there is no separate
|
|
386
|
+
override to clear). Raises KeyError for unknown spinners.
|
|
387
|
+
"""
|
|
388
|
+
global _active_cache, _active_cache_stamp
|
|
389
|
+
catalogue = get_catalogue()
|
|
390
|
+
if name not in catalogue:
|
|
391
|
+
raise KeyError(name)
|
|
392
|
+
set_value(CONFIG_KEY, name)
|
|
393
|
+
spinner = catalogue[name]
|
|
394
|
+
if interval is not None:
|
|
395
|
+
interval = clamp_interval(interval)
|
|
396
|
+
if save_interval_tweak(name, interval):
|
|
397
|
+
spinner = get_catalogue()[name] # re-read: the file is the truth
|
|
398
|
+
else:
|
|
399
|
+
# Couldn't write (broken/readonly file): honor the speed for
|
|
400
|
+
# this session rather than silently dropping the request.
|
|
401
|
+
spinner = replace(spinner, interval=interval)
|
|
402
|
+
_active_cache = spinner
|
|
403
|
+
_active_cache_stamp = user_file_stamp()
|
|
404
|
+
return _active_cache
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def invalidate_cache() -> None:
|
|
408
|
+
"""Drop the cache so the next read re-checks config + spinners.json.
|
|
409
|
+
|
|
410
|
+
Rarely needed -- file edits are caught automatically via mtime; this
|
|
411
|
+
covers out-of-band ``puppy.cfg`` changes (and test isolation).
|
|
412
|
+
"""
|
|
413
|
+
global _active_cache, _active_cache_stamp
|
|
414
|
+
_active_cache = None
|
|
415
|
+
_active_cache_stamp = None
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# --- Starter template for /spinner init --------------------------------------
|
|
419
|
+
_TEMPLATE = {
|
|
420
|
+
"sniffer": {
|
|
421
|
+
"frames": ["( . ) ", "( . ) ", "( . ) ", "( . ) "],
|
|
422
|
+
"interval": 0.1,
|
|
423
|
+
"description": "a very minimalist puppy",
|
|
424
|
+
},
|
|
425
|
+
# A frameless entry naming a builtin just re-speeds it (this example
|
|
426
|
+
# matches the builtin default, so it's a safe demo -- edit away).
|
|
427
|
+
"zoomies": {"interval": 0.2},
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def write_template() -> bool:
|
|
432
|
+
"""Write a starter spinners.json. Returns False if one already exists.
|
|
433
|
+
|
|
434
|
+
Locked so two concurrent first-runs (e.g. two terminals) can't both
|
|
435
|
+
pass the existence check and race each other's write.
|
|
436
|
+
"""
|
|
437
|
+
with atomic_io.path_lock(USER_SPINNERS_FILE):
|
|
438
|
+
if os.path.isfile(USER_SPINNERS_FILE):
|
|
439
|
+
return False
|
|
440
|
+
atomic_io.atomic_write_bytes(
|
|
441
|
+
USER_SPINNERS_FILE,
|
|
442
|
+
(json.dumps(_TEMPLATE, indent=2) + "\n").encode("utf-8"),
|
|
443
|
+
)
|
|
444
|
+
return True
|
|
File without changes
|