Files
pad/internal/cli/format.go
T
xarmian 7ba69abb88 Misc improvements: CLI field summaries, editor enhancements, CI and UI polish
Show field summary after create/update CLI commands. Make svelte-check
blocking in CI. Improve editor block handling, field editor layout,
conventions page, and minor UI consistency fixes across pages.
2026-03-27 01:13:07 +00:00

277 lines
5.8 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"time"
"github.com/xarmian/pad/internal/models"
)
// StatusIcon returns a status indicator.
func StatusIcon(status string) string {
switch status {
case "active":
return "● Active"
case "draft":
return "○ Draft"
case "completed":
return "✓ Done"
case "archived":
return "⊘ Archived"
case "open":
return "● Open"
case "in_progress":
return "◐ In Progress"
case "done":
return "✓ Done"
default:
return status
}
}
// RelativeTime returns a human-readable relative time string.
func RelativeTime(t time.Time) string {
d := time.Since(t)
switch {
case d < time.Minute:
return "just now"
case d < time.Hour:
m := int(d.Minutes())
if m == 1 {
return "1 min ago"
}
return fmt.Sprintf("%d mins ago", m)
case d < 24*time.Hour:
h := int(d.Hours())
if h == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", h)
case d < 7*24*time.Hour:
days := int(d.Hours() / 24)
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
default:
return t.Format("Jan 2, 2006")
}
}
// PrintItemTable prints items in a formatted table.
func PrintItemTable(items []models.Item) {
if len(items) == 0 {
fmt.Println("No items found.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "TITLE\tCOLLECTION\tUPDATED\tBY\n")
for _, item := range items {
pin := ""
if item.Pinned {
pin = "* "
}
collLabel := item.CollectionName
if item.CollectionIcon != "" {
collLabel = item.CollectionIcon + " " + collLabel
}
fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n",
pin, item.Title,
collLabel,
RelativeTime(item.UpdatedAt),
item.LastModifiedBy,
)
}
w.Flush()
}
// PrintItemTitles prints just item titles.
func PrintItemTitles(items []models.Item) {
for _, item := range items {
fmt.Println(item.Title)
}
}
// FormatFieldSummary returns a formatted summary of item fields.
// Example output: "status: open | priority: high | category: platform"
func FormatFieldSummary(fieldsJSON string) string {
if fieldsJSON == "" || fieldsJSON == "{}" || fieldsJSON == "null" {
return ""
}
var fields map[string]any
if err := json.Unmarshal([]byte(fieldsJSON), &fields); err != nil {
return ""
}
if len(fields) == 0 {
return ""
}
// Sort keys for consistent output
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
v := fields[k]
str := fmt.Sprintf("%v", v)
if str == "" || str == "<nil>" {
continue
}
parts = append(parts, fmt.Sprintf("%s: %s", k, str))
}
if len(parts) == 0 {
return ""
}
return strings.Join(parts, " | ")
}
// PrintJSON prints any value as formatted JSON.
func PrintJSON(v interface{}) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(v)
}
// PrintItemMeta prints item metadata header.
func PrintItemMeta(item *models.Item) {
fmt.Printf("Title: %s\n", item.Title)
fmt.Printf("Slug: %s\n", item.Slug)
if item.CollectionName != "" {
collLabel := item.CollectionName
if item.CollectionIcon != "" {
collLabel = item.CollectionIcon + " " + collLabel
}
fmt.Printf("Collection: %s\n", collLabel)
}
tags := item.Tags
if tags == "[]" || tags == "" || tags == "null" {
tags = "(none)"
}
fmt.Printf("Tags: %s\n", tags)
if item.Pinned {
fmt.Printf("Pinned: yes\n")
}
fmt.Printf("Updated: %s by %s via %s\n", RelativeTime(item.UpdatedAt), item.LastModifiedBy, item.Source)
fmt.Println("---")
}
// PrintCollectionTable prints collections in a formatted table.
func PrintCollectionTable(collections []models.Collection) {
if len(collections) == 0 {
fmt.Println("No collections found.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tSLUG\tITEMS\tDEFAULT\n")
for _, col := range collections {
icon := col.Icon
if icon == "" {
icon = " "
}
def := ""
if col.IsDefault {
def = "yes"
}
fmt.Fprintf(w, "%s %s\t%s\t%d\t%s\n",
icon, col.Name,
col.Slug,
col.ItemCount,
def,
)
}
w.Flush()
}
// PrintLinkTable prints item links in a formatted table.
func PrintLinkTable(links []models.ItemLink) {
if len(links) == 0 {
fmt.Println("No links found.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "TYPE\tSOURCE\tTARGET\tCREATED\n")
for _, link := range links {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
link.LinkType,
link.SourceTitle,
link.TargetTitle,
RelativeTime(link.CreatedAt),
)
}
w.Flush()
}
// PrintActivityTable prints activity entries in a table.
func PrintActivityTable(activities []models.Activity) {
if len(activities) == 0 {
fmt.Println("No recent activity.")
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ACTION\tACTOR\tSOURCE\tWHEN\n")
for _, a := range activities {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
a.Action,
a.Actor,
a.Source,
RelativeTime(a.CreatedAt),
)
}
w.Flush()
}
// PrintCommentTable prints comments in a formatted table.
func PrintCommentTable(comments []models.Comment) {
if len(comments) == 0 {
fmt.Println("No comments.")
return
}
for i, c := range comments {
badge := c.CreatedBy
if c.Author != "" && c.Author != c.CreatedBy {
badge = c.Author + " (" + c.CreatedBy + ")"
}
fmt.Printf("💬 %s • %s via %s\n", badge, RelativeTime(c.CreatedAt), c.Source)
fmt.Println(c.Body)
if i < len(comments)-1 {
fmt.Println()
}
}
}
func stripHTMLTags(s string) string {
var result strings.Builder
inTag := false
for _, r := range s {
if r == '<' {
inTag = true
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
result.WriteRune(r)
}
}
return result.String()
}