shortcutxl 0.2.0
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.
- package/README.md +59 -0
- package/agent-docs/README.md +397 -0
- package/agent-docs/docs/compaction.md +390 -0
- package/agent-docs/docs/custom-provider.md +580 -0
- package/agent-docs/docs/development.md +69 -0
- package/agent-docs/docs/extensions.md +1971 -0
- package/agent-docs/docs/json.md +79 -0
- package/agent-docs/docs/keybindings.md +174 -0
- package/agent-docs/docs/models.md +293 -0
- package/agent-docs/docs/packages.md +209 -0
- package/agent-docs/docs/prompt-templates.md +67 -0
- package/agent-docs/docs/providers.md +186 -0
- package/agent-docs/docs/rpc.md +1317 -0
- package/agent-docs/docs/sdk.md +962 -0
- package/agent-docs/docs/session.md +412 -0
- package/agent-docs/docs/settings.md +223 -0
- package/agent-docs/docs/shell-aliases.md +13 -0
- package/agent-docs/docs/skills.md +231 -0
- package/agent-docs/docs/terminal-setup.md +70 -0
- package/agent-docs/docs/termux.md +127 -0
- package/agent-docs/docs/themes.md +295 -0
- package/agent-docs/docs/tree.md +219 -0
- package/agent-docs/docs/tui.md +887 -0
- package/agent-docs/docs/windows.md +17 -0
- package/agent-docs/examples/README.md +25 -0
- package/agent-docs/examples/extensions/README.md +205 -0
- package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -0
- package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -0
- package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -0
- package/agent-docs/examples/extensions/bookmark.ts +50 -0
- package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -0
- package/agent-docs/examples/extensions/claude-rules.ts +86 -0
- package/agent-docs/examples/extensions/commands.ts +75 -0
- package/agent-docs/examples/extensions/confirm-destructive.ts +59 -0
- package/agent-docs/examples/extensions/custom-compaction.ts +126 -0
- package/agent-docs/examples/extensions/custom-footer.ts +63 -0
- package/agent-docs/examples/extensions/custom-header.ts +73 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
- package/agent-docs/examples/extensions/custom-provider-anthropic/package.json +19 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/package.json +16 -0
- package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -0
- package/agent-docs/examples/extensions/custom-provider-qwen-cli/package.json +16 -0
- package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -0
- package/agent-docs/examples/extensions/doom-overlay/README.md +46 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/build.sh +152 -0
- package/agent-docs/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-engine.ts +186 -0
- package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -0
- package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -0
- package/agent-docs/examples/extensions/doom-overlay/wad-finder.ts +51 -0
- package/agent-docs/examples/extensions/dynamic-resources/SKILL.md +8 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.json +79 -0
- package/agent-docs/examples/extensions/dynamic-resources/dynamic.md +5 -0
- package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -0
- package/agent-docs/examples/extensions/dynamic-tools.ts +77 -0
- package/agent-docs/examples/extensions/event-bus.ts +43 -0
- package/agent-docs/examples/extensions/file-trigger.ts +41 -0
- package/agent-docs/examples/extensions/git-checkpoint.ts +53 -0
- package/agent-docs/examples/extensions/handoff.ts +155 -0
- package/agent-docs/examples/extensions/hello.ts +25 -0
- package/agent-docs/examples/extensions/inline-bash.ts +94 -0
- package/agent-docs/examples/extensions/input-transform.ts +43 -0
- package/agent-docs/examples/extensions/interactive-shell.ts +209 -0
- package/agent-docs/examples/extensions/mac-system-theme.ts +47 -0
- package/agent-docs/examples/extensions/message-renderer.ts +59 -0
- package/agent-docs/examples/extensions/minimal-mode.ts +430 -0
- package/agent-docs/examples/extensions/modal-editor.ts +90 -0
- package/agent-docs/examples/extensions/model-status.ts +31 -0
- package/agent-docs/examples/extensions/notify.ts +55 -0
- package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -0
- package/agent-docs/examples/extensions/overlay-test.ts +159 -0
- package/agent-docs/examples/extensions/permission-gate.ts +37 -0
- package/agent-docs/examples/extensions/pirate.ts +47 -0
- package/agent-docs/examples/extensions/plan-mode/README.md +65 -0
- package/agent-docs/examples/extensions/plan-mode/index.ts +363 -0
- package/agent-docs/examples/extensions/plan-mode/utils.ts +173 -0
- package/agent-docs/examples/extensions/preset.ts +418 -0
- package/agent-docs/examples/extensions/protected-paths.ts +30 -0
- package/agent-docs/examples/extensions/qna.ts +122 -0
- package/agent-docs/examples/extensions/question.ts +278 -0
- package/agent-docs/examples/extensions/questionnaire.ts +440 -0
- package/agent-docs/examples/extensions/rainbow-editor.ts +90 -0
- package/agent-docs/examples/extensions/reload-runtime.ts +37 -0
- package/agent-docs/examples/extensions/rpc-demo.ts +124 -0
- package/agent-docs/examples/extensions/sandbox/index.ts +324 -0
- package/agent-docs/examples/extensions/sandbox/package-lock.json +92 -0
- package/agent-docs/examples/extensions/sandbox/package.json +19 -0
- package/agent-docs/examples/extensions/send-user-message.ts +97 -0
- package/agent-docs/examples/extensions/session-name.ts +27 -0
- package/agent-docs/examples/extensions/shutdown-command.ts +69 -0
- package/agent-docs/examples/extensions/snake.ts +343 -0
- package/agent-docs/examples/extensions/space-invaders.ts +566 -0
- package/agent-docs/examples/extensions/ssh.ts +233 -0
- package/agent-docs/examples/extensions/status-line.ts +40 -0
- package/agent-docs/examples/extensions/subagent/README.md +172 -0
- package/agent-docs/examples/extensions/subagent/agents/planner.md +37 -0
- package/agent-docs/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/agent-docs/examples/extensions/subagent/agents/scout.md +50 -0
- package/agent-docs/examples/extensions/subagent/agents/worker.md +24 -0
- package/agent-docs/examples/extensions/subagent/agents.ts +130 -0
- package/agent-docs/examples/extensions/subagent/index.ts +1068 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/implement.md +10 -0
- package/agent-docs/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/agent-docs/examples/extensions/summarize.ts +206 -0
- package/agent-docs/examples/extensions/system-prompt-header.ts +17 -0
- package/agent-docs/examples/extensions/timed-confirm.ts +72 -0
- package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -0
- package/agent-docs/examples/extensions/todo.ts +314 -0
- package/agent-docs/examples/extensions/tool-override.ts +146 -0
- package/agent-docs/examples/extensions/tools.ts +145 -0
- package/agent-docs/examples/extensions/trigger-compact.ts +40 -0
- package/agent-docs/examples/extensions/truncated-tool.ts +194 -0
- package/agent-docs/examples/extensions/widget-placement.ts +17 -0
- package/agent-docs/examples/extensions/with-deps/index.ts +37 -0
- package/agent-docs/examples/extensions/with-deps/package-lock.json +31 -0
- package/agent-docs/examples/extensions/with-deps/package.json +22 -0
- package/agent-docs/examples/rpc-extension-ui.ts +654 -0
- package/agent-docs/examples/sdk/01-minimal.ts +22 -0
- package/agent-docs/examples/sdk/02-custom-model.ts +48 -0
- package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -0
- package/agent-docs/examples/sdk/04-skills.ts +53 -0
- package/agent-docs/examples/sdk/05-tools.ts +56 -0
- package/agent-docs/examples/sdk/06-extensions.ts +88 -0
- package/agent-docs/examples/sdk/07-context-files.ts +40 -0
- package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -0
- package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -0
- package/agent-docs/examples/sdk/10-settings.ts +54 -0
- package/agent-docs/examples/sdk/11-sessions.ts +48 -0
- package/agent-docs/examples/sdk/12-full-control.ts +82 -0
- package/agent-docs/examples/sdk/README.md +144 -0
- package/agent-docs/xll-skill.md +61 -0
- package/agent-docs/xll-spec.md +110 -0
- package/dist/cli/args.js +290 -0
- package/dist/cli/config-selector.js +31 -0
- package/dist/cli/file-processor.js +79 -0
- package/dist/cli/list-models.js +92 -0
- package/dist/cli/package-commands.js +210 -0
- package/dist/cli/report-settings-errors.js +11 -0
- package/dist/cli/session-picker.js +34 -0
- package/dist/cli.js +19 -0
- package/dist/config.js +288 -0
- package/dist/core/abort.js +15 -0
- package/dist/core/agent-loop.js +352 -0
- package/dist/core/agent-session.js +2019 -0
- package/dist/core/agent.js +410 -0
- package/dist/core/auth-storage.js +456 -0
- package/dist/core/bash-executor.js +222 -0
- package/dist/core/compaction/branch-summarization.js +242 -0
- package/dist/core/compaction/compaction.js +610 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/utils.js +139 -0
- package/dist/core/defaults.js +6 -0
- package/dist/core/diagnostics.js +2 -0
- package/dist/core/event-bus.js +25 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/export-html/ansi-to-html.js +256 -0
- package/dist/core/export-html/index.js +238 -0
- package/dist/core/export-html/session-view-model.js +342 -0
- package/dist/core/export-html/template.css +1110 -0
- package/dist/core/export-html/template.html +76 -0
- package/dist/core/export-html/template.js +1990 -0
- package/dist/core/export-html/tool-renderer.js +63 -0
- package/dist/core/export-html/vendor/highlight.min.js +7725 -0
- package/dist/core/export-html/vendor/marked.min.js +1803 -0
- package/dist/core/extensions/index.js +9 -0
- package/dist/core/extensions/loader.js +422 -0
- package/dist/core/extensions/runner.js +651 -0
- package/dist/core/extensions/types.js +35 -0
- package/dist/core/extensions/wrapper.js +102 -0
- package/dist/core/footer-data-provider.js +162 -0
- package/dist/core/index.js +9 -0
- package/dist/core/keybindings.js +153 -0
- package/dist/core/messages.js +133 -0
- package/dist/core/model-registry.js +539 -0
- package/dist/core/model-resolver.js +370 -0
- package/dist/core/package-manager.js +1485 -0
- package/dist/core/prompt-templates.js +253 -0
- package/dist/core/resolve-config-value.js +59 -0
- package/dist/core/resource-loader.js +700 -0
- package/dist/core/sdk.js +197 -0
- package/dist/core/session-bash.js +99 -0
- package/dist/core/session-compaction.js +165 -0
- package/dist/core/session-manager.js +1153 -0
- package/dist/core/session-models.js +99 -0
- package/dist/core/session-retry.js +155 -0
- package/dist/core/settings-manager.js +572 -0
- package/dist/core/skills.js +382 -0
- package/dist/core/slash-commands.js +31 -0
- package/dist/core/system-prompt.js +161 -0
- package/dist/core/theme.js +770 -0
- package/dist/core/timings.js +26 -0
- package/dist/core/tools/bash.js +258 -0
- package/dist/core/tools/edit-diff.js +245 -0
- package/dist/core/tools/edit.js +148 -0
- package/dist/core/tools/find.js +208 -0
- package/dist/core/tools/grep.js +246 -0
- package/dist/core/tools/index.js +67 -0
- package/dist/core/tools/ls.js +123 -0
- package/dist/core/tools/path-utils.js +81 -0
- package/dist/core/tools/read.js +160 -0
- package/dist/core/tools/truncate.js +70 -0
- package/dist/core/tools/write.js +82 -0
- package/dist/custom/agents/action.js +13 -0
- package/dist/custom/agents/document-reader.js +70 -0
- package/dist/custom/agents/general.js +26 -0
- package/dist/custom/agents/index.js +49 -0
- package/dist/custom/agents/installation.js +13 -0
- package/dist/custom/agents/types.js +7 -0
- package/dist/custom/auth/refresh-timer.js +33 -0
- package/dist/custom/auth/shortcut-oauth.js +145 -0
- package/dist/custom/constants.js +21 -0
- package/dist/custom/context/workbook-summary.js +73 -0
- package/dist/custom/credits/shortcut-credits.js +29 -0
- package/dist/custom/cron/cron-daemon-entry.js +18 -0
- package/dist/custom/cron/daemon-ipc.js +131 -0
- package/dist/custom/cron/daemon.js +224 -0
- package/dist/custom/cron/jobs.js +226 -0
- package/dist/custom/cron/run-log.js +51 -0
- package/dist/custom/cron/schedule.js +72 -0
- package/dist/custom/cron/status-line.js +98 -0
- package/dist/custom/cron/store.js +87 -0
- package/dist/custom/cron/types.js +8 -0
- package/dist/custom/dev/index.js +59 -0
- package/dist/custom/dev/trace-export.js +58 -0
- package/dist/custom/ensure-excel.js +63 -0
- package/dist/custom/excel-config.js +36 -0
- package/dist/custom/preflight.js +422 -0
- package/dist/custom/prompts/action.js +100 -0
- package/dist/custom/prompts/api.js +66 -0
- package/dist/custom/prompts/installation.js +124 -0
- package/dist/custom/prompts/shared.js +138 -0
- package/dist/custom/providers/llm-usage.js +42 -0
- package/dist/custom/providers/message-converter.js +74 -0
- package/dist/custom/providers/provider-ids.js +9 -0
- package/dist/custom/providers/register-openai-codex-provider.js +27 -0
- package/dist/custom/providers/register-shortcut-provider.js +52 -0
- package/dist/custom/providers/shortcut-invoke.js +117 -0
- package/dist/custom/providers/shortcut-stream.js +252 -0
- package/dist/custom/providers/sse-protocol.js +38 -0
- package/dist/custom/sync-xll.js +130 -0
- package/dist/custom/tools/cron.js +413 -0
- package/dist/custom/tools/excel-exec.js +167 -0
- package/dist/custom/tools/excel-range.js +50 -0
- package/dist/custom/tools/llm-analysis.js +265 -0
- package/dist/custom/tools/render-helpers.js +38 -0
- package/dist/custom/tools/switch-mode.js +94 -0
- package/dist/custom/tools/task/agents.js +6 -0
- package/dist/custom/tools/task/index.js +8 -0
- package/dist/custom/tools/task/render.js +348 -0
- package/dist/custom/tools/task/subprocess.js +320 -0
- package/dist/custom/tools/task/task.js +205 -0
- package/dist/custom/tools/todo-list.js +195 -0
- package/dist/custom/tracing/session-upload.js +93 -0
- package/dist/index.js +45 -0
- package/dist/main.js +613 -0
- package/dist/migrations.js +265 -0
- package/dist/modes/index.js +8 -0
- package/dist/modes/interactive/components/armin.js +337 -0
- package/dist/modes/interactive/components/assistant-message.js +94 -0
- package/dist/modes/interactive/components/bash-execution.js +171 -0
- package/dist/modes/interactive/components/bordered-loader.js +51 -0
- package/dist/modes/interactive/components/branch-summary-message.js +45 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +46 -0
- package/dist/modes/interactive/components/config-selector.js +488 -0
- package/dist/modes/interactive/components/countdown-timer.js +33 -0
- package/dist/modes/interactive/components/custom-editor.js +93 -0
- package/dist/modes/interactive/components/custom-message.js +81 -0
- package/dist/modes/interactive/components/daxnuts.js +140 -0
- package/dist/modes/interactive/components/diff.js +133 -0
- package/dist/modes/interactive/components/dynamic-border.js +21 -0
- package/dist/modes/interactive/components/extension-editor.js +105 -0
- package/dist/modes/interactive/components/extension-input.js +61 -0
- package/dist/modes/interactive/components/extension-selector.js +78 -0
- package/dist/modes/interactive/components/footer.js +309 -0
- package/dist/modes/interactive/components/index.js +33 -0
- package/dist/modes/interactive/components/keybinding-hints.js +61 -0
- package/dist/modes/interactive/components/layout.js +64 -0
- package/dist/modes/interactive/components/login-dialog.js +148 -0
- package/dist/modes/interactive/components/model-selector.js +237 -0
- package/dist/modes/interactive/components/oauth-selector.js +111 -0
- package/dist/modes/interactive/components/session-selector-search.js +157 -0
- package/dist/modes/interactive/components/session-selector.js +860 -0
- package/dist/modes/interactive/components/settings-selector.js +123 -0
- package/dist/modes/interactive/components/show-images-selector.js +35 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +48 -0
- package/dist/modes/interactive/components/theme-selector.js +47 -0
- package/dist/modes/interactive/components/thinking-selector.js +47 -0
- package/dist/modes/interactive/components/tool-execution.js +789 -0
- package/dist/modes/interactive/components/tool-group.js +106 -0
- package/dist/modes/interactive/components/tree-selector.js +962 -0
- package/dist/modes/interactive/components/user-message-selector.js +115 -0
- package/dist/modes/interactive/components/user-message.js +48 -0
- package/dist/modes/interactive/components/visual-truncate.js +33 -0
- package/dist/modes/interactive/file-attachments.js +135 -0
- package/dist/modes/interactive/interactive-mode.js +3775 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +85 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.js +177 -0
- package/dist/modes/print-mode.js +101 -0
- package/dist/modes/rpc/rpc-client.js +387 -0
- package/dist/modes/rpc/rpc-mode.js +509 -0
- package/dist/modes/rpc/rpc-types.js +8 -0
- package/dist/subagent-entry.js +145 -0
- package/dist/tool-names.js +34 -0
- package/dist/tui/autocomplete.js +596 -0
- package/dist/tui/components/box.js +104 -0
- package/dist/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/components/editor.js +1679 -0
- package/dist/tui/components/image.js +69 -0
- package/dist/tui/components/input.js +433 -0
- package/dist/tui/components/loader.js +49 -0
- package/dist/tui/components/markdown.js +629 -0
- package/dist/tui/components/select-list.js +152 -0
- package/dist/tui/components/settings-list.js +185 -0
- package/dist/tui/components/spacer.js +23 -0
- package/dist/tui/components/text.js +89 -0
- package/dist/tui/components/truncated-text.js +51 -0
- package/dist/tui/editor-component.js +2 -0
- package/dist/tui/fuzzy.js +107 -0
- package/dist/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/index.js +32 -0
- package/dist/tui/keybindings.js +114 -0
- package/dist/tui/keys.js +959 -0
- package/dist/tui/kill-ring.js +44 -0
- package/dist/tui/stdin-buffer.js +317 -0
- package/dist/tui/terminal-image.js +288 -0
- package/dist/tui/terminal.js +249 -0
- package/dist/tui/tui/autocomplete.js +596 -0
- package/dist/tui/tui/components/box.js +106 -0
- package/dist/tui/tui/components/cancellable-loader.js +35 -0
- package/dist/tui/tui/components/editor.js +1679 -0
- package/dist/tui/tui/components/image.js +69 -0
- package/dist/tui/tui/components/input.js +433 -0
- package/dist/tui/tui/components/loader.js +49 -0
- package/dist/tui/tui/components/markdown.js +629 -0
- package/dist/tui/tui/components/select-list.js +152 -0
- package/dist/tui/tui/components/settings-list.js +185 -0
- package/dist/tui/tui/components/spacer.js +23 -0
- package/dist/tui/tui/components/text.js +91 -0
- package/dist/tui/tui/components/truncated-text.js +51 -0
- package/dist/tui/tui/editor-component.js +2 -0
- package/dist/tui/tui/fuzzy.js +107 -0
- package/dist/tui/tui/get-east-asian-width/index.js +32 -0
- package/dist/tui/tui/get-east-asian-width/lookup.js +404 -0
- package/dist/tui/tui/index.js +32 -0
- package/dist/tui/tui/keybindings.js +114 -0
- package/dist/tui/tui/keys.js +959 -0
- package/dist/tui/tui/kill-ring.js +44 -0
- package/dist/tui/tui/stdin-buffer.js +317 -0
- package/dist/tui/tui/terminal-image.js +288 -0
- package/dist/tui/tui/terminal.js +249 -0
- package/dist/tui/tui/tui.js +955 -0
- package/dist/tui/tui/undo-stack.js +25 -0
- package/dist/tui/tui/utils.js +800 -0
- package/dist/tui/tui.js +955 -0
- package/dist/tui/undo-stack.js +25 -0
- package/dist/tui/utils.js +800 -0
- package/dist/utils/changelog.js +87 -0
- package/dist/utils/clipboard-image.js +164 -0
- package/dist/utils/clipboard-native.js +14 -0
- package/dist/utils/clipboard.js +67 -0
- package/dist/utils/frontmatter.js +26 -0
- package/dist/utils/git.js +166 -0
- package/dist/utils/image-convert.js +35 -0
- package/dist/utils/image-resize.js +183 -0
- package/dist/utils/mime.js +26 -0
- package/dist/utils/photon.js +121 -0
- package/dist/utils/shell.js +217 -0
- package/dist/utils/sleep.js +17 -0
- package/dist/utils/tools-manager.js +259 -0
- package/package.json +78 -0
- package/skills/excel-com-api/SKILL.md +74 -0
- package/skills/excel-com-api/excel-type-library.py +27767 -0
- package/skills/excel-com-api/office-type-library.py +10867 -0
- package/skills/integrations/SKILL.md +138 -0
- package/skills/integrations/alphasense.md +457 -0
- package/skills/integrations/bloomberg.md +803 -0
- package/skills/integrations/calcbench.md +315 -0
- package/skills/integrations/capiq.md +848 -0
- package/skills/integrations/dynamics-365-finance.md +354 -0
- package/skills/integrations/earnings_transcripts.md +387 -0
- package/skills/integrations/factset.md +758 -0
- package/skills/integrations/ice-fixed-income.md +344 -0
- package/skills/integrations/moodys-analytics.md +313 -0
- package/skills/integrations/morningstar.md +433 -0
- package/skills/integrations/nasdaq-data-link.md +249 -0
- package/skills/integrations/pitchbook.md +413 -0
- package/skills/integrations/preqin.md +422 -0
- package/skills/integrations/quickbooks.md +289 -0
- package/skills/integrations/quickfs.md +314 -0
- package/skills/integrations/refinitiv.md +473 -0
- package/skills/integrations/sage-intacct.md +401 -0
- package/skills/integrations/visible-alpha.md +320 -0
- package/skills/integrations/xero.md +393 -0
- package/skills/integrations/ycharts.md +306 -0
- package/skills/pdf-creation/SKILL.md +93 -0
- package/skills/pdf-extraction/SKILL.md +32 -0
- package/skills/powerpoint-creation/SKILL.md +110 -0
- package/skills/sec-edgar/SKILL.md +127 -0
- package/skills/sec-edgar/sec_to_pdf.py +109 -0
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/debug_render.py +272 -0
- package/xll/modules/gameboy.py +241 -0
- package/xll/modules/pong.py +188 -0
- package/xll/modules/shortcut_xl/__init__.py +18 -0
- package/xll/modules/shortcut_xl/_categorize.py +200 -0
- package/xll/modules/shortcut_xl/_com.py +108 -0
- package/xll/modules/shortcut_xl/_format.py +252 -0
- package/xll/modules/shortcut_xl/_log.py +12 -0
- package/xll/modules/shortcut_xl/_managed.py +116 -0
- package/xll/modules/shortcut_xl/_registry.py +44 -0
- package/xll/modules/shortcut_xl/_threading.py +161 -0
- package/xll/modules/shortcut_xl/_tracking.py +283 -0
- package/xll/modules/stocks.py +100 -0
- package/xll/python3.dll +0 -0
- package/xll/python312.dll +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pong in Excel — type =pong_start() to play!
|
|
3
|
+
|
|
4
|
+
Two AI paddles rally a ball across a 30x20 cell grid.
|
|
5
|
+
Type =pong_stop() to end the game.
|
|
6
|
+
"""
|
|
7
|
+
import shortcut_xl
|
|
8
|
+
from shortcut_xl import xl_func
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
WIDTH = 30
|
|
12
|
+
HEIGHT = 20
|
|
13
|
+
PADDLE_H = 4
|
|
14
|
+
FPS = 30
|
|
15
|
+
|
|
16
|
+
# Grid occupies A3:AD22 (20 rows x 30 cols)
|
|
17
|
+
_GRID_RANGE = "A3:AD22"
|
|
18
|
+
|
|
19
|
+
_state = {
|
|
20
|
+
'ball_x': 15.0, 'ball_y': 10.0,
|
|
21
|
+
'ball_dx': 1.0, 'ball_dy': 0.7,
|
|
22
|
+
'paddle_l': 8, 'paddle_r': 8,
|
|
23
|
+
'score_l': 0, 'score_r': 0,
|
|
24
|
+
'running': False,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _reset_ball():
|
|
29
|
+
_state['ball_x'] = WIDTH / 2.0
|
|
30
|
+
_state['ball_y'] = HEIGHT / 2.0
|
|
31
|
+
_state['ball_dx'] = random.choice([-1.0, 1.0])
|
|
32
|
+
_state['ball_dy'] = random.uniform(-1.0, 1.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _render():
|
|
36
|
+
# Build the grid
|
|
37
|
+
grid = []
|
|
38
|
+
for r in range(HEIGHT):
|
|
39
|
+
row = []
|
|
40
|
+
for c in range(WIDTH):
|
|
41
|
+
ch = ""
|
|
42
|
+
# Left paddle
|
|
43
|
+
if c == 0 and _state['paddle_l'] <= r < _state['paddle_l'] + PADDLE_H:
|
|
44
|
+
ch = chr(9608) # full block
|
|
45
|
+
# Right paddle
|
|
46
|
+
elif c == WIDTH - 1 and _state['paddle_r'] <= r < _state['paddle_r'] + PADDLE_H:
|
|
47
|
+
ch = chr(9608)
|
|
48
|
+
# Ball
|
|
49
|
+
elif int(round(_state['ball_y'])) == r and int(round(_state['ball_x'])) == c:
|
|
50
|
+
ch = chr(9679) # black circle
|
|
51
|
+
# Center line
|
|
52
|
+
elif c == WIDTH // 2 and r % 2 == 0:
|
|
53
|
+
ch = chr(9474) # light vertical
|
|
54
|
+
row.append(ch)
|
|
55
|
+
grid.append(row)
|
|
56
|
+
|
|
57
|
+
# Write entire frame in one batch — one repaint, one retry point
|
|
58
|
+
score = f"PONG {_state['score_l']} : {_state['score_r']}"
|
|
59
|
+
|
|
60
|
+
def _paint(app):
|
|
61
|
+
# Suppress auto-recalculation — without this every 600-cell write
|
|
62
|
+
# triggers a full recalc cycle that fights our rendering and causes
|
|
63
|
+
# the visible "flashing" effect.
|
|
64
|
+
app.Calculation = -4135 # xlCalculationManual
|
|
65
|
+
|
|
66
|
+
sheet = app.ActiveSheet
|
|
67
|
+
|
|
68
|
+
# One-time setup: make cells square-ish and use a monospace font
|
|
69
|
+
# so the Unicode glyphs (█ ● │) are actually visible.
|
|
70
|
+
if not _state.get('_formatted'):
|
|
71
|
+
_state['_formatted'] = True
|
|
72
|
+
area = sheet.Range(_GRID_RANGE)
|
|
73
|
+
area.ColumnWidth = 2.14
|
|
74
|
+
area.Font.Name = "Consolas"
|
|
75
|
+
area.Font.Size = 11
|
|
76
|
+
area.HorizontalAlignment = -4108 # xlCenter
|
|
77
|
+
|
|
78
|
+
# Grid (20x30) — use direct range address, NOT Resize()
|
|
79
|
+
# win32com Resize() is broken: returns only the bottom-right cell
|
|
80
|
+
grid_com = tuple(tuple(row) for row in grid)
|
|
81
|
+
sheet.Range(_GRID_RANGE).Value = grid_com
|
|
82
|
+
# Scoreboard + instructions
|
|
83
|
+
sheet.Range("A1").Value = score
|
|
84
|
+
sheet.Range("A2").Value = "pong_start() to play pong_stop() to end"
|
|
85
|
+
|
|
86
|
+
shortcut_xl.xl_batch(_paint)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _tick():
|
|
90
|
+
shortcut_xl.xl_log(f"_tick: running={_state['running']}")
|
|
91
|
+
if not _state['running']:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
_tick_inner()
|
|
96
|
+
shortcut_xl.xl_log("_tick: frame OK")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
shortcut_xl.xl_log(f"pong tick error: {e}")
|
|
99
|
+
import traceback
|
|
100
|
+
shortcut_xl.xl_log(traceback.format_exc())
|
|
101
|
+
_state['running'] = False
|
|
102
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _tick_inner():
|
|
107
|
+
s = _state
|
|
108
|
+
|
|
109
|
+
# Move ball
|
|
110
|
+
s['ball_x'] += s['ball_dx']
|
|
111
|
+
s['ball_y'] += s['ball_dy']
|
|
112
|
+
|
|
113
|
+
# Bounce top/bottom
|
|
114
|
+
if s['ball_y'] <= 0:
|
|
115
|
+
s['ball_y'] = 0
|
|
116
|
+
s['ball_dy'] = abs(s['ball_dy'])
|
|
117
|
+
elif s['ball_y'] >= HEIGHT - 1:
|
|
118
|
+
s['ball_y'] = HEIGHT - 1
|
|
119
|
+
s['ball_dy'] = -abs(s['ball_dy'])
|
|
120
|
+
|
|
121
|
+
# Left paddle check
|
|
122
|
+
if s['ball_x'] <= 1:
|
|
123
|
+
if s['paddle_l'] - 0.5 <= s['ball_y'] < s['paddle_l'] + PADDLE_H + 0.5:
|
|
124
|
+
s['ball_dx'] = abs(s['ball_dx'])
|
|
125
|
+
s['ball_x'] = 1
|
|
126
|
+
# Add spin based on where ball hit paddle
|
|
127
|
+
hit_pos = (s['ball_y'] - s['paddle_l']) / PADDLE_H
|
|
128
|
+
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
129
|
+
else:
|
|
130
|
+
s['score_r'] += 1
|
|
131
|
+
_reset_ball()
|
|
132
|
+
|
|
133
|
+
# Right paddle check
|
|
134
|
+
if s['ball_x'] >= WIDTH - 2:
|
|
135
|
+
if s['paddle_r'] - 0.5 <= s['ball_y'] < s['paddle_r'] + PADDLE_H + 0.5:
|
|
136
|
+
s['ball_dx'] = -abs(s['ball_dx'])
|
|
137
|
+
s['ball_x'] = WIDTH - 2
|
|
138
|
+
hit_pos = (s['ball_y'] - s['paddle_r']) / PADDLE_H
|
|
139
|
+
s['ball_dy'] = (hit_pos - 0.5) * 2.0
|
|
140
|
+
else:
|
|
141
|
+
s['score_l'] += 1
|
|
142
|
+
_reset_ball()
|
|
143
|
+
|
|
144
|
+
# AI: move paddles toward ball with slight delay
|
|
145
|
+
ball_y = s['ball_y']
|
|
146
|
+
for paddle in ('paddle_l', 'paddle_r'):
|
|
147
|
+
target = int(ball_y) - PADDLE_H // 2
|
|
148
|
+
diff = target - s[paddle]
|
|
149
|
+
# Add some imperfection
|
|
150
|
+
speed = 1 if abs(diff) < 3 else 2
|
|
151
|
+
if diff > 0:
|
|
152
|
+
s[paddle] = min(s[paddle] + speed, HEIGHT - PADDLE_H)
|
|
153
|
+
elif diff < 0:
|
|
154
|
+
s[paddle] = max(s[paddle] - speed, 0)
|
|
155
|
+
|
|
156
|
+
# Speed up slightly over time
|
|
157
|
+
if abs(s['ball_dx']) < 2.5:
|
|
158
|
+
s['ball_dx'] *= 1.005
|
|
159
|
+
|
|
160
|
+
_render()
|
|
161
|
+
shortcut_xl.schedule_call(_tick, 1.0 / FPS)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _restore_calc():
|
|
165
|
+
"""Restore automatic calculation after the game ends."""
|
|
166
|
+
def _do(app):
|
|
167
|
+
app.Calculation = -4105 # xlCalculationAutomatic
|
|
168
|
+
shortcut_xl.xl_batch(_do)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@xl_func
|
|
172
|
+
def pong_start():
|
|
173
|
+
if _state['running']:
|
|
174
|
+
return "Already running!"
|
|
175
|
+
_state['running'] = True
|
|
176
|
+
_state['score_l'] = 0
|
|
177
|
+
_state['score_r'] = 0
|
|
178
|
+
_state['_formatted'] = False
|
|
179
|
+
_reset_ball()
|
|
180
|
+
shortcut_xl.schedule_call(_tick, 0.1)
|
|
181
|
+
return "PONG!"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@xl_func
|
|
185
|
+
def pong_stop():
|
|
186
|
+
_state['running'] = False
|
|
187
|
+
shortcut_xl.schedule_call(_restore_calc, 0.3)
|
|
188
|
+
return "Stopped"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""ShortcutXL — Python helpers for the Excel XLL add-in.
|
|
2
|
+
|
|
3
|
+
Public API
|
|
4
|
+
----------
|
|
5
|
+
@xl_func — decorator to register a function as an Excel UDF
|
|
6
|
+
xl_app() — return the Excel Application COM object
|
|
7
|
+
run_managed(fn) — run fn(app) on main thread with tracking + state management
|
|
8
|
+
xl_batch(fn) — run fn(app) on the main thread with ScreenUpdating off
|
|
9
|
+
schedule_call(fn) — schedule fn() on a background thread
|
|
10
|
+
xl_log(msg) — append to %TEMP%\\shortcutxl.log
|
|
11
|
+
format_cell_diff — format dirty cells into a human-readable summary
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from ._log import xl_log
|
|
15
|
+
from ._registry import xl_func, _registry
|
|
16
|
+
from ._com import xl_app
|
|
17
|
+
from ._managed import run_managed, xl_batch, schedule_call
|
|
18
|
+
from ._format import format_cell_diff
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Cell issue categorization — port of cell-categorizer.ts.
|
|
2
|
+
|
|
3
|
+
Detects hardcoded numbers, formula errors, large percentages,
|
|
4
|
+
and hardcoded numbers inside formulas.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
# Issue types — also used as dict keys in categorize_cells() result
|
|
10
|
+
GOOD = 'good'
|
|
11
|
+
HARDCODED_NUMBER = 'hardcodes'
|
|
12
|
+
HARDCODED_NUMBER_IN_FORMULA = 'hardcodes in formulas'
|
|
13
|
+
LARGE_PERCENTAGE = 'large %'
|
|
14
|
+
INVALID_FORMULA = 'formula errs'
|
|
15
|
+
ALL_PROBLEMS = 'all problems'
|
|
16
|
+
|
|
17
|
+
# Number format characters that indicate date/time (skip these as hardcoded)
|
|
18
|
+
_DATE_TIME_CHARS = ('d', 'm', 'y', 'h', 's')
|
|
19
|
+
|
|
20
|
+
# Number format characters/strings that indicate currency/accounting (skip these)
|
|
21
|
+
# Must stay in sync with _format.py currency regex.
|
|
22
|
+
_CURRENCY_CHARS = ('$', '£', '€', '¥', '₹', '₩', '₪', '₱', '฿', 'kr')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_numeric(value):
|
|
26
|
+
"""Check if a value is numeric (int, float, Decimal, etc.)."""
|
|
27
|
+
if isinstance(value, bool):
|
|
28
|
+
return False
|
|
29
|
+
if isinstance(value, (int, float)):
|
|
30
|
+
return True
|
|
31
|
+
# COM may return Decimal or other numeric types
|
|
32
|
+
try:
|
|
33
|
+
float(value)
|
|
34
|
+
return True
|
|
35
|
+
except (TypeError, ValueError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _to_float(value):
|
|
40
|
+
"""Coerce a numeric value to float."""
|
|
41
|
+
if isinstance(value, (int, float)):
|
|
42
|
+
return value
|
|
43
|
+
return float(value)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_hardcoded_number(cell):
|
|
47
|
+
"""Check if a cell contains a hardcoded number (no formula, numeric value,
|
|
48
|
+
not a date/time/currency format, not a COM error code)."""
|
|
49
|
+
if cell.get('formula'):
|
|
50
|
+
return False
|
|
51
|
+
value = cell.get('value')
|
|
52
|
+
if not _is_numeric(value):
|
|
53
|
+
return False
|
|
54
|
+
if is_com_error(value):
|
|
55
|
+
return False
|
|
56
|
+
fmt = (cell.get('numberFormat') or '').lower()
|
|
57
|
+
# Skip dates/times
|
|
58
|
+
if any(ch in fmt for ch in _DATE_TIME_CHARS):
|
|
59
|
+
return False
|
|
60
|
+
# Skip currency/accounting
|
|
61
|
+
if any(ch in fmt for ch in _CURRENCY_CHARS):
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Matches standalone numbers in formulas, excluding cell refs / named ranges.
|
|
67
|
+
_HARDCODED_NUM_RE = re.compile(
|
|
68
|
+
r'(?<![A-Z$!])(?<![A-Z]\d)\b\d+\.?\d*\b(?![A-Z])', re.IGNORECASE)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def has_hardcoded_numbers_in_formula(formula):
|
|
72
|
+
"""Check if a formula contains hardcoded numbers."""
|
|
73
|
+
# Remove string literals to avoid false positives
|
|
74
|
+
cleaned = re.sub(r'"[^"]*"', '', formula)
|
|
75
|
+
return bool(_HARDCODED_NUM_RE.search(cleaned))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_ERROR_PATTERNS = (
|
|
79
|
+
'#ERROR', '#REF', '#VALUE', '#DIV/0', '#NUM', '#N/A', '#NAME', '#NULL')
|
|
80
|
+
|
|
81
|
+
# COM returns Excel error values as negative integers (CVErr codes).
|
|
82
|
+
# Map them to their string equivalents so categorization works.
|
|
83
|
+
_COM_ERROR_CODES = {
|
|
84
|
+
-2146826281: '#DIV/0!',
|
|
85
|
+
-2146826246: '#N/A',
|
|
86
|
+
-2146826259: '#NAME?',
|
|
87
|
+
-2146826265: '#REF!',
|
|
88
|
+
-2146826273: '#VALUE!',
|
|
89
|
+
-2146826252: '#NUM!',
|
|
90
|
+
-2146826288: '#NULL!',
|
|
91
|
+
-2146826245: '#GETTING_DATA',
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _is_error_value(s):
|
|
96
|
+
"""Check if a string is an Excel error value (e.g. '#REF!', '#N/A').
|
|
97
|
+
|
|
98
|
+
Excel error values always start with '#' and may have a trailing '!'
|
|
99
|
+
or '?'. We check that the string starts with a known error prefix
|
|
100
|
+
and contains no spaces (real errors never do)."""
|
|
101
|
+
if not s or ' ' in s:
|
|
102
|
+
return False
|
|
103
|
+
return any(s.startswith(e) for e in _ERROR_PATTERNS)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_com_error(value):
|
|
107
|
+
"""Check if a value is a COM error code (negative integer)."""
|
|
108
|
+
return isinstance(value, int) and value in _COM_ERROR_CODES
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def com_error_to_str(value):
|
|
112
|
+
"""Convert a COM error code to its Excel error string, or None."""
|
|
113
|
+
return _COM_ERROR_CODES.get(value)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def has_formula_error(cell):
|
|
117
|
+
"""Check if a cell has a formula error.
|
|
118
|
+
|
|
119
|
+
Handles both string error values (#DIV/0!, #REF!, etc.) and
|
|
120
|
+
COM integer error codes (-2146826281, etc.) returned by win32com.
|
|
121
|
+
"""
|
|
122
|
+
value = cell.get('value')
|
|
123
|
+
if is_com_error(value):
|
|
124
|
+
return True
|
|
125
|
+
value_str = str(value or '')
|
|
126
|
+
formula_str = str(cell.get('formula') or '')
|
|
127
|
+
return _is_error_value(value_str) or _is_error_value(formula_str)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_large_percentage(cell):
|
|
131
|
+
"""Check if a cell contains a large percentage (>= 100%, hardcoded)."""
|
|
132
|
+
fmt = cell.get('numberFormat') or ''
|
|
133
|
+
if '%' not in fmt:
|
|
134
|
+
return False
|
|
135
|
+
if cell.get('formula'):
|
|
136
|
+
return False
|
|
137
|
+
value = cell.get('value')
|
|
138
|
+
if not _is_numeric(value):
|
|
139
|
+
return False
|
|
140
|
+
# In spreadsheets, 1.0 = 100%
|
|
141
|
+
return abs(_to_float(value)) >= 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def categorize_cell(cell):
|
|
145
|
+
"""Return (cell, issues_list) for a single cell dict."""
|
|
146
|
+
issues = []
|
|
147
|
+
if is_large_percentage(cell):
|
|
148
|
+
issues.append(LARGE_PERCENTAGE)
|
|
149
|
+
if is_hardcoded_number(cell):
|
|
150
|
+
issues.append(HARDCODED_NUMBER)
|
|
151
|
+
is_error = has_formula_error(cell)
|
|
152
|
+
if is_error:
|
|
153
|
+
issues.append(INVALID_FORMULA)
|
|
154
|
+
# Only flag hardcoded numbers in formula when there's no error —
|
|
155
|
+
# error formulas need the error fixed first, flagging "=1/0" for
|
|
156
|
+
# containing hardcoded 1 and 0 is just noise.
|
|
157
|
+
if not is_error and cell.get('formula') and has_hardcoded_numbers_in_formula(cell['formula']):
|
|
158
|
+
issues.append(HARDCODED_NUMBER_IN_FORMULA)
|
|
159
|
+
return issues
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def categorize_cells(dirty_cells):
|
|
163
|
+
"""Categorize a list of dirty cell dicts into groups.
|
|
164
|
+
|
|
165
|
+
Returns a dict with keys:
|
|
166
|
+
good, large_percentages, hardcoded_numbers,
|
|
167
|
+
hardcoded_in_formulas, invalid_formulas, all_problems
|
|
168
|
+
Each value is a list of (cell, issues) tuples (except good which is just cells).
|
|
169
|
+
"""
|
|
170
|
+
good = []
|
|
171
|
+
large_pct = []
|
|
172
|
+
hardcoded = []
|
|
173
|
+
hardcoded_in_formula = []
|
|
174
|
+
invalid = []
|
|
175
|
+
all_problems = []
|
|
176
|
+
|
|
177
|
+
for cell in dirty_cells:
|
|
178
|
+
issues = categorize_cell(cell)
|
|
179
|
+
if not issues:
|
|
180
|
+
good.append(cell)
|
|
181
|
+
else:
|
|
182
|
+
entry = (cell, issues)
|
|
183
|
+
all_problems.append(entry)
|
|
184
|
+
if LARGE_PERCENTAGE in issues:
|
|
185
|
+
large_pct.append(entry)
|
|
186
|
+
if HARDCODED_NUMBER in issues:
|
|
187
|
+
hardcoded.append(entry)
|
|
188
|
+
if HARDCODED_NUMBER_IN_FORMULA in issues:
|
|
189
|
+
hardcoded_in_formula.append(entry)
|
|
190
|
+
if INVALID_FORMULA in issues:
|
|
191
|
+
invalid.append(entry)
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
GOOD: good,
|
|
195
|
+
LARGE_PERCENTAGE: large_pct,
|
|
196
|
+
HARDCODED_NUMBER: hardcoded,
|
|
197
|
+
HARDCODED_NUMBER_IN_FORMULA: hardcoded_in_formula,
|
|
198
|
+
INVALID_FORMULA: invalid,
|
|
199
|
+
ALL_PROBLEMS: all_problems,
|
|
200
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Excel COM helpers — locate the Excel Application object."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
from ctypes import wintypes
|
|
5
|
+
from ._log import xl_log
|
|
6
|
+
|
|
7
|
+
# AccessibleObjectFromWindow native object ID
|
|
8
|
+
_OBJID_NATIVEOM = -16
|
|
9
|
+
|
|
10
|
+
# Excel window class names (hierarchy: XLMAIN → XLDESK → EXCEL7)
|
|
11
|
+
_WC_XLMAIN = "XLMAIN"
|
|
12
|
+
_WC_XLDESK = "XLDESK"
|
|
13
|
+
_WC_EXCEL7 = "EXCEL7"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _GUID(ctypes.Structure):
|
|
17
|
+
_fields_ = [
|
|
18
|
+
('Data1', ctypes.c_ulong),
|
|
19
|
+
('Data2', ctypes.c_ushort),
|
|
20
|
+
('Data3', ctypes.c_ushort),
|
|
21
|
+
('Data4', ctypes.c_byte * 8),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# IID_IDispatch = {00020400-0000-0000-C000-000000000046}
|
|
26
|
+
_IID_IDISPATCH = _GUID(
|
|
27
|
+
0x00020400, 0x0000, 0x0000,
|
|
28
|
+
(ctypes.c_byte * 8)(0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_user32 = ctypes.windll.user32
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _find_own_xlmain():
|
|
35
|
+
"""Find the XLMAIN window belonging to our own process.
|
|
36
|
+
|
|
37
|
+
The XLL runs in-process, so our PID == Excel's PID.
|
|
38
|
+
EnumWindows + GetWindowThreadProcessId ensures we find the right
|
|
39
|
+
instance even when multiple Excels are open.
|
|
40
|
+
|
|
41
|
+
Uses local variables captured by a closure instead of module-level
|
|
42
|
+
mutable globals, making this safe for concurrent calls."""
|
|
43
|
+
target_pid = ctypes.windll.kernel32.GetCurrentProcessId()
|
|
44
|
+
result = ctypes.c_void_p(0)
|
|
45
|
+
|
|
46
|
+
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
47
|
+
def callback(hwnd, _lparam):
|
|
48
|
+
buf = ctypes.create_unicode_buffer(32)
|
|
49
|
+
_user32.GetClassNameW(hwnd, buf, 32)
|
|
50
|
+
if buf.value != _WC_XLMAIN:
|
|
51
|
+
return True
|
|
52
|
+
pid = wintypes.DWORD()
|
|
53
|
+
_user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
54
|
+
if pid.value == target_pid:
|
|
55
|
+
result.value = hwnd
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
_user32.EnumWindows(callback, 0)
|
|
60
|
+
return result.value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _ensure_com_init():
|
|
64
|
+
"""Initialize COM on the current thread (idempotent)."""
|
|
65
|
+
import pythoncom
|
|
66
|
+
pythoncom.CoInitialize()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def xl_app():
|
|
70
|
+
"""Return the Excel Application COM object.
|
|
71
|
+
|
|
72
|
+
Performs a fresh HWND lookup every call. The cost is negligible
|
|
73
|
+
(microseconds) and avoids stale-reference bugs from caching.
|
|
74
|
+
|
|
75
|
+
Uses EnumWindows + PID matching to find this process's Excel,
|
|
76
|
+
not just the first XLMAIN window on the desktop."""
|
|
77
|
+
_ensure_com_init()
|
|
78
|
+
import pythoncom
|
|
79
|
+
import win32com.client
|
|
80
|
+
|
|
81
|
+
# Use windll (not oledll) so we get the HRESULT as a return value
|
|
82
|
+
# instead of an auto-raised OSError — lets us fall through to the
|
|
83
|
+
# GetActiveObject fallback on failure.
|
|
84
|
+
oleacc = ctypes.windll.oleacc
|
|
85
|
+
|
|
86
|
+
xlmain = _find_own_xlmain()
|
|
87
|
+
if xlmain:
|
|
88
|
+
xldesk = _user32.FindWindowExW(xlmain, 0, _WC_XLDESK, None)
|
|
89
|
+
excel7 = _user32.FindWindowExW(xldesk, 0, _WC_EXCEL7, None) if xldesk else 0
|
|
90
|
+
if excel7:
|
|
91
|
+
punk = ctypes.c_void_p()
|
|
92
|
+
hr = oleacc.AccessibleObjectFromWindow(
|
|
93
|
+
excel7, _OBJID_NATIVEOM, ctypes.byref(_IID_IDISPATCH), ctypes.byref(punk))
|
|
94
|
+
if hr == 0 and punk.value:
|
|
95
|
+
iunk = pythoncom.ObjectFromAddress(punk.value)
|
|
96
|
+
disp = iunk.QueryInterface(pythoncom.IID_IDispatch)
|
|
97
|
+
window = win32com.client.Dispatch(disp)
|
|
98
|
+
return window.Application
|
|
99
|
+
|
|
100
|
+
# Fallback — try ROT, but NEVER create a new instance.
|
|
101
|
+
xl_log("xl_app: HWND approach failed, trying GetActiveObject")
|
|
102
|
+
try:
|
|
103
|
+
return win32com.client.GetActiveObject("Excel.Application")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
"Could not find a running Excel instance. "
|
|
107
|
+
"Make sure Excel is open before loading the XLL."
|
|
108
|
+
) from e
|