172 lines
3.7 KiB
Go
172 lines
3.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/steipete/gogcli/internal/outfmt"
|
|
"github.com/steipete/gogcli/internal/ui"
|
|
)
|
|
|
|
type SheetsNotesCmd struct {
|
|
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
|
|
Range string `arg:"" name:"range" help:"Range (A1 notation or named range name; e.g. Sheet1!A1:B10 or MyNamedRange)"`
|
|
}
|
|
|
|
func (c *SheetsNotesCmd) Run(ctx context.Context, flags *RootFlags) error {
|
|
u := ui.FromContext(ctx)
|
|
|
|
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 := requireSheetsService(ctx, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := svc.Spreadsheets.Get(spreadsheetID).
|
|
Ranges(rangeSpec).
|
|
IncludeGridData(true).
|
|
Fields("sheets(properties(title),data(startRow,startColumn,rowData(values(note,formattedValue))))").
|
|
Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
type cellNote struct {
|
|
Sheet string `json:"sheet"`
|
|
A1 string `json:"a1"`
|
|
Row int `json:"row"`
|
|
Col int `json:"col"`
|
|
Value string `json:"value"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
var notes []cellNote
|
|
|
|
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
|
|
}
|
|
if cell.Note == "" {
|
|
continue
|
|
}
|
|
absRow := startRow + ri + 1
|
|
absCol := startCol + ci + 1
|
|
notes = append(notes, cellNote{
|
|
Sheet: sheetTitle,
|
|
A1: formatA1Cell(sheetTitle, absRow, absCol),
|
|
Row: absRow,
|
|
Col: absCol,
|
|
Value: cell.FormattedValue,
|
|
Note: cell.Note,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if outfmt.IsJSON(ctx) {
|
|
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
|
|
"spreadsheetId": spreadsheetID,
|
|
"range": rangeSpec,
|
|
"notes": notes,
|
|
})
|
|
}
|
|
|
|
if len(notes) == 0 {
|
|
u.Err().Println("No notes found")
|
|
return nil
|
|
}
|
|
|
|
w, flush := tableWriter(ctx)
|
|
defer flush()
|
|
fmt.Fprintln(w, "A1\tVALUE\tNOTE")
|
|
for _, n := range notes {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n",
|
|
oneLine(n.A1),
|
|
oneLine(n.Value),
|
|
oneLine(n.Note),
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var simpleSheetNameRe = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
|
|
|
func formatA1Cell(sheetTitle string, row, col int) string {
|
|
colLetters, err := colIndexToLetters(col)
|
|
if err != nil || row <= 0 {
|
|
return ""
|
|
}
|
|
cell := fmt.Sprintf("%s%d", colLetters, row)
|
|
if strings.TrimSpace(sheetTitle) == "" {
|
|
return cell
|
|
}
|
|
return formatSheetPrefix(sheetTitle) + cell
|
|
}
|
|
|
|
func formatSheetPrefix(sheetTitle string) string {
|
|
title := strings.TrimSpace(sheetTitle)
|
|
if title == "" {
|
|
return ""
|
|
}
|
|
if simpleSheetNameRe.MatchString(title) {
|
|
return title + "!"
|
|
}
|
|
escaped := strings.ReplaceAll(title, "'", "''")
|
|
return "'" + escaped + "'!"
|
|
}
|
|
|
|
func colIndexToLetters(col int) (string, error) {
|
|
if col <= 0 {
|
|
return "", fmt.Errorf("invalid column index %d", col)
|
|
}
|
|
var b []byte
|
|
for col > 0 {
|
|
col--
|
|
b = append(b, byte('A'+(col%26)))
|
|
col /= 26
|
|
}
|
|
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
|
|
b[i], b[j] = b[j], b[i]
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func oneLine(s string) string {
|
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
|
s = strings.ReplaceAll(s, "\r", "\n")
|
|
// Keep output parseable in tables/TSV.
|
|
s = strings.ReplaceAll(s, "\t", " ")
|
|
s = strings.ReplaceAll(s, "\n", "\\n")
|
|
return s
|
|
}
|