Skip to content

feat(slides): add insert-text and replace-text commands#521

Closed
chrissanchez-iops wants to merge 1 commit intoopenclaw:mainfrom
chrissanchez-iops:feat/slides-insert-replace-text
Closed

feat(slides): add insert-text and replace-text commands#521
chrissanchez-iops wants to merge 1 commit intoopenclaw:mainfrom
chrissanchez-iops:feat/slides-insert-replace-text

Conversation

@chrissanchez-iops
Copy link
Copy Markdown
Contributor

Motivation

Today, gog slides can:

  • update-notes — edit speaker notes on an existing slide
  • replace-slide — swap a whole image in-place
  • create-from-markdown / create-from-template — build new decks

…but there is no way to programmatically write text into an existing slide's text box or table cell. That gap makes it awkward to patch decks in place from scripts and agents (e.g. filling a shape whose objectId you already know, or doing {{token}} replacements in a template that has already been copied).

These two commands close the gap using the Slides API primitives already used internally elsewhere in this file:

  • presentations.batchUpdate with InsertTextRequest (+ optional DeleteTextRequest for a clean replace) → gog slides insert-text
  • presentations.batchUpdate with ReplaceAllTextRequestgog slides replace-text

They are intentionally thin wrappers; no business logic, no new auth scopes, no new dependencies. Style mirrors update-notes and replace-slide exactly (same flag conventions, same error messages, same test patterns).

What's added

gog slides insert-text <presentationId> <objectId> <text>

Inserts text into an existing text-capable page element by its Slides API objectId.

# Insert at the start of an element's existing text (default insertion-index=0)
gog slides insert-text <presentationId> <objectId> "Hello, world"

# Insert at a specific position
gog slides insert-text <presentationId> <objectId> " (appended)" --insertion-index 12

# Replace the element's existing text entirely (DeleteText + InsertText in one batch)
gog slides insert-text <presentationId> <objectId> "Brand-new body" --replace

# Read long content from stdin
cat long-content.md | gog slides insert-text <presentationId> <objectId> -

# Preview the batchUpdate request body without calling the API
gog slides insert-text <presentationId> <objectId> "demo" --replace --dry-run

Flags

Flag Default Purpose
--insertion-index <int> 0 Zero-based index within the element's existing text
--replace false Clears existing text first — emits DeleteText (range=ALL) followed by InsertText in a single batch
--dry-run (global) false Prints the intended BatchUpdatePresentationRequest as JSON to stdout and exits; no API call, no auth needed
--json (global) Emits the full BatchUpdatePresentationResponse (revisionId + replies)
Positional <text> If <text> is -, reads from stdin (useful for long or piped content)

gog slides replace-text <presentationId> <find> <replacement>

Find-and-replace across the whole deck.

gog slides replace-text <presentationId> "{{customer_name}}" "Acme Corp"
gog slides replace-text <presentationId> "TODO" "DONE" --match-case
gog slides replace-text <presentationId> "REV" "FY26" --page slide_1 --page slide_2
gog slides replace-text <presentationId> "old" "new" --dry-run

Flags

Flag Default Purpose
--match-case false Case-sensitive match (maps to SubstringMatchCriteria.matchCase)
--page <objectId> (repeatable) Restrict replacement to specific slides (maps to pageObjectIds)
--dry-run (global) false Prints the intended request body; no API call
--json (global) Full BatchUpdatePresentationResponse

Plain output (both commands)

ok | revisionId=<new-revisionId> | replies=<n>                # insert-text
ok | revisionId=<new-revisionId> | replaced=<totalOccurrences> # replace-text

Testing

Unit tests (make test)

Five insert-text tests and four replace-text tests, all using the same httptest-backed pattern as slides_update_notes_test.go:

  • insert-text: basic insert (captures InsertText + verifies index), --replace emits DeleteText→InsertText in one batch, stdin via -, --dry-run produces valid JSON and makes zero API calls, empty objectId errors before auth.
  • replace-text: basic replace with revisionId + occurrences, --match-case + --page scoping, --dry-run produces valid JSON, empty find errors before auth.

make ci passes cleanly:

pnpm gate skipped (no package.json)
tools up to date
0 issues.
ok  github.com/steipete/gogcli/internal/cmd  12.578s
(...all packages OK)

Integration tests (live API, throwaway deck)

Ran the full matrix against a gogcli-pr-test-* deck I created via create-from-markdown (then cleaned up with drive delete --force). Excerpts:

=== [1] insert-text without --replace (insertion-index=0) ===
ok | revisionId=S4-owze0AcUN2w | replies=1
body_1     PREFIX: Original body content line one.

=== [2] insert-text with --replace ===
ok | revisionId=8zEPV2WwWI36uQ | replies=2
body_1     Replaced body via --replace

=== [3] insert-text from stdin (dash) ===
ok | revisionId=eHsciIpPahf_8g | replies=2
body_1     STDIN-LINE-1
           STDIN-LINE-2

=== [4] insert-text --dry-run (no mutation) ===
{
  "requests": [
    {"deleteText":  {"objectId": "body_1", "textRange": {"type": "ALL"}}},
    {"insertText":  {"objectId": "body_1", "text": "dry-run only"}}
  ]
}
(slide content unchanged after dry-run: confirmed)

=== [6] replace-text --match-case (lowercase only) ===
body_2 before: "Case TEST and test tokens"
ok | revisionId=VrV0_rPPYsKOkA | replaced=1
body_2 after:  "Case TEST and lowered tokens"   ← only lowercase "test" replaced

=== [7] replace-text --page scoping ===
Both slides contained SCOPED_WORD; replaced with --page slide_2 only.
ok | revisionId=UR65EYB6nzO5rA | replaced=1
body_1 after: "SCOPED_WORD here on slide_1"      ← unchanged
body_2 after: "ONLY_SLIDE_2 here on slide_2"     ← replaced

=== [8] replace-text --dry-run ===
{"requests":[{"replaceAllText":{"containsText":{"matchCase":true,"text":"..."},"pageObjectIds":["slide_2"],"replaceText":"WHATEVER"}}]}
(slide content unchanged: confirmed)

=== [9] replace-text --json --results-only ===
[{"replaceAllText":{"occurrencesChanged":1}}]

=== [10] error: bad presentationId ===
Google API error (404 notFound): Requested entity was not found.

=== [11] error: non-existent objectId ===
Google API error (400 badRequest): Invalid requests[0].insertText: The object (does_not_exist_object) could not be found.

=== [12] error: empty objectId ===
empty objectId

Scope

  • Non-breaking: only additive command registrations in SlidesCmd.
  • No new auth scopes: both commands use the existing presentations scope already set for slides.
  • No new dependencies: only types already in google.golang.org/api/slides/v1.
  • Mirrors existing patterns: flag names, error messages, test structure, and output format all follow slides_update_notes.go / slides_replace_slide.go.
  • Documentation: README updated in both the quick-start commands index and the detailed ### Slides examples section.

Files

  • internal/cmd/slides_insert_text.go (new)
  • internal/cmd/slides_insert_text_test.go (new)
  • internal/cmd/slides_replace_text.go (new)
  • internal/cmd/slides_replace_text_test.go (new)
  • internal/cmd/slides.go (+2 lines: command registration)
  • README.md (usage examples)

Thin wrappers around presentations.batchUpdate for surgical text edits on
an existing deck.

- insert-text inserts text into a page element by objectId, with
  --insertion-index, --replace (emits DeleteText+InsertText), and
  stdin support via '-'.
- replace-text runs ReplaceAllText across the deck, with --match-case
  and repeatable --page for slide-scoped replacements.

Both honor the global --dry-run (prints the batchUpdate request body as
JSON without calling the API) and --json (emits the full
BatchUpdatePresentationResponse). Plain output gives a one-line
confirmation with revisionId + replies/replaced count. Style mirrors
existing update-notes and replace-slide commands.
@steipete
Copy link
Copy Markdown
Collaborator

Thanks @chrissanchez-iops, landed this on main.

Landed commits:

  • d047a03 feat(slides): add insert-text and replace-text commands
  • e6b6046 fix(slides): harden text edit commands

Maintainer follow-up included:

  • reused the shared Slides clear/insert helper for --replace
  • allowed --replace with empty text to clear an element without sending an empty insert request
  • rejected blank --page values so scoped replace cannot accidentally widen to the whole deck
  • regenerated command docs and added the 0.14.0 changelog entry

Verification:

  • go test ./internal/cmd -run 'TestSlides(InsertText|ReplaceText)|TestResolveSlidesNotesInput|TestFindSpeakerNotesObjectID' -count=1
  • make lint
  • make test
  • make ci
  • live Google Slides smoke: created a temp deck, verified dry-run request JSON, inserted text, replaced body text from stdin, scoped replace-text to slide_2, read back both mutations, then trashed the temp deck
  • GitHub main CI green on e6b6046

@steipete
Copy link
Copy Markdown
Collaborator

Landed on main in d047a03 and e6b6046; closing this PR. Thanks again!

@steipete steipete closed this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants