Skip to content

Commit 5c20921

Browse files
committed
feat(docs): add tab-targeted editing flags (#330) (thanks @ignacioreyna)
1 parent 2bd8c34 commit 5c20921

4 files changed

Lines changed: 359 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Auth: add `--access-token` / `GOG_ACCESS_TOKEN` for direct access-token auth in headless or CI flows, bypassing stored refresh tokens. (#419) — thanks @mmkal.
1212
- Auth: add `auth add --redirect-uri` for manual/remote OAuth flows, so custom callback hosts can be reused across the printed auth URL, state cache, and code exchange. (#398) — thanks @salmonumbrella.
1313
- Auth: add `--extra-scopes` to `auth add` for appending custom OAuth scope URIs beyond the built-in service scopes. (#421) — thanks @peteradams2026.
14+
- Docs: add `--tab-id` to editing commands so write/update/insert/delete/find-replace can target a specific Google Docs tab. (#330) — thanks @ignacioreyna.
1415
- Chat: add `chat messages reactions create|list|delete` to manage emoji reactions on messages; `react` and `reaction` are aliases for the reactions command group. (#426) — thanks @fernandopps.
1516
- Sheets: add `sheets insert` to insert rows/columns into a sheet. (#203) — thanks @andybergon.
1617
- Sheets: add `sheets links` (alias `hyperlinks`) to list cell links from ranges, including rich-text links. (#374) — thanks @omothm.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,10 +904,13 @@ gog docs list-tabs <docId>
904904
gog docs cat <docId> --tab "Notes"
905905
gog docs cat <docId> --all-tabs
906906
gog docs update <docId> --text "Append this later"
907+
gog docs update <docId> --text "Only in this tab" --tab-id t.notes
907908
gog docs update <docId> --file ./insert.txt --index 25 --pageless
908909
gog docs write <docId> --text "Fresh content"
910+
gog docs write <docId> --text "Rewrite one tab" --tab-id t.notes
909911
gog docs write <docId> --file ./body.txt --append --pageless
910912
gog docs find-replace <docId> "old" "new"
913+
gog docs find-replace <docId> "old" "new" --tab-id t.notes
911914

912915
# Slides
913916
gog slides info <presentationId>

internal/cmd/docs.go

Lines changed: 121 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ type DocsWriteCmd struct {
290290
File string `name:"file" help:"Text file path ('-' for stdin)"`
291291
Append bool `name:"append" help:"Append instead of replacing the document body"`
292292
Pageless bool `name:"pageless" help:"Set document to pageless mode"`
293+
TabID string `name:"tab-id" help:"Target a specific tab by ID (see docs list-tabs)"`
293294
}
294295

295296
func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
@@ -320,21 +321,10 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
320321
return err
321322
}
322323

323-
doc, err := svc.Documents.Get(id).
324-
Fields("documentId,body/content(startIndex,endIndex)").
325-
Context(ctx).
326-
Do()
324+
endIndex, err := docsTargetEndIndex(ctx, svc, id, c.TabID)
327325
if err != nil {
328-
if isDocsNotFound(err) {
329-
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", id)
330-
}
331326
return err
332327
}
333-
if doc == nil {
334-
return errors.New("doc not found")
335-
}
336-
337-
endIndex := docsDocumentEndIndex(doc)
338328
insertIndex := int64(1)
339329
if c.Append {
340330
insertIndex = docsAppendIndex(endIndex)
@@ -349,6 +339,7 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
349339
Range: &docs.Range{
350340
StartIndex: 1,
351341
EndIndex: deleteEnd,
342+
TabId: c.TabID,
352343
},
353344
},
354345
})
@@ -357,7 +348,7 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
357348

358349
reqs = append(reqs, &docs.Request{
359350
InsertText: &docs.InsertTextRequest{
360-
Location: &docs.Location{Index: insertIndex},
351+
Location: &docs.Location{Index: insertIndex, TabId: c.TabID},
361352
Text: text,
362353
},
363354
})
@@ -384,6 +375,9 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
384375
"append": c.Append,
385376
"index": insertIndex,
386377
}
378+
if c.TabID != "" {
379+
payload["tabId"] = c.TabID
380+
}
387381
if resp.WriteControl != nil {
388382
payload["writeControl"] = resp.WriteControl
389383
}
@@ -394,6 +388,9 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
394388
u.Out().Printf("requests\t%d", len(reqs))
395389
u.Out().Printf("append\t%t", c.Append)
396390
u.Out().Printf("index\t%d", insertIndex)
391+
if c.TabID != "" {
392+
u.Out().Printf("tabId\t%s", c.TabID)
393+
}
397394
if resp.WriteControl != nil && resp.WriteControl.RequiredRevisionId != "" {
398395
u.Out().Printf("revision\t%s", resp.WriteControl.RequiredRevisionId)
399396
}
@@ -406,6 +403,7 @@ type DocsUpdateCmd struct {
406403
File string `name:"file" help:"Text file path ('-' for stdin)"`
407404
Index int64 `name:"index" help:"Insert index (default: end of document)"`
408405
Pageless bool `name:"pageless" help:"Set document to pageless mode"`
406+
TabID string `name:"tab-id" help:"Target a specific tab by ID (see docs list-tabs)"`
409407
}
410408

411409
func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootFlags) error {
@@ -442,27 +440,17 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root
442440

443441
insertIndex := c.Index
444442
if insertIndex <= 0 {
445-
var doc *docs.Document
446-
doc, err = svc.Documents.Get(id).
447-
Fields("documentId,body/content(startIndex,endIndex)").
448-
Context(ctx).
449-
Do()
450-
if err != nil {
451-
if isDocsNotFound(err) {
452-
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", id)
453-
}
454-
return err
455-
}
456-
if doc == nil {
457-
return errors.New("doc not found")
443+
endIndex, endErr := docsTargetEndIndex(ctx, svc, id, c.TabID)
444+
if endErr != nil {
445+
return endErr
458446
}
459-
insertIndex = docsAppendIndex(docsDocumentEndIndex(doc))
447+
insertIndex = docsAppendIndex(endIndex)
460448
}
461449

462450
reqs := []*docs.Request{
463451
{
464452
InsertText: &docs.InsertTextRequest{
465-
Location: &docs.Location{Index: insertIndex},
453+
Location: &docs.Location{Index: insertIndex, TabId: c.TabID},
466454
Text: text,
467455
},
468456
},
@@ -489,6 +477,9 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root
489477
"requests": len(reqs),
490478
"index": insertIndex,
491479
}
480+
if c.TabID != "" {
481+
payload["tabId"] = c.TabID
482+
}
492483
if resp.WriteControl != nil {
493484
payload["writeControl"] = resp.WriteControl
494485
}
@@ -498,6 +489,9 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root
498489
u.Out().Printf("id\t%s", resp.DocumentId)
499490
u.Out().Printf("requests\t%d", len(reqs))
500491
u.Out().Printf("index\t%d", insertIndex)
492+
if c.TabID != "" {
493+
u.Out().Printf("tabId\t%s", c.TabID)
494+
}
501495
if resp.WriteControl != nil && resp.WriteControl.RequiredRevisionId != "" {
502496
u.Out().Printf("revision\t%s", resp.WriteControl.RequiredRevisionId)
503497
}
@@ -721,6 +715,7 @@ type DocsInsertCmd struct {
721715
Content string `arg:"" optional:"" name:"content" help:"Text to insert (or use --file / stdin)"`
722716
Index int64 `name:"index" help:"Character index to insert at (1 = beginning)" default:"1"`
723717
File string `name:"file" short:"f" help:"Read content from file (use - for stdin)"`
718+
TabID string `name:"tab-id" help:"Target a specific tab by ID (see docs list-tabs)"`
724719
}
725720

726721
func (c *DocsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
@@ -758,6 +753,7 @@ func (c *DocsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
758753
Text: content,
759754
Location: &docs.Location{
760755
Index: c.Index,
756+
TabId: c.TabID,
761757
},
762758
},
763759
}},
@@ -767,23 +763,31 @@ func (c *DocsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
767763
}
768764

769765
if outfmt.IsJSON(ctx) {
770-
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
766+
payload := map[string]any{
771767
"documentId": result.DocumentId,
772768
"inserted": len(content),
773769
"atIndex": c.Index,
774-
})
770+
}
771+
if c.TabID != "" {
772+
payload["tabId"] = c.TabID
773+
}
774+
return outfmt.WriteJSON(ctx, os.Stdout, payload)
775775
}
776776

777777
u.Out().Printf("documentId\t%s", result.DocumentId)
778778
u.Out().Printf("inserted\t%d bytes", len(content))
779779
u.Out().Printf("atIndex\t%d", c.Index)
780+
if c.TabID != "" {
781+
u.Out().Printf("tabId\t%s", c.TabID)
782+
}
780783
return nil
781784
}
782785

783786
type DocsDeleteCmd struct {
784787
DocID string `arg:"" name:"docId" help:"Doc ID"`
785788
Start int64 `name:"start" required:"" help:"Start index (>= 1)"`
786789
End int64 `name:"end" required:"" help:"End index (> start)"`
790+
TabID string `name:"tab-id" help:"Target a specific tab by ID (see docs list-tabs)"`
787791
}
788792

789793
func (c *DocsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
@@ -816,6 +820,7 @@ func (c *DocsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
816820
Range: &docs.Range{
817821
StartIndex: c.Start,
818822
EndIndex: c.End,
823+
TabId: c.TabID,
819824
},
820825
},
821826
}},
@@ -825,17 +830,24 @@ func (c *DocsDeleteCmd) Run(ctx context.Context, flags *RootFlags) error {
825830
}
826831

827832
if outfmt.IsJSON(ctx) {
828-
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
833+
payload := map[string]any{
829834
"documentId": result.DocumentId,
830835
"deleted": c.End - c.Start,
831836
"startIndex": c.Start,
832837
"endIndex": c.End,
833-
})
838+
}
839+
if c.TabID != "" {
840+
payload["tabId"] = c.TabID
841+
}
842+
return outfmt.WriteJSON(ctx, os.Stdout, payload)
834843
}
835844

836845
u.Out().Printf("documentId\t%s", result.DocumentId)
837846
u.Out().Printf("deleted\t%d characters", c.End-c.Start)
838847
u.Out().Printf("range\t%d-%d", c.Start, c.End)
848+
if c.TabID != "" {
849+
u.Out().Printf("tabId\t%s", c.TabID)
850+
}
839851
return nil
840852
}
841853

@@ -956,6 +968,7 @@ type DocsFindReplaceCmd struct {
956968
Find string `arg:"" name:"find" help:"Text to find"`
957969
ReplaceText string `arg:"" name:"replace" help:"Replacement text"`
958970
MatchCase bool `name:"match-case" help:"Case-sensitive matching"`
971+
TabID string `name:"tab-id" help:"Target a specific tab by ID (see docs list-tabs)"`
959972
}
960973

961974
func (c *DocsFindReplaceCmd) Run(ctx context.Context, flags *RootFlags) error {
@@ -978,16 +991,19 @@ func (c *DocsFindReplaceCmd) Run(ctx context.Context, flags *RootFlags) error {
978991
return err
979992
}
980993

994+
req := &docs.ReplaceAllTextRequest{
995+
ContainsText: &docs.SubstringMatchCriteria{
996+
Text: c.Find,
997+
MatchCase: c.MatchCase,
998+
},
999+
ReplaceText: c.ReplaceText,
1000+
}
1001+
if c.TabID != "" {
1002+
req.TabsCriteria = &docs.TabsCriteria{TabIds: []string{c.TabID}}
1003+
}
1004+
9811005
result, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{
982-
Requests: []*docs.Request{{
983-
ReplaceAllText: &docs.ReplaceAllTextRequest{
984-
ContainsText: &docs.SubstringMatchCriteria{
985-
Text: c.Find,
986-
MatchCase: c.MatchCase,
987-
},
988-
ReplaceText: c.ReplaceText,
989-
},
990-
}},
1006+
Requests: []*docs.Request{{ReplaceAllText: req}},
9911007
}).Context(ctx).Do()
9921008
if err != nil {
9931009
return fmt.Errorf("find-replace: %w", err)
@@ -999,18 +1015,25 @@ func (c *DocsFindReplaceCmd) Run(ctx context.Context, flags *RootFlags) error {
9991015
}
10001016

10011017
if outfmt.IsJSON(ctx) {
1002-
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
1018+
payload := map[string]any{
10031019
"documentId": result.DocumentId,
10041020
"find": c.Find,
10051021
"replace": c.ReplaceText,
10061022
"replacements": replacements,
1007-
})
1023+
}
1024+
if c.TabID != "" {
1025+
payload["tabId"] = c.TabID
1026+
}
1027+
return outfmt.WriteJSON(ctx, os.Stdout, payload)
10081028
}
10091029

10101030
u.Out().Printf("documentId\t%s", result.DocumentId)
10111031
u.Out().Printf("find\t%s", c.Find)
10121032
u.Out().Printf("replace\t%s", c.ReplaceText)
10131033
u.Out().Printf("replacements\t%d", replacements)
1034+
if c.TabID != "" {
1035+
u.Out().Printf("tabId\t%s", c.TabID)
1036+
}
10141037
return nil
10151038
}
10161039

@@ -1280,6 +1303,61 @@ func docsDocumentEndIndex(doc *docs.Document) int64 {
12801303
return end
12811304
}
12821305

1306+
func findTabByID(tabs []*docs.Tab, tabID string) *docs.Tab {
1307+
tabID = strings.TrimSpace(tabID)
1308+
for _, tab := range tabs {
1309+
if tab != nil && tab.TabProperties != nil && tab.TabProperties.TabId == tabID {
1310+
return tab
1311+
}
1312+
}
1313+
return nil
1314+
}
1315+
1316+
func docsTabEndIndex(tab *docs.Tab) int64 {
1317+
if tab == nil || tab.DocumentTab == nil || tab.DocumentTab.Body == nil {
1318+
return 1
1319+
}
1320+
end := int64(1)
1321+
for _, el := range tab.DocumentTab.Body.Content {
1322+
if el == nil {
1323+
continue
1324+
}
1325+
if el.EndIndex > end {
1326+
end = el.EndIndex
1327+
}
1328+
}
1329+
return end
1330+
}
1331+
1332+
func docsTargetEndIndex(ctx context.Context, svc *docs.Service, docID, tabID string) (int64, error) {
1333+
getCall := svc.Documents.Get(docID).Context(ctx)
1334+
if tabID != "" {
1335+
getCall = getCall.IncludeTabsContent(true)
1336+
} else {
1337+
getCall = getCall.Fields("documentId,body/content(startIndex,endIndex)")
1338+
}
1339+
1340+
doc, err := getCall.Do()
1341+
if err != nil {
1342+
if isDocsNotFound(err) {
1343+
return 0, fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
1344+
}
1345+
return 0, err
1346+
}
1347+
if doc == nil {
1348+
return 0, errors.New("doc not found")
1349+
}
1350+
if tabID == "" {
1351+
return docsDocumentEndIndex(doc), nil
1352+
}
1353+
1354+
tab := findTabByID(flattenTabs(doc.Tabs), tabID)
1355+
if tab == nil {
1356+
return 0, fmt.Errorf("tab not found: %s", tabID)
1357+
}
1358+
return docsTabEndIndex(tab), nil
1359+
}
1360+
12831361
func docsAppendIndex(endIndex int64) int64 {
12841362
if endIndex > 1 {
12851363
return endIndex - 1

0 commit comments

Comments
 (0)