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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added
- Sheets: add `sheets insert` to insert rows/columns into a sheet. (#203) — thanks @andybergon.
- Sheets: add `sheets links` (alias `hyperlinks`) to list cell links from ranges, including rich-text links. (#374) — thanks @omothm.
- Gmail: add `watch serve --history-types` filtering (`messageAdded|messageDeleted|labelAdded|labelRemoved`) and include `deletedMessageIds` in webhook payloads. (#168) — thanks @salmonumbrella.
- Contacts: support `--org`, `--title`, `--url`, `--note`, and `--custom` on create/update; include custom fields in get output with deterministic ordering. (#199) — thanks @phuctm97.
- Drive: add `drive ls --all` (alias `--global`) to list across all accessible files; make `--all` and `--parent` mutually exclusive. (#107) — thanks @struong.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,7 @@ gog sheets export <spreadsheetId> --format pdf --out ./sheet.pdf
gog sheets format <spreadsheetId> 'Sheet1!A1:B2' --format-json '{"textFormat":{"bold":true}}' --format-fields 'userEnteredFormat.textFormat.bold'
gog sheets insert <spreadsheetId> "Sheet1" rows 2 --count 3
gog sheets notes <spreadsheetId> 'Sheet1!A1:B10'
gog sheets links <spreadsheetId> 'Sheet1!A1:B10'
```

### Contacts
Expand Down Expand Up @@ -997,6 +998,7 @@ gog sheets insert <spreadsheetId> "Sheet1" cols 3 --after

# Notes
gog sheets notes <spreadsheetId> 'Sheet1!A1:B10'
gog sheets links <spreadsheetId> 'Sheet1!A1:B10' # Includes rich-text links

# Create
gog sheets create "My New Spreadsheet" --sheets "Sheet1,Sheet2"
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/sheets.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type SheetsCmd struct {
Clear SheetsClearCmd `cmd:"" name:"clear" help:"Clear values in a range"`
Format SheetsFormatCmd `cmd:"" name:"format" help:"Apply cell formatting to a range"`
Notes SheetsNotesCmd `cmd:"" name:"notes" help:"Get cell notes from a range"`
Links SheetsLinksCmd `cmd:"" name:"links" aliases:"hyperlinks" help:"Get cell hyperlinks from a range"`
Metadata SheetsMetadataCmd `cmd:"" name:"metadata" aliases:"info" help:"Get spreadsheet metadata"`
Create SheetsCreateCmd `cmd:"" name:"create" aliases:"new" help:"Create a new spreadsheet"`
Copy SheetsCopyCmd `cmd:"" name:"copy" aliases:"cp,duplicate" help:"Copy a Google Sheet"`
Expand Down
163 changes: 163 additions & 0 deletions internal/cmd/sheets_links.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package cmd

import (
"context"
"fmt"
"os"
"strings"

"google.golang.org/api/sheets/v4"

"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)

type SheetsLinksCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
Range string `arg:"" name:"range" help:"Range (eg. Sheet1!A1:B10)"`
}

func (c *SheetsLinksCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
account, err := requireAccount(flags)
if err != nil {
return err
}

spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
rangeSpec := cleanRange(c.Range)
if spreadsheetID == "" {
return usage("empty spreadsheetId")
}
if strings.TrimSpace(rangeSpec) == "" {
return usage("empty range")
}

svc, err := newSheetsService(ctx, account)
if err != nil {
return err
}

resp, err := svc.Spreadsheets.Get(spreadsheetID).
Ranges(rangeSpec).
IncludeGridData(true).
Fields("sheets(properties(title),data(startRow,startColumn,rowData(values(hyperlink,formattedValue,userEnteredFormat(textFormat(link(uri))),textFormatRuns(format(link(uri)))))))").
Do()
if err != nil {
return err
}

type cellLink struct {
Sheet string `json:"sheet"`
A1 string `json:"a1"`
Row int `json:"row"`
Col int `json:"col"`
Value string `json:"value"`
Link string `json:"link"`
}

var links []cellLink

for _, sheet := range resp.Sheets {
if sheet == nil {
continue
}
sheetTitle := ""
if sheet.Properties != nil {
sheetTitle = strings.TrimSpace(sheet.Properties.Title)
}
for _, data := range sheet.Data {
if data == nil {
continue
}
startRow := int(data.StartRow)
startCol := int(data.StartColumn)
for ri, row := range data.RowData {
if row == nil {
continue
}
for ci, cell := range row.Values {
if cell == nil {
continue
}
cellLinks := extractCellLinks(cell)
if len(cellLinks) == 0 {
continue
}
absRow := startRow + ri + 1
absCol := startCol + ci + 1
for _, link := range cellLinks {
links = append(links, cellLink{
Sheet: sheetTitle,
A1: formatA1Cell(sheetTitle, absRow, absCol),
Row: absRow,
Col: absCol,
Value: cell.FormattedValue,
Link: link,
})
}
}
}
}
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"spreadsheetId": spreadsheetID,
"range": rangeSpec,
"links": links,
})
}

if len(links) == 0 {
u.Err().Println("No links found")
return nil
}

w, flush := tableWriter(ctx)
defer flush()
fmt.Fprintln(w, "A1\tVALUE\tLINK")
for _, l := range links {
fmt.Fprintf(w, "%s\t%s\t%s\n",
oneLine(l.A1),
oneLine(l.Value),
oneLine(l.Link),
)
}
return nil
}

func extractCellLinks(cell *sheets.CellData) []string {
if cell == nil {
return nil
}

seen := make(map[string]struct{})
links := make([]string, 0, 1)
add := func(link string) {
trimmed := strings.TrimSpace(link)
if trimmed == "" {
return
}
if _, ok := seen[trimmed]; ok {
return
}
seen[trimmed] = struct{}{}
links = append(links, trimmed)
}

add(cell.Hyperlink)

if cell.UserEnteredFormat != nil && cell.UserEnteredFormat.TextFormat != nil && cell.UserEnteredFormat.TextFormat.Link != nil {
add(cell.UserEnteredFormat.TextFormat.Link.Uri)
}

for _, run := range cell.TextFormatRuns {
if run == nil || run.Format == nil || run.Format.Link == nil {
continue
}
add(run.Format.Link.Uri)
}

return links
}
Loading
Loading