Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
- Auth: add `auth credentials set --expand-env` for strict environment placeholder expansion in OAuth client JSON. (#599)
- Auth: let `auth import` seed an initial access token and expiry, and round-trip cached access tokens through token export/import. (#598)
- CLI: add XDG kind-aware config/data/state/cache paths with `GOG_HOME`, per-kind `GOG_*_DIR` overrides, and `--home` while preserving legacy auth/keyring/service-account reads. (#621, #622) — thanks @alexminza.
- Docs: add explicit `--page-width`, `--page-height`, and page margin flags to `docs write` and `docs page-layout`, keeping `--pageless` width unchanged unless requested. (#629, #630) — thanks @sebsnyk.

### Fixed

- People: fall back to token identity when `gog me` / `gog whoami` hit a disabled People API on the OAuth client project. (#460, #461)
- Docs: drop all-whitespace Markdown table header rows during Docs markdown writes, and rewrite same-document `#heading-slug` links to native Google Docs heading links after Drive markdown import. (#632, #633) — thanks @sebsnyk.
- Gmail: include attachment metadata in `gmail messages search --include-body --json` results. (#620)
- Auth: let `auth service-account set` read service account keys from stdin (`--key=-` or `--key-stdin`) or an environment variable (`--key-env`). (#600)
- Auth: serialize file-keyring reads and writes with a shared lock so concurrent `gog` processes cannot observe partial keyring entries or clobber multi-key token updates. (#597)
Expand Down
6 changes: 6 additions & 0 deletions docs/commands/gog-docs-page-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ gog docs (doc) page-layout (set-page-layout,page-setup) <docId> [flags]
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--layout` | `string` | pageless | Page layout: pageless or pages |
| `--margin-bottom` | `string` | | Set bottom page margin (points by default; supports pt, in, cm, mm) |
| `--margin-left` | `string` | | Set left page margin (points by default; supports pt, in, cm, mm) |
| `--margin-right` | `string` | | Set right page margin (points by default; supports pt, in, cm, mm) |
| `--margin-top` | `string` | | Set top page margin (points by default; supports pt, in, cm, mm) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `--page-height` | `string` | | Set page height (points by default; supports pt, in, cm, mm) |
| `--page-width` | `string` | | Set page width (points by default; supports pt, in, cm, mm) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--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. |
Expand Down
6 changes: 6 additions & 0 deletions docs/commands/gog-docs-write.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@ gog docs (doc) write <docId> [flags]
| `--italic` | `bool` | | Set italic |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--line-spacing` | `float64` | | Paragraph line spacing percentage, for example 100 or 150 |
| `--margin-bottom` | `string` | | Set bottom page margin (points by default; supports pt, in, cm, mm) |
| `--margin-left` | `string` | | Set left page margin (points by default; supports pt, in, cm, mm) |
| `--margin-right` | `string` | | Set right page margin (points by default; supports pt, in, cm, mm) |
| `--margin-top` | `string` | | Set top page margin (points by default; supports pt, in, cm, mm) |
| `--markdown` | `bool` | | Convert markdown to Google Docs formatting (requires --replace or --append) |
| `--named-style` | `string` | | Set paragraph named style: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6 |
| `--no-bold` | `bool` | | Clear bold |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `--no-italic` | `bool` | | Clear italic |
| `--no-strikethrough`<br>`--no-strike` | `bool` | | Clear strikethrough |
| `--no-underline` | `bool` | | Clear underline |
| `--page-height` | `string` | | Set page height (points by default; supports pt, in, cm, mm) |
| `--page-width` | `string` | | Set page width (points by default; supports pt, in, cm, mm) |
| `--pageless` | `bool` | | Set document to pageless mode |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--replace` | `bool` | | Replace all content explicitly (required with --markdown unless --append is set) |
Expand Down
28 changes: 28 additions & 0 deletions docs/docs-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@ Command page:

- [`gog docs insert-page-break`](commands/gog-docs-insert-page-break.md)

## Page Layout

Set an existing document to pageless or paged mode:

```bash
gog docs page-layout <docId> --layout pageless
gog docs page-layout <docId> --layout pages
```

Use explicit page size and margin flags when the output width matters:

```bash
gog docs page-layout <docId> --page-width 960
gog docs page-layout <docId> --layout pages --page-width 8.5in --page-height 11in \
--margin-left 0.5in --margin-right 0.5in
gog docs write <docId> --replace --markdown --file report.md --pageless --page-width 960
```

Lengths default to points and also accept `pt`, `in`, `cm`, or `mm`.
`docs page-layout` preserves the current page mode when only size or margin
flags are supplied; pass `--layout` when you also want to toggle pageless/pages.
`--pageless` preserves Google Docs' existing width unless `--page-width` is set
explicitly.

Command page:

- [`gog docs page-layout`](commands/gog-docs-page-layout.md)

## Tables

Insert a native Google Docs table directly via the Docs API, bypassing the
Expand Down
104 changes: 86 additions & 18 deletions internal/cmd/docs_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type DocsWriteCmd struct {
Markdown bool `name:"markdown" help:"Convert markdown to Google Docs formatting (requires --replace or --append)"`
Append bool `name:"append" help:"Append instead of replacing the document body"`
Pageless bool `name:"pageless" help:"Set document to pageless mode"`
Layout DocsLayoutFlags `embed:""`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
Format DocsFormatFlags `embed:""`
Expand All @@ -65,6 +66,10 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
}
c.Tab = tab

if err := c.validateDocumentStyle(); err != nil {
return err
}

if c.Markdown {
if c.Format.any() {
return usage("formatting flags are only supported for plain-text docs write; use markdown syntax or run docs format after writing")
Expand All @@ -75,6 +80,21 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
return c.writePlainText(ctx, flags, id, text)
}

func (c *DocsWriteCmd) validateDocumentStyle() error {
if !c.Pageless && !c.Layout.any() {
return nil
}
mode := ""
if c.Pageless {
mode = docsDocumentModePageless
}
_, err := buildUpdateDocumentStyleRequest(docsDocumentStyleOptions{
Mode: mode,
DocsLayoutFlags: c.Layout,
})
return err
}

func (c *DocsWriteCmd) resolveWriteText(kctx *kong.Context) (string, error) {
text, provided, err := resolveTextInput(c.Text, c.File, kctx, "text", "file")
if err != nil {
Expand All @@ -96,15 +116,19 @@ func (c *DocsWriteCmd) writePlainText(ctx context.Context, flags *RootFlags, doc
}
}

if err := dryRunExit(ctx, flags, "docs.write", map[string]any{
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(text),
"append": c.Append,
"replace": !c.Append,
"markdown": false,
"pageless": c.Pageless,
"tab": c.Tab,
}); err != nil {
}
for k, v := range c.Layout.dryRunPayload() {
dryRunPayload[k] = v
}
if err := dryRunExit(ctx, flags, "docs.write", dryRunPayload); err != nil {
return err
}

Expand Down Expand Up @@ -134,7 +158,7 @@ func (c *DocsWriteCmd) writePlainText(ctx context.Context, flags *RootFlags, doc
}
return err
}
if err := c.applyPageless(ctx, svc, docID); err != nil {
if err := c.applyDocumentStyle(ctx, svc, docID); err != nil {
return err
}

Expand Down Expand Up @@ -169,12 +193,19 @@ func (c *DocsWriteCmd) buildPlainWriteRequests(endIndex, insertIndex int64, text
return reqs, nil
}

func (c *DocsWriteCmd) applyPageless(ctx context.Context, svc *docs.Service, docID string) error {
if !c.Pageless {
func (c *DocsWriteCmd) applyDocumentStyle(ctx context.Context, svc *docs.Service, docID string) error {
if !c.Pageless && !c.Layout.any() {
return nil
}
if err := setDocumentPageless(ctx, svc, docID); err != nil {
return fmt.Errorf("set pageless mode: %w", err)
mode := ""
if c.Pageless {
mode = docsDocumentModePageless
}
if err := setDocumentStyle(ctx, svc, docID, docsDocumentStyleOptions{
Mode: mode,
DocsLayoutFlags: c.Layout,
}); err != nil {
return fmt.Errorf("set document style: %w", err)
}
return nil
}
Expand All @@ -191,6 +222,9 @@ func (c *DocsWriteCmd) writePlainTextResult(ctx context.Context, resp *docs.Batc
if c.Tab != "" {
payload["tabId"] = c.Tab
}
for k, v := range c.Layout.dryRunPayload() {
payload[k] = v
}
if resp.WriteControl != nil {
payload["writeControl"] = resp.WriteControl
}
Expand Down Expand Up @@ -227,15 +261,20 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
}

cleaned, images := extractMarkdownImages(content)
if err := dryRunExit(ctx, flags, "docs.write", map[string]any{
cleaned = normalizeMarkdownTablesForDriveImport(cleaned)
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(content),
"append": false,
"replace": true,
"markdown": true,
"pageless": c.Pageless,
"images": len(images),
}); err != nil {
}
for k, v := range c.Layout.dryRunPayload() {
dryRunPayload[k] = v
}
if err := dryRunExit(ctx, flags, "docs.write", dryRunPayload); err != nil {
return err
}

Expand All @@ -255,21 +294,30 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
}

var docsSvc *docs.Service
if len(images) > 0 || c.Pageless {
needsDocsSvc := len(images) > 0 || c.Pageless || c.Layout.any() || markdownMayContainHeadingLinks(cleaned)
if needsDocsSvc {
var svcErr error
docsSvc, svcErr = newDocsService(ctx, account)
if svcErr != nil {
return svcErr
}
}
rewrittenHeadingLinks := 0
if markdownMayContainHeadingLinks(cleaned) {
count, rewriteErr := rewriteMarkdownHeadingLinks(ctx, docsSvc, docID)
if rewriteErr != nil {
return fmt.Errorf("rewrite heading links: %w", rewriteErr)
}
rewrittenHeadingLinks = count
}
if len(images) > 0 {
if err := insertImagesIntoDocs(ctx, docsSvc, docID, images, ""); err != nil {
cleanupDocsImagePlaceholders(ctx, docsSvc, docID, images, "")
return fmt.Errorf("insert images: %w", err)
}
}
if c.Pageless {
if err := c.applyPageless(ctx, docsSvc, docID); err != nil {
if c.Pageless || c.Layout.any() {
if err := c.applyDocumentStyle(ctx, docsSvc, docID); err != nil {
return err
}
}
Expand All @@ -284,6 +332,9 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
if c.Pageless {
payload["pageless"] = true
}
if rewrittenHeadingLinks > 0 {
payload["headingLinks"] = rewrittenHeadingLinks
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}

Expand All @@ -293,6 +344,9 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
if c.Pageless {
u.Out().Linef("pageless\ttrue")
}
if rewrittenHeadingLinks > 0 {
u.Out().Linef("headingLinks\t%d", rewrittenHeadingLinks)
}
if updated.WebViewLink != "" {
u.Out().Linef("link\t%s", updated.WebViewLink)
}
Expand All @@ -301,7 +355,7 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI

func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, docID, content string) error {
cleaned, images := extractMarkdownImages(content)
if err := dryRunExit(ctx, flags, "docs.write", map[string]any{
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(cleaned),
"append": true,
Expand All @@ -310,7 +364,11 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
"pageless": c.Pageless,
"tab": c.Tab,
"images": len(images),
}); err != nil {
}
for k, v := range c.Layout.dryRunPayload() {
dryRunPayload[k] = v
}
if err := dryRunExit(ctx, flags, "docs.write", dryRunPayload); err != nil {
return err
}

Expand All @@ -333,7 +391,7 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
}
return err
}
if err := c.applyPageless(ctx, svc, docID); err != nil {
if err := c.applyDocumentStyle(ctx, svc, docID); err != nil {
return err
}

Expand All @@ -349,6 +407,9 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
if c.Pageless {
payload["pageless"] = true
}
for k, v := range c.Layout.dryRunPayload() {
payload[k] = v
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}

Expand All @@ -371,7 +432,7 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
// body content via DeleteContentRange. Other tabs are untouched.
func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlags, docID, content string) error {
cleaned, images := extractMarkdownImages(content)
if err := dryRunExit(ctx, flags, "docs.write", map[string]any{
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(cleaned),
"append": false,
Expand All @@ -380,7 +441,11 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
"pageless": c.Pageless,
"tab": c.Tab,
"images": len(images),
}); err != nil {
}
for k, v := range c.Layout.dryRunPayload() {
dryRunPayload[k] = v
}
if err := dryRunExit(ctx, flags, "docs.write", dryRunPayload); err != nil {
return err
}

Expand Down Expand Up @@ -421,7 +486,7 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
}
return err
}
if err := c.applyPageless(ctx, svc, docID); err != nil {
if err := c.applyDocumentStyle(ctx, svc, docID); err != nil {
return err
}

Expand All @@ -437,6 +502,9 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
if c.Pageless {
payload["pageless"] = true
}
for k, v := range c.Layout.dryRunPayload() {
payload[k] = v
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}

Expand Down
20 changes: 0 additions & 20 deletions internal/cmd/docs_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,6 @@ const (
docsDocumentModePageless = "PAGELESS"
)

func setDocumentPageless(ctx context.Context, svc *docs.Service, docID string) error {
return setDocumentMode(ctx, svc, docID, docsDocumentModePageless)
}

// setDocumentMode toggles documentStyle.documentFormat.documentMode via a
// single batchUpdate call.
func setDocumentMode(ctx context.Context, svc *docs.Service, docID, mode string) error {
_, err := svc.Documents.BatchUpdate(docID, &docs.BatchUpdateDocumentRequest{
Requests: []*docs.Request{{
UpdateDocumentStyle: &docs.UpdateDocumentStyleRequest{
DocumentStyle: &docs.DocumentStyle{
DocumentFormat: &docs.DocumentFormat{DocumentMode: mode},
},
Fields: "documentFormat",
},
}},
}).Context(ctx).Do()
return err
}

func resolveTextInput(text, file string, kctx *kong.Context, textFlag, fileFlag string) (string, bool, error) {
file = strings.TrimSpace(file)
textProvided := text != "" || flagProvided(kctx, textFlag)
Expand Down
Loading
Loading