pptlive 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptlive-0.1.0/.github/workflows/docs.yml +60 -0
- pptlive-0.1.0/.github/workflows/release.yml +118 -0
- pptlive-0.1.0/.gitignore +22 -0
- pptlive-0.1.0/.python-version +1 -0
- pptlive-0.1.0/CLAUDE.md +166 -0
- pptlive-0.1.0/IMPLEMENTATION.md +721 -0
- pptlive-0.1.0/PKG-INFO +382 -0
- pptlive-0.1.0/README.md +346 -0
- pptlive-0.1.0/docs/cli.md +558 -0
- pptlive-0.1.0/docs/concepts.md +238 -0
- pptlive-0.1.0/docs/cookbook.md +392 -0
- pptlive-0.1.0/docs/design.md +152 -0
- pptlive-0.1.0/docs/errors.md +179 -0
- pptlive-0.1.0/docs/getting-started.md +162 -0
- pptlive-0.1.0/docs/index.md +71 -0
- pptlive-0.1.0/docs/mcp.md +152 -0
- pptlive-0.1.0/docs/python-api.md +191 -0
- pptlive-0.1.0/docs/stylesheets/extra.css +10 -0
- pptlive-0.1.0/examples/README.md +56 -0
- pptlive-0.1.0/examples/powershell/build_report.ps1 +76 -0
- pptlive-0.1.0/examples/powershell/quickstart.ps1 +62 -0
- pptlive-0.1.0/examples/python/01_quickstart.py +72 -0
- pptlive-0.1.0/examples/python/02_build_a_deck.py +95 -0
- pptlive-0.1.0/examples/python/03_restyle_deck.py +70 -0
- pptlive-0.1.0/mcpb/.mcpbignore +3 -0
- pptlive-0.1.0/mcpb/README.md +48 -0
- pptlive-0.1.0/mcpb/manifest.json +34 -0
- pptlive-0.1.0/mcpb/pyproject.toml +7 -0
- pptlive-0.1.0/mcpb/src/server.py +13 -0
- pptlive-0.1.0/mkdocs.yml +87 -0
- pptlive-0.1.0/pyproject.toml +139 -0
- pptlive-0.1.0/scripts/chart_spike.py +131 -0
- pptlive-0.1.0/scripts/layout_spike.py +163 -0
- pptlive-0.1.0/scripts/master_spike.py +152 -0
- pptlive-0.1.0/scripts/mcp_spike.py +81 -0
- pptlive-0.1.0/scripts/picture_spike.py +177 -0
- pptlive-0.1.0/scripts/render_select_spike.py +195 -0
- pptlive-0.1.0/scripts/shape_spike.py +201 -0
- pptlive-0.1.0/scripts/show_spike.py +119 -0
- pptlive-0.1.0/scripts/smartart_spike.py +236 -0
- pptlive-0.1.0/scripts/spike.py +201 -0
- pptlive-0.1.0/scripts/table_spike.py +144 -0
- pptlive-0.1.0/scripts/text_spike.py +190 -0
- pptlive-0.1.0/scripts/undo_test.py +223 -0
- pptlive-0.1.0/spec.md +495 -0
- pptlive-0.1.0/src/pptlive/__init__.py +89 -0
- pptlive-0.1.0/src/pptlive/_anchors.py +484 -0
- pptlive-0.1.0/src/pptlive/_app.py +93 -0
- pptlive-0.1.0/src/pptlive/_charts.py +243 -0
- pptlive-0.1.0/src/pptlive/_com.py +127 -0
- pptlive-0.1.0/src/pptlive/_edit.py +101 -0
- pptlive-0.1.0/src/pptlive/_guide.py +58 -0
- pptlive-0.1.0/src/pptlive/_presentation.py +414 -0
- pptlive-0.1.0/src/pptlive/_selection.py +249 -0
- pptlive-0.1.0/src/pptlive/_shapes.py +775 -0
- pptlive-0.1.0/src/pptlive/_show.py +232 -0
- pptlive-0.1.0/src/pptlive/_skill/pptlive-cli/SKILL.md +110 -0
- pptlive-0.1.0/src/pptlive/_skill/pptlive-python/SKILL.md +163 -0
- pptlive-0.1.0/src/pptlive/_slides.py +356 -0
- pptlive-0.1.0/src/pptlive/_smartart.py +213 -0
- pptlive-0.1.0/src/pptlive/_tables.py +227 -0
- pptlive-0.1.0/src/pptlive/_theme.py +296 -0
- pptlive-0.1.0/src/pptlive/cli/__init__.py +1 -0
- pptlive-0.1.0/src/pptlive/cli/__main__.py +4 -0
- pptlive-0.1.0/src/pptlive/cli/commands.py +2129 -0
- pptlive-0.1.0/src/pptlive/cli/main.py +96 -0
- pptlive-0.1.0/src/pptlive/constants.py +1072 -0
- pptlive-0.1.0/src/pptlive/exceptions.py +219 -0
- pptlive-0.1.0/src/pptlive/mcp/__init__.py +19 -0
- pptlive-0.1.0/src/pptlive/mcp/__main__.py +19 -0
- pptlive-0.1.0/src/pptlive/mcp/server.py +848 -0
- pptlive-0.1.0/src/pptlive/py.typed +0 -0
- pptlive-0.1.0/src/pptlive/units.py +37 -0
- pptlive-0.1.0/tests/conftest.py +1638 -0
- pptlive-0.1.0/tests/test_anchor_by_id.py +63 -0
- pptlive-0.1.0/tests/test_anchors.py +80 -0
- pptlive-0.1.0/tests/test_app.py +80 -0
- pptlive-0.1.0/tests/test_charts.py +261 -0
- pptlive-0.1.0/tests/test_cli.py +628 -0
- pptlive-0.1.0/tests/test_exceptions.py +124 -0
- pptlive-0.1.0/tests/test_mcp.py +716 -0
- pptlive-0.1.0/tests/test_paragraphs.py +151 -0
- pptlive-0.1.0/tests/test_pictures.py +175 -0
- pptlive-0.1.0/tests/test_render.py +90 -0
- pptlive-0.1.0/tests/test_selection.py +91 -0
- pptlive-0.1.0/tests/test_selection_edit.py +106 -0
- pptlive-0.1.0/tests/test_shapes.py +156 -0
- pptlive-0.1.0/tests/test_show.py +190 -0
- pptlive-0.1.0/tests/test_slide_lifecycle.py +135 -0
- pptlive-0.1.0/tests/test_slides.py +120 -0
- pptlive-0.1.0/tests/test_smartart.py +232 -0
- pptlive-0.1.0/tests/test_smoke.py +84 -0
- pptlive-0.1.0/tests/test_tables.py +179 -0
- pptlive-0.1.0/tests/test_theme.py +230 -0
- pptlive-0.1.0/uv.lock +1988 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- 'mkdocs.yml'
|
|
9
|
+
- 'src/pptlive/**'
|
|
10
|
+
- 'pyproject.toml'
|
|
11
|
+
- '.github/workflows/docs.yml'
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- 'docs/**'
|
|
15
|
+
- 'mkdocs.yml'
|
|
16
|
+
- 'src/pptlive/**'
|
|
17
|
+
- 'pyproject.toml'
|
|
18
|
+
- '.github/workflows/docs.yml'
|
|
19
|
+
workflow_dispatch:
|
|
20
|
+
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
pages: write
|
|
24
|
+
id-token: write
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: pages
|
|
28
|
+
cancel-in-progress: false
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
build:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: '3.13'
|
|
39
|
+
cache: pip
|
|
40
|
+
|
|
41
|
+
- name: Install docs deps
|
|
42
|
+
run: pip install -e ".[docs]"
|
|
43
|
+
|
|
44
|
+
- name: Build site
|
|
45
|
+
run: mkdocs build --strict
|
|
46
|
+
|
|
47
|
+
- uses: actions/upload-pages-artifact@v3
|
|
48
|
+
with:
|
|
49
|
+
path: site
|
|
50
|
+
|
|
51
|
+
deploy:
|
|
52
|
+
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
|
53
|
+
needs: build
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
environment:
|
|
56
|
+
name: github-pages
|
|
57
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
58
|
+
steps:
|
|
59
|
+
- id: deployment
|
|
60
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Push a version tag to publish: git tag v0.1.0 && git push origin v0.1.0
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.13'
|
|
21
|
+
|
|
22
|
+
- name: Verify tag matches the package version
|
|
23
|
+
run: |
|
|
24
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
25
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
26
|
+
echo "pyproject version: $version"
|
|
27
|
+
echo "release tag: $GITHUB_REF_NAME -> $tag"
|
|
28
|
+
if [ "$version" != "$tag" ]; then
|
|
29
|
+
echo "::error::Tag '$GITHUB_REF_NAME' does not match pyproject version '$version'. Bump the version in pyproject.toml or retag."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
# The .mcpb bundle pins the same version in two files; we keep them in
|
|
33
|
+
# sync by hand (no bumpversion wiring yet), so guard against shipping a
|
|
34
|
+
# stale-versioned bundle.
|
|
35
|
+
mcpb_manifest=$(python -c "import json; print(json.load(open('mcpb/manifest.json'))['version'])")
|
|
36
|
+
mcpb_pyproject=$(python -c "import tomllib; print(tomllib.load(open('mcpb/pyproject.toml','rb'))['project']['version'])")
|
|
37
|
+
echo "mcpb manifest version: $mcpb_manifest"
|
|
38
|
+
echo "mcpb pyproject version: $mcpb_pyproject"
|
|
39
|
+
if [ "$mcpb_manifest" != "$tag" ] || [ "$mcpb_pyproject" != "$tag" ]; then
|
|
40
|
+
echo "::error::mcpb/ version ($mcpb_manifest / $mcpb_pyproject) does not match tag '$tag'. Update mcpb/manifest.json and mcpb/pyproject.toml (the '>=' dep pin too) to $tag."
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
- name: Build sdist + wheel
|
|
45
|
+
run: |
|
|
46
|
+
python -m pip install --upgrade build twine
|
|
47
|
+
python -m build
|
|
48
|
+
twine check dist/*
|
|
49
|
+
|
|
50
|
+
- uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: '20'
|
|
58
|
+
|
|
59
|
+
- name: Build the .mcpb bundle
|
|
60
|
+
# Package the built-in MCP server as a .mcpb for one-click install in
|
|
61
|
+
# Claude Desktop. The bundle only declares `pptlive[mcp]` as a dependency
|
|
62
|
+
# (resolved by uv on the user's machine at install time), so packing here
|
|
63
|
+
# before the PyPI publish is fine. Pin the CLI for reproducible releases.
|
|
64
|
+
run: |
|
|
65
|
+
npm install -g @anthropic-ai/mcpb@2.1.2
|
|
66
|
+
mcpb validate mcpb/manifest.json
|
|
67
|
+
mcpb pack mcpb pptlive.mcpb
|
|
68
|
+
|
|
69
|
+
# Kept out of the `dist` artifact on purpose: PyPI rejects non-wheel/sdist
|
|
70
|
+
# files, and the github-release job attaches this separately.
|
|
71
|
+
- uses: actions/upload-artifact@v4
|
|
72
|
+
with:
|
|
73
|
+
name: mcpb
|
|
74
|
+
path: pptlive.mcpb
|
|
75
|
+
|
|
76
|
+
pypi-publish:
|
|
77
|
+
needs: build
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
# Must match the PyPI trusted-publisher "Environment name".
|
|
80
|
+
environment:
|
|
81
|
+
name: pypi
|
|
82
|
+
url: https://pypi.org/p/pptlive
|
|
83
|
+
permissions:
|
|
84
|
+
id-token: write # OIDC token for trusted publishing — no API token needed
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/download-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: dist
|
|
89
|
+
path: dist/
|
|
90
|
+
|
|
91
|
+
- name: Publish to PyPI
|
|
92
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
93
|
+
|
|
94
|
+
github-release:
|
|
95
|
+
needs: pypi-publish
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
permissions:
|
|
98
|
+
contents: write # to create the Release and upload assets
|
|
99
|
+
steps:
|
|
100
|
+
- uses: actions/download-artifact@v4
|
|
101
|
+
with:
|
|
102
|
+
name: dist
|
|
103
|
+
path: dist/
|
|
104
|
+
|
|
105
|
+
- uses: actions/download-artifact@v4
|
|
106
|
+
with:
|
|
107
|
+
name: mcpb
|
|
108
|
+
path: .
|
|
109
|
+
|
|
110
|
+
- name: Create GitHub Release from the tag
|
|
111
|
+
env:
|
|
112
|
+
GH_TOKEN: ${{ github.token }}
|
|
113
|
+
run: >-
|
|
114
|
+
gh release create "$GITHUB_REF_NAME"
|
|
115
|
+
--repo "$GITHUB_REPOSITORY"
|
|
116
|
+
--title "$GITHUB_REF_NAME"
|
|
117
|
+
--generate-notes
|
|
118
|
+
dist/* pptlive.mcpb
|
pptlive-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# MkDocs build output
|
|
13
|
+
site/
|
|
14
|
+
|
|
15
|
+
# Built MCPB bundles (produced by `mcpb pack`; attached to releases, not checked in)
|
|
16
|
+
*.mcpb
|
|
17
|
+
|
|
18
|
+
# Spike harness scratch state (scripts/undo_test.py)
|
|
19
|
+
scripts/.undo_test_state.json
|
|
20
|
+
|
|
21
|
+
# Personal scratch notes (local only)
|
|
22
|
+
notes.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
pptlive-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# pptlive — guide for Claude
|
|
2
|
+
|
|
3
|
+
`pptlive` drives a **running** Microsoft PowerPoint instance from Python over COM
|
|
4
|
+
(pywin32) — `xlwings`, but for PowerPoint, and built for LLM agents. Windows-only.
|
|
5
|
+
It is the sibling of `../wordlive`: the same design, re-applied to PowerPoint's
|
|
6
|
+
2-D object model.
|
|
7
|
+
|
|
8
|
+
- **`spec.md`** is the design doc. It is written deliberately as *the diff
|
|
9
|
+
against wordlive* — read wordlive's `spec.md` first if a section assumes it.
|
|
10
|
+
- **`IMPLEMENTATION.md`** tracks staged build progress (the analog of wordlive's
|
|
11
|
+
`feature-plan.md`). Update it as milestones land.
|
|
12
|
+
|
|
13
|
+
## `../wordlive` is the reference implementation
|
|
14
|
+
|
|
15
|
+
pptlive copies wordlive's structure, error taxonomy, `EditScope` shape, CLI
|
|
16
|
+
contract, `_com` seam, and test approach **almost verbatim**. Before designing
|
|
17
|
+
or building anything, open the equivalent wordlive module
|
|
18
|
+
(`../wordlive/src/wordlive/`), then apply *only* the PowerPoint diff from
|
|
19
|
+
`spec.md`. Matching wordlive's established conventions matters more than
|
|
20
|
+
inventing new ones — when in doubt, do what wordlive does.
|
|
21
|
+
|
|
22
|
+
## Toolchain — use `uv` for everything
|
|
23
|
+
|
|
24
|
+
Never call `pip` or a bare `python`; always go through `uv`.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
uv sync --extra dev # install deps + dev tools into .venv
|
|
28
|
+
uv run pytest # unit tests (fake COM; no PowerPoint needed)
|
|
29
|
+
uv run pytest -m smoke # smoke suite — needs PowerPoint installed
|
|
30
|
+
uv run ruff check . && uv run ruff format .
|
|
31
|
+
uv run mypy
|
|
32
|
+
uv run pptlive status # exercise the CLI
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Dev pins Python 3.13 (`.python-version`), but the **library targets 3.10+** to
|
|
36
|
+
match wordlive — do not use 3.11+ syntax. `ruff` and `mypy` are configured for
|
|
37
|
+
`py310` in `pyproject.toml`.
|
|
38
|
+
|
|
39
|
+
## Module layout (built through v0.9 — see IMPLEMENTATION.md)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/pptlive/
|
|
43
|
+
__init__.py public surface + __all__
|
|
44
|
+
constants.py typed IntEnums for Mso*/Pp*/Xl* magic constants (+ friendly string aliases)
|
|
45
|
+
exceptions.py PptliveError taxonomy; _decode_com_error / from_com_error / _BUSY_HRESULTS
|
|
46
|
+
units.py points / inches() / cm() helpers — never expose EMUs
|
|
47
|
+
_com.py the ONLY pywin32 seam: com_apartment, get_active_powerpoint,
|
|
48
|
+
launch_powerpoint, translate_com_errors, retry_on_busy
|
|
49
|
+
(tests monkeypatch the getters)
|
|
50
|
+
_app.py PowerPoint handle + attach() / connect()
|
|
51
|
+
_presentation.py Presentation (the wordlive Document analog) + PresentationCollection
|
|
52
|
+
_slides.py SlideCollection / Slide (add/delete/duplicate/move_to/set_layout, notes, read())
|
|
53
|
+
_shapes.py ShapeCollection / Shape (a Shape IS an Anchor when it has a text frame; geometry verbs)
|
|
54
|
+
_anchors.py Anchor base + Paragraph, Cell, Notes
|
|
55
|
+
_tables.py Table / Cell (a table is a shape; cell:S:N:R:C anchors) [v0.5]
|
|
56
|
+
_charts.py Chart (a chart is a shape; data via embedded Excel) [v0.7]
|
|
57
|
+
_smartart.py SmartArt (a diagram is a shape; node tree read/set_nodes) [v0.8]
|
|
58
|
+
_theme.py Theme + Master (deck-wide palette/fonts/text-styles/background) [v0.9]
|
|
59
|
+
_selection.py viewed-slide + Selection snapshot/restore
|
|
60
|
+
_edit.py EditScope — view/Selection preservation + atomic undo via StartNewUndoEntry (see below)
|
|
61
|
+
_show.py SlideShow control (deck.show)
|
|
62
|
+
_guide.py loads the bundled SKILL.md guides (cli/python); shared by CLI + MCP
|
|
63
|
+
_skill/pptlive-cli/SKILL.md, _skill/pptlive-python/SKILL.md the two agent skills
|
|
64
|
+
cli/{__init__,__main__,main,commands}.py + llm-help / install-skill / install-mcp
|
|
65
|
+
mcp/{__init__,__main__,server}.py five op-dispatch tools (ppt_read/edit/render/show/batch)
|
|
66
|
+
+ pptlive://guide resources; pptlive[mcp]
|
|
67
|
+
mcpb/ one-click `.mcpb` bundle (manifest.json, pyproject.toml, src/server.py)
|
|
68
|
+
tests/conftest.py fake_powerpoint fixture (MagicMock COM), no_powerpoint, real_powerpoint
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Not yet built (still in `spec.md`): `_findreplace.py` (`find()` / `find_replace()`).
|
|
72
|
+
Agent skills shipped as **two** guides (`pptlive-cli` + `pptlive-python`), not
|
|
73
|
+
wordlive's single one — `llm-help [--python]` dumps one, `install-skill` writes
|
|
74
|
+
them to `.agents/skills/`, and `install-mcp` / the `mcpb/` bundle wire up MCP.
|
|
75
|
+
|
|
76
|
+
## Conventions (inherited from wordlive — keep them)
|
|
77
|
+
|
|
78
|
+
1. **`.com` escape hatch on every wrapper.** Each wrapper (`PowerPoint`,
|
|
79
|
+
`Presentation`, `Slide`, `Shape`, `Anchor`, …) exposes a `.com` property
|
|
80
|
+
returning the raw COM object. We never block a caller because we haven't
|
|
81
|
+
wrapped something.
|
|
82
|
+
2. **`_com.py` is the only place that touches pywin32.** Everything else sees
|
|
83
|
+
duck-typed dispatch objects. This is the mockable seam: tests monkeypatch
|
|
84
|
+
`_com.get_active_powerpoint` / `_com.launch_powerpoint` to inject a fake.
|
|
85
|
+
Don't `import win32com` anywhere else.
|
|
86
|
+
3. **Wrap COM calls in `with _com.translate_com_errors():`** so
|
|
87
|
+
`pywintypes.com_error` becomes a typed `PptliveError`. Reuse wordlive's
|
|
88
|
+
`_decode_com_error` / `from_com_error` / `_BUSY_HRESULTS` verbatim.
|
|
89
|
+
4. **`from __future__ import annotations` at the top of every module.**
|
|
90
|
+
5. **Points throughout, never EMUs.** PowerPoint's COM layer is points
|
|
91
|
+
(1 in = 72 pt). EMUs are a `python-pptx`/OOXML concern and must never surface.
|
|
92
|
+
Provide `pl.units.inches()` / `pl.units.cm()` helpers.
|
|
93
|
+
6. **Structured I/O.** Reads return dataclasses/dicts. The CLI prints exactly
|
|
94
|
+
**one JSON object on stdout** per invocation (logs to stderr), with
|
|
95
|
+
deterministic exit codes. No string scraping. Global flags (`--json`/`--text`,
|
|
96
|
+
`--doc NAME`) go *before* the subcommand.
|
|
97
|
+
7. **Constants are typed `IntEnum`s, added only as a feature needs them.** Don't
|
|
98
|
+
pre-populate. Friendly string aliases (`"title"`, `"two_content"`,
|
|
99
|
+
`"textbox"`) coerce to the right int the way wordlive's alignment names do.
|
|
100
|
+
|
|
101
|
+
## The three things PowerPoint changes vs. Word — read before coding
|
|
102
|
+
|
|
103
|
+
1. **Atomic undo works after all — just differently. PowerPoint has no
|
|
104
|
+
`UndoRecord`.** Word's `EditScope` brackets a block with
|
|
105
|
+
`Application.UndoRecord` (start/end); PowerPoint has no such bracket. **But the
|
|
106
|
+
2026-05-26 spike (`scripts/undo_test.py`) found PowerPoint already groups
|
|
107
|
+
consecutive in-session COM edits into one undo entry by default**, and
|
|
108
|
+
`Application.StartNewUndoEntry()` is a verified *boundary* primitive. So
|
|
109
|
+
`edit()` calls `StartNewUndoEntry()` on entry to fence the block, and the whole
|
|
110
|
+
block is **one Ctrl-Z** — near-parity with wordlive. Honest caveat: there's no
|
|
111
|
+
explicit "end" fence (the next `edit()` or a user action closes it), so always
|
|
112
|
+
wrap mutations in `deck.edit(...)`. Cross-*process* edits (separate CLI calls)
|
|
113
|
+
are verified to stay distinct undo entries — each re-fences at its own `edit()`
|
|
114
|
+
entry. (`_edit.py` is where the fence lives.)
|
|
115
|
+
2. **PowerPoint must be visible.** `Application.Visible = False` raises in most
|
|
116
|
+
builds, so `connect()` has no `visible=False` mode. Politeness is about *not
|
|
117
|
+
moving the user's view*, not about working hidden.
|
|
118
|
+
3. **Anchors are hierarchical (slide → shape → paragraph), not a global offset.**
|
|
119
|
+
There is no document-wide character stream and no deck-wide `range:`. Anchor
|
|
120
|
+
ids are colon-separated, slide-index first:
|
|
121
|
+
|
|
122
|
+
| anchor_id | resolves to |
|
|
123
|
+
| ---------------- | ----------- |
|
|
124
|
+
| `slide:S` | slide S (1-based) — a **container**, not a text anchor; use `deck.slides[S]`, not `anchor_by_id` |
|
|
125
|
+
| `shape:S:N` | Nth shape (1-based z-order) on slide S — canonical handle; an `Anchor` if it has a text frame |
|
|
126
|
+
| `ph:S:KIND` | placeholder of semantic KIND (`title`/`ctrtitle`/`subtitle`/`body`/`footer`/`date`/`slidenum`) — the LLM-preferred form |
|
|
127
|
+
| `para:S:N:P` | paragraph P in shape N on slide S |
|
|
128
|
+
| `cell:S:N:R:C` | cell (row R, col C) of the table in shape N on slide S |
|
|
129
|
+
| `notes:S` | speaker-notes body of slide S |
|
|
130
|
+
|
|
131
|
+
`shape:` is int-only (z-order) to avoid index-vs-name ambiguity; expose shape
|
|
132
|
+
`.Name` separately (`slide.shapes["Title 1"]`). **z-order drifts** when shapes
|
|
133
|
+
are added/removed: resolve `shape:S:N` **live** (never cache it), and have
|
|
134
|
+
every shape listing emit `name` + `id` (`Shape.Id`, stable across reorder) so
|
|
135
|
+
an agent can re-identify after drift. Steer toward `ph:S:KIND` and `.Name` as
|
|
136
|
+
the drift-proof forms; document the hazard honestly rather than building
|
|
137
|
+
re-resolution machinery. (Resolved Open Q #3 — symbolic `exec` binding deferred.)
|
|
138
|
+
|
|
139
|
+
## Politeness model (the whole point)
|
|
140
|
+
|
|
141
|
+
Default behavior preserves the user's **viewed slide**, **shape/text Selection**,
|
|
142
|
+
and focus. Operations that *must* move what the user sees say so in their name
|
|
143
|
+
(`go_to`, `show.goto`, `allow_view_move()`). Never target the live `Selection`
|
|
144
|
+
unless explicitly asked — write text through `Shape.TextFrame.TextRange.Text`
|
|
145
|
+
directly so no edit needs to select anything.
|
|
146
|
+
|
|
147
|
+
## Error taxonomy → exit codes
|
|
148
|
+
|
|
149
|
+
`PptliveError` base; reuse wordlive's COM-error decoding. Exit codes:
|
|
150
|
+
`0` ok · `1` other · `2` anchor/slide/shape/presentation not found (incl. zero
|
|
151
|
+
`find` matches) · `3` PowerPoint busy / slide show running · `4` PowerPoint not
|
|
152
|
+
running · `5` ambiguous match · `6` shape has no text frame (`NoTextFrameError`,
|
|
153
|
+
the one genuinely new code). `SlideNotFoundError` subclasses
|
|
154
|
+
`AnchorNotFoundError` (so it reuses exit 2).
|
|
155
|
+
|
|
156
|
+
## Testing
|
|
157
|
+
|
|
158
|
+
- Unit tests run against a **`fake_powerpoint`** MagicMock fixture in
|
|
159
|
+
`tests/conftest.py` (model it on wordlive's `fake_word`) — no PowerPoint
|
|
160
|
+
needed, runs on any OS in CI. This is where the politeness/anchor logic is
|
|
161
|
+
proven.
|
|
162
|
+
- Live COM behavior goes behind `@pytest.mark.smoke`, skipped by default
|
|
163
|
+
(`addopts = -m 'not smoke'`); the `real_powerpoint` fixture skips if PowerPoint
|
|
164
|
+
isn't reachable. Run with `uv run pytest -m smoke` on a Windows box.
|
|
165
|
+
- Resolve spike questions (see IMPLEMENTATION.md) against real PowerPoint before
|
|
166
|
+
hardening the corresponding code.
|