abra/pkg/formatter/formatter.go
decentral1se 97959ef5da
All checks were successful
continuous-integration/drone/push Build is passing
refactor!: vertical render & UI/UX fixes
See coop-cloud/abra#454
2024-12-28 15:00:31 +00:00

245 lines
5.2 KiB
Go

package formatter
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units"
"golang.org/x/term"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3"
)
var BoldStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string {
return str[:12]
}
func SmallSHA(hash string) string {
return hash[:8]
}
// RemoveSha remove image sha from a string that are added in some docker outputs
func RemoveSha(str string) string {
return strings.Split(str, "@")[0]
}
// HumanDuration from docker/cli RunningFor() to be accessible outside of the class
func HumanDuration(timestamp int64) string {
date := time.Unix(timestamp, 0)
now := time.Now().UTC()
return units.HumanDuration(now.Sub(date)) + " ago"
}
// CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) {
var (
renderer = lipgloss.NewRenderer(os.Stdout)
headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center)
cellStyle = renderer.NewStyle().Padding(0, 1)
borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
)
table := table.New().
Border(lipgloss.ThickBorder()).
BorderStyle(borderStyle).
StyleFunc(func(row, col int) lipgloss.Style {
var style lipgloss.Style
switch {
case row == table.HeaderRow:
return headerStyle
default:
style = cellStyle
}
return style
})
return table, nil
}
func PrintTable(t *table.Table) error {
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1")
fmt.Println(t)
return nil
}
tWidth, _ := lipgloss.Size(t.String())
width, _, err := term.GetSize(0)
if err != nil {
return err
}
if tWidth > width {
t.Width(width - 10)
}
fmt.Println(t)
return nil
}
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right)
}
func CreateOverview(header string, rows [][]string) string {
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
var longest int
for _, row := range rows {
if len(row[0]) > longest {
longest = len(row[0])
}
}
var renderedRows []string
for _, row := range rows {
if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2")
}
lenOffset := 4
if len(row[0]) < longest {
lenOffset += longest - len(row[0])
}
offset := ""
for range lenOffset {
offset = offset + " "
}
renderedRows = append(
renderedRows,
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
)
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(header),
lipgloss.JoinVertical(
lipgloss.Left,
renderedRows...,
),
),
),
)
return body.String()
}
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
// implementation and mainly caters for our current use case which is basically
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
// for the real thing (hopefully).
func ToJSON(headers []string, rows [][]string) (string, error) {
var buff bytes.Buffer
buff.Write([]byte("["))
for idx, row := range rows {
payload := make(map[string]string)
for idx, header := range headers {
payload[strings.ToLower(header)] = row[idx]
}
serialized, err := json.Marshal(payload)
if err != nil {
return "", err
}
buff.Write(serialized)
if idx < (len(rows) - 1) {
buff.Write([]byte(","))
}
}
buff.Write([]byte("]"))
return buff.String(), nil
}
// CreateProgressbar generates a progress bar
func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
return progressbar.NewOptions(
length,
progressbar.OptionClearOnFinish(),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowCount(),
progressbar.OptionFullWidth(),
progressbar.OptionSetDescription(title),
)
}
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
originalImage := image
if strings.Contains(image, "docker.io") {
image = strings.Split(image, "/")[1]
}
if strings.Contains(image, "library") {
image = strings.Split(image, "/")[1]
}
if originalImage != image {
log.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image
}
// ByteCountSI presents a human friendly representation of a byte count. See
// https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format.
func ByteCountSI(b uint64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}