Skip to content

Commit b873f85

Browse files
feat(slides): add native table creation
1 parent 7712dc6 commit b873f85

12 files changed

Lines changed: 593 additions & 7 deletions

docs/commands.generated.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,8 @@ Generated from `gog schema --json`.
600600
- [`gog slides (slide) read-slide <presentationId> <slideId>`](commands/gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
601601
- [`gog slides (slide) replace-slide <presentationId> <slideId> [<image>] [flags]`](commands/gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
602602
- [`gog slides (slide) replace-text <presentationId> <find> <replacement> [flags]`](commands/gog-slides-replace-text.md) - Find-and-replace text across a presentation
603+
- [`gog slides (slide) table <command>`](commands/gog-slides-table.md) - Create and update native tables
604+
- [`gog slides (slide) table create (add) --rows=INT-64 --cols=INT-64 --width=FLOAT-64 --height=FLOAT-64 <presentationId> <slideId> [flags]`](commands/gog-slides-table-create.md) - Create a native table on a slide
603605
- [`gog slides (slide) thumbnail (thumb) <presentationId> <slideId> [flags]`](commands/gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
604606
- [`gog slides (slide) update-notes <presentationId> <slideId> [flags]`](commands/gog-slides-update-notes.md) - Update speaker notes on an existing slide
605607
- [`gog status (st) [flags]`](commands/gog-status.md) - Show auth/config status (alias for 'auth status')

docs/commands/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.
44

5-
Generated pages: 646.
5+
Generated pages: 648.
66

77
## Top-level Commands
88

@@ -651,6 +651,8 @@ Generated pages: 646.
651651
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
652652
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
653653
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation
654+
- [gog slides table](gog-slides-table.md) - Create and update native tables
655+
- [gog slides table create](gog-slides-table-create.md) - Create a native table on a slide
654656
- [gog slides thumbnail](gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
655657
- [gog slides update-notes](gog-slides-update-notes.md) - Update speaker notes on an existing slide
656658
- [gog status](gog-status.md) - Show auth/config status (alias for 'auth status')

docs/commands/gog-slides-insert-text.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ gog slides (slide) insert-text <presentationId> <objectId> <text> [flags]
2121
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
2222
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
2323
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
24+
| `--col` | `*int64` | | 0-based table column index for cell-targeted text; requires --row |
2425
| `--color` | `string` | auto | Color output: auto\|always\|never |
2526
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
2627
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
@@ -36,6 +37,7 @@ gog slides (slide) insert-text <presentationId> <objectId> <text> [flags]
3637
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
3738
| `--replace` | `bool` | | Clear existing text in the element before inserting (emits DeleteText + InsertText in the same batch) |
3839
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
40+
| `--row` | `*int64` | | 0-based table row index for cell-targeted text; requires --col |
3941
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
4042
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
4143
| `--version` | `kong.VersionFlag` | | Print version and exit |
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# `gog slides table create`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Create a native table on a slide
6+
7+
## Usage
8+
9+
```bash
10+
gog slides (slide) table create (add) --rows=INT-64 --cols=INT-64 --width=FLOAT-64 --height=FLOAT-64 <presentationId> <slideId> [flags]
11+
```
12+
13+
## Parent
14+
15+
- [gog slides table](gog-slides-table.md)
16+
17+
## Flags
18+
19+
| Flag | Type | Default | Help |
20+
| --- | --- | --- | --- |
21+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
22+
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
23+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
24+
| `--color` | `string` | auto | Color output: auto\|always\|never |
25+
| `--cols` | `int64` | | Number of table columns (>=1) |
26+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
27+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
28+
| `--enable-commands` | `string` | | Comma-separated list of enabled command prefixes; dot paths allowed (restricts CLI) |
29+
| `--enable-commands-exact` | `string` | | Comma-separated list of exact enabled commands; dot paths allowed and parent commands do not enable children |
30+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
31+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
32+
| `--height` | `float64` | | Table height, in --unit |
33+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
34+
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |
35+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
36+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
37+
| `--object-id` | `string` | | Optional table object ID to assign |
38+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
39+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
40+
| `--rows` | `int64` | | Number of table rows (>=1) |
41+
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
42+
| `--unit` | `string` | PT | Measurement unit for x/y/width/height (PT or EMU) |
43+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
44+
| `--version` | `kong.VersionFlag` | | Print version and exit |
45+
| `--width` | `float64` | | Table width, in --unit |
46+
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
47+
| `--x` | `float64` | 0 | Left position of the table, in --unit |
48+
| `--y` | `float64` | 0 | Top position of the table, in --unit |
49+
50+
## See Also
51+
52+
- [gog slides table](gog-slides-table.md)
53+
- [Command index](README.md)

docs/commands/gog-slides-table.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# `gog slides table`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Create and update native tables
6+
7+
## Usage
8+
9+
```bash
10+
gog slides (slide) table <command>
11+
```
12+
13+
## Parent
14+
15+
- [gog slides](gog-slides.md)
16+
17+
## Subcommands
18+
19+
- [gog slides table create](gog-slides-table-create.md) - Create a native table on a slide
20+
21+
## Flags
22+
23+
| Flag | Type | Default | Help |
24+
| --- | --- | --- | --- |
25+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
26+
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
27+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
28+
| `--color` | `string` | auto | Color output: auto\|always\|never |
29+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
30+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
31+
| `--enable-commands` | `string` | | Comma-separated list of enabled command prefixes; dot paths allowed (restricts CLI) |
32+
| `--enable-commands-exact` | `string` | | Comma-separated list of exact enabled commands; dot paths allowed and parent commands do not enable children |
33+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
34+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
35+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
36+
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |
37+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
38+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
39+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
40+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
41+
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
42+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
43+
| `--version` | `kong.VersionFlag` | | Print version and exit |
44+
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
45+
46+
## See Also
47+
48+
- [gog slides](gog-slides.md)
49+
- [Command index](README.md)

docs/commands/gog-slides.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ gog slides (slide) <command> [flags]
3131
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
3232
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
3333
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation
34+
- [gog slides table](gog-slides-table.md) - Create and update native tables
3435
- [gog slides thumbnail](gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide
3536
- [gog slides update-notes](gog-slides-update-notes.md) - Update speaker notes on an existing slide
3637

internal/cmd/slides.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type SlidesCmd struct {
3030
ReplaceSlide SlidesReplaceSlideCmd `cmd:"" name:"replace-slide" help:"Replace an existing slide image from a local file or public URL"`
3131
InsertImage SlidesInsertImageCmd `cmd:"" name:"insert-image" help:"Insert a local or public image at a position and size"`
3232
InsertText SlidesInsertTextCmd `cmd:"" name:"insert-text" help:"Insert text into an existing page element (shape or table) by objectId"`
33+
Table SlidesTableCmd `cmd:"" name:"table" help:"Create and update native tables"`
3334
ReplaceText SlidesReplaceTextCmd `cmd:"" name:"replace-text" help:"Find-and-replace text across a presentation"`
3435
Raw SlidesRawCmd `cmd:"" name:"raw" help:"Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)"`
3536
}

internal/cmd/slides_insert_text.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type SlidesInsertTextCmd struct {
2121
Text string `arg:"" name:"text" help:"Text to insert (use '-' to read from stdin)"`
2222
InsertionIndex int64 `name:"insertion-index" help:"Zero-based index where text is inserted within the element's existing text" default:"0"`
2323
Replace bool `name:"replace" help:"Clear existing text in the element before inserting (emits DeleteText + InsertText in the same batch)"`
24+
Row *int64 `name:"row" help:"0-based table row index for cell-targeted text; requires --col"`
25+
Col *int64 `name:"col" help:"0-based table column index for cell-targeted text; requires --row"`
2426
}
2527

2628
// Run executes the insert-text command.
@@ -51,14 +53,28 @@ func (c *SlidesInsertTextCmd) Run(ctx context.Context, flags *RootFlags) error {
5153
if c.InsertionIndex < 0 {
5254
return usage("insertion-index must be >= 0")
5355
}
56+
var cellLocation *slides.TableCellLocation
57+
if c.Row != nil || c.Col != nil {
58+
if c.Row == nil || c.Col == nil {
59+
return usage("--row and --col must be provided together")
60+
}
61+
if *c.Row < 0 {
62+
return usage("--row must be >= 0")
63+
}
64+
if *c.Col < 0 {
65+
return usage("--col must be >= 0")
66+
}
67+
cellLocation = slidesTableCellLocation(*c.Row, *c.Col)
68+
}
5469

5570
// Build the batchUpdate request body.
5671
var requests []*slides.Request
5772
if c.Replace {
58-
requests = buildSlidesClearAndInsertTextRequests(objectID, text)
73+
requests = buildSlidesClearAndInsertTextRequestsAt(objectID, text, cellLocation)
5974
} else {
6075
requests = append(requests, &slides.Request{
6176
InsertText: &slides.InsertTextRequest{
77+
CellLocation: cellLocation,
6278
ObjectId: objectID,
6379
Text: text,
6480
InsertionIndex: c.InsertionIndex,
@@ -74,6 +90,8 @@ func (c *SlidesInsertTextCmd) Run(ctx context.Context, flags *RootFlags) error {
7490
"text_length": len(text),
7591
"insertion_index": c.InsertionIndex,
7692
"replace": c.Replace,
93+
"row": c.Row,
94+
"col": c.Col,
7795
"batch_update": body,
7896
}); err != nil {
7997
return err

internal/cmd/slides_insert_text_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,84 @@ func TestSlidesInsertText_ReplaceEmitsDeleteThenInsert(t *testing.T) {
132132
}
133133
}
134134

135+
func TestSlidesInsertText_CellLocation(t *testing.T) {
136+
var captured []*slides.Request
137+
srv := mockSlidesBatchUpdateServer(t, &captured, map[string]any{
138+
"presentationId": "pres1",
139+
"replies": []any{map[string]any{}},
140+
})
141+
defer srv.Close()
142+
143+
svc := newSlidesServiceFromServer(t, srv)
144+
flags := &RootFlags{Account: "[email protected]"}
145+
ctx := withSlidesTestService(newCmdRuntimeOutputContext(t, io.Discard, io.Discard), svc)
146+
row, col := int64(0), int64(2)
147+
148+
cmd := &SlidesInsertTextCmd{
149+
PresentationID: "pres1",
150+
ObjectID: "table_1",
151+
Text: "cell text",
152+
Row: &row,
153+
Col: &col,
154+
}
155+
if err := cmd.Run(ctx, flags); err != nil {
156+
t.Fatalf("Run: %v", err)
157+
}
158+
159+
if len(captured) != 1 || captured[0].InsertText == nil {
160+
t.Fatalf("expected single InsertText request, got %+v", captured)
161+
}
162+
insert := captured[0].InsertText
163+
if insert.ObjectId != "table_1" || insert.Text != "cell text" {
164+
t.Fatalf("unexpected insert text request: %+v", insert)
165+
}
166+
if insert.CellLocation == nil || insert.CellLocation.RowIndex != 0 || insert.CellLocation.ColumnIndex != 2 {
167+
t.Fatalf("unexpected cell location: %+v", insert.CellLocation)
168+
}
169+
}
170+
171+
func TestSlidesInsertText_CellReplaceEmitsCellDeleteThenInsert(t *testing.T) {
172+
var captured []*slides.Request
173+
srv := mockSlidesBatchUpdateServer(t, &captured, map[string]any{
174+
"presentationId": "pres1",
175+
"replies": []any{map[string]any{}, map[string]any{}},
176+
})
177+
defer srv.Close()
178+
179+
svc := newSlidesServiceFromServer(t, srv)
180+
flags := &RootFlags{Account: "[email protected]"}
181+
ctx := withSlidesTestService(newCmdRuntimeOutputContext(t, io.Discard, io.Discard), svc)
182+
row, col := int64(1), int64(0)
183+
184+
cmd := &SlidesInsertTextCmd{
185+
PresentationID: "pres1",
186+
ObjectID: "table_1",
187+
Text: "replacement",
188+
Replace: true,
189+
Row: &row,
190+
Col: &col,
191+
}
192+
if err := cmd.Run(ctx, flags); err != nil {
193+
t.Fatalf("Run: %v", err)
194+
}
195+
196+
if len(captured) != 2 {
197+
t.Fatalf("expected DeleteText + InsertText, got %+v", captured)
198+
}
199+
if captured[0].DeleteText == nil || captured[0].DeleteText.CellLocation == nil {
200+
t.Fatalf("expected cell-targeted DeleteText, got %+v", captured[0])
201+
}
202+
if captured[0].DeleteText.CellLocation.RowIndex != 1 || captured[0].DeleteText.CellLocation.ColumnIndex != 0 {
203+
t.Fatalf("unexpected delete cell location: %+v", captured[0].DeleteText.CellLocation)
204+
}
205+
if captured[1].InsertText == nil || captured[1].InsertText.CellLocation == nil {
206+
t.Fatalf("expected cell-targeted InsertText, got %+v", captured[1])
207+
}
208+
if captured[1].InsertText.CellLocation.RowIndex != 1 || captured[1].InsertText.CellLocation.ColumnIndex != 0 {
209+
t.Fatalf("unexpected insert cell location: %+v", captured[1].InsertText.CellLocation)
210+
}
211+
}
212+
135213
func TestSlidesInsertText_ReplaceEmptyClearsOnly(t *testing.T) {
136214
var captured []*slides.Request
137215
srv := mockSlidesBatchUpdateServer(t, &captured, map[string]any{
@@ -236,6 +314,42 @@ func TestSlidesInsertText_DryRunNoAPICall(t *testing.T) {
236314
}
237315
}
238316

317+
func TestSlidesInsertText_DryRunCellLocationIncludesZeroIndexes(t *testing.T) {
318+
flags := &RootFlags{Account: "[email protected]", DryRun: true}
319+
var out bytes.Buffer
320+
ctx := withSlidesTestServiceFactory(
321+
newCmdRuntimeJSONOutputContext(t, &out, io.Discard),
322+
func(context.Context, string) (*slides.Service, error) {
323+
t.Fatal("slides service should not be created during dry-run")
324+
return nil, context.Canceled
325+
},
326+
)
327+
row, col := int64(0), int64(0)
328+
cmd := &SlidesInsertTextCmd{
329+
PresentationID: "pres1",
330+
ObjectID: "table_1",
331+
Text: "dry",
332+
Row: &row,
333+
Col: &col,
334+
}
335+
if err := cmd.Run(ctx, flags); err != nil && ExitCode(err) != 0 {
336+
t.Fatalf("Run: %v", err)
337+
}
338+
339+
var got struct {
340+
Request struct {
341+
BatchUpdate slides.BatchUpdatePresentationRequest `json:"batch_update"`
342+
} `json:"request"`
343+
}
344+
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
345+
t.Fatalf("decode dry-run output: %v\n%s", err, out.String())
346+
}
347+
insert := got.Request.BatchUpdate.Requests[0].InsertText
348+
if insert == nil || insert.CellLocation == nil || insert.CellLocation.RowIndex != 0 || insert.CellLocation.ColumnIndex != 0 {
349+
t.Fatalf("dry-run output should include zero cell indexes, got: %+v", got.Request.BatchUpdate.Requests)
350+
}
351+
}
352+
239353
func TestSlidesInsertText_InvalidInsertionIndexIsUsageErrorBeforeDryRun(t *testing.T) {
240354
flags := &RootFlags{Account: "[email protected]", DryRun: true}
241355
ctx := withSlidesTestServiceFactory(
@@ -261,6 +375,36 @@ func TestSlidesInsertText_InvalidInsertionIndexIsUsageErrorBeforeDryRun(t *testi
261375
}
262376
}
263377

378+
func TestSlidesInsertText_InvalidCellFlags(t *testing.T) {
379+
flags := &RootFlags{Account: "[email protected]"}
380+
ctx := withSlidesTestServiceFactory(
381+
newCmdRuntimeOutputContext(t, io.Discard, io.Discard),
382+
func(context.Context, string) (*slides.Service, error) {
383+
t.Fatal("slides service should not be created")
384+
return nil, context.Canceled
385+
},
386+
)
387+
zero, neg := int64(0), int64(-1)
388+
cases := []struct {
389+
name string
390+
cmd SlidesInsertTextCmd
391+
want string
392+
}{
393+
{name: "row without col", cmd: SlidesInsertTextCmd{PresentationID: "pres1", ObjectID: "table_1", Text: "x", Row: &zero}, want: "--row and --col"},
394+
{name: "col without row", cmd: SlidesInsertTextCmd{PresentationID: "pres1", ObjectID: "table_1", Text: "x", Col: &zero}, want: "--row and --col"},
395+
{name: "negative row", cmd: SlidesInsertTextCmd{PresentationID: "pres1", ObjectID: "table_1", Text: "x", Row: &neg, Col: &zero}, want: "--row must be >= 0"},
396+
{name: "negative col", cmd: SlidesInsertTextCmd{PresentationID: "pres1", ObjectID: "table_1", Text: "x", Row: &zero, Col: &neg}, want: "--col must be >= 0"},
397+
}
398+
for _, tt := range cases {
399+
t.Run(tt.name, func(t *testing.T) {
400+
err := tt.cmd.Run(ctx, flags)
401+
if err == nil || !strings.Contains(err.Error(), tt.want) {
402+
t.Fatalf("expected %q error, got %v", tt.want, err)
403+
}
404+
})
405+
}
406+
}
407+
264408
func TestSlidesInsertText_EmptyTextWithoutReplace(t *testing.T) {
265409
flags := &RootFlags{Account: "[email protected]"}
266410
ctx := withSlidesTestServiceFactory(

0 commit comments

Comments
 (0)