Something went wrong. Try again.
A terminal client for Tangled. forked from aly.codes/tg
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156package cli
import ( "context" "encoding/json" "fmt" "os" "strings" "text/tabwriter"
"github.com/alyraffauf/tg/tangled")
const defaultListLimit int64 = 100
// shortDate trims an ISO 8601 timestamp to its YYYY-MM-DD prefix.func shortDate(timestamp string) string { if len(timestamp) > 10 { return timestamp[:10] } return timestamp}
// renderTable writes a tab-aligned table of rows to stdout under header.// emptyMessage is shown when rows has no entries. Every renderer in this// package (issues, pulls, repos, SSH keys) goes through this.func renderTable(header []string, rows [][]string, emptyMessage string) { if len(rows) == 0 { fmt.Println(emptyMessage) return }
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent) fmt.Fprintln(tw, strings.Join(header, "\t")) for _, row := range rows { fmt.Fprintln(tw, strings.Join(row, "\t")) } tw.Flush()}
func extractDID(uri string) string { uri = strings.TrimPrefix(uri, "at://") did, _, _ := strings.Cut(uri, "/") return did}
func extractRKey(uri string) string { if idx := strings.LastIndex(uri, "/"); idx != -1 { return uri[idx+1:] } return uri}
// resolveAuthor resolves a DID to an author, falling back to the raw// DID string for Handle if resolution fails.func resolveAuthor(ctx context.Context, did string) author { result := author{DID: did} if ident, err := resolver.ResolveDID(ctx, did); err == nil { result.Handle = ident.Handle.String() } else { result.Handle = did } return result}
// recordView is the fields common to an issue or pull-request record,// as decoded from a tangled.ListItem's raw Value.type recordView struct { Title string Body string CreatedAt string SourceBranch string TargetBranch string}
func decodeIssue(raw json.RawMessage) (recordView, error) { var r tangled.IssueRecord if err := json.Unmarshal(raw, &r); err != nil { return recordView{}, err } return recordView{Title: r.Title, Body: r.Body, CreatedAt: r.CreatedAt}, nil}
func decodePull(raw json.RawMessage) (recordView, error) { var r tangled.PullRecord if err := json.Unmarshal(raw, &r); err != nil { return recordView{}, err } return recordView{ Title: r.Title, Body: r.Body, CreatedAt: r.CreatedAt, SourceBranch: r.Source.Branch, TargetBranch: r.Target.Branch, }, nil}
// buildItems decodes a listing's items into display/JSON-ready items,// silently skipping any whose Value fails to decode. decode is// decodeIssue or decodePull depending on the resource being listed.func buildItems(ctx context.Context, items []tangled.ListItem, decode func(json.RawMessage) (recordView, error)) []item { result := make([]item, 0, len(items))
for _, listItem := range items { decoded, err := decode(listItem.Value) if err != nil { continue }
updated := listItem.StateUpdatedAt if updated == "" { updated = decoded.CreatedAt }
title := decoded.Title if title == "" { title = "(no title)" }
result = append(result, item{ Rkey: extractRKey(listItem.URI), URI: listItem.URI, Title: title, State: listItem.State, Author: resolveAuthor(ctx, extractDID(listItem.URI)), CreatedAt: decoded.CreatedAt, UpdatedAt: updated, CommentCount: listItem.CommentCount, SourceBranch: decoded.SourceBranch, TargetBranch: decoded.TargetBranch, }) }
return result}
// renderList renders issue or pull-request items as a table.func renderList(items []item, emptyMessage string) { rows := make([][]string, 0, len(items)) for _, it := range items { rows = append(rows, []string{it.Rkey, it.Title, it.State, it.Author.Handle, shortDate(it.UpdatedAt)}) } renderTable([]string{"RKEY", "TITLE", "STATE", "AUTHOR", "UPDATED"}, rows, emptyMessage)}
// findByRKey finds the listing item whose URI ends in "/"+rkey. what names// the resource kind (e.g. "issue", "pull request") for the error message.func findByRKey(items []tangled.ListItem, rkey, what string) (*tangled.ListItem, error) { for i := range items { if strings.HasSuffix(items[i].URI, "/"+rkey) { return &items[i], nil } } return nil, fmt.Errorf("%s %q not found", what, rkey)}