forked from toolshed/abra
		
	
		
			
				
	
	
		
			276 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			276 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package formatter
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/i18n"
 | |
| 	"github.com/charmbracelet/lipgloss"
 | |
| 	"github.com/charmbracelet/lipgloss/table"
 | |
| 	"github.com/docker/go-units"
 | |
| 	"golang.org/x/term"
 | |
| 
 | |
| 	"coopcloud.tech/abra/pkg/config"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"github.com/schollz/progressbar/v3"
 | |
| )
 | |
| 
 | |
| var BoldStyle = lipgloss.NewStyle().
 | |
| 	Bold(true)
 | |
| 
 | |
| var BoldUnderlineStyle = 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)) + i18n.G(" 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(i18n.G("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).
 | |
| 		BorderForeground(lipgloss.Color("63"))
 | |
| 
 | |
| 	var headerStyle = lipgloss.NewStyle().
 | |
| 		Underline(true).
 | |
| 		Bold(true).
 | |
| 		PaddingBottom(1)
 | |
| 
 | |
| 	var leftStyle = lipgloss.NewStyle()
 | |
| 	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 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if len(row) > 2 {
 | |
| 			panic(i18n.G("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 + " "
 | |
| 		}
 | |
| 
 | |
| 		rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))
 | |
| 
 | |
| 		if row[1] == "---" {
 | |
| 			rendered = horizontal(
 | |
| 				leftStyle.
 | |
| 					Bold(true).
 | |
| 					Underline(true).
 | |
| 					PaddingTop(1).
 | |
| 					Render(row[0]),
 | |
| 				offset,
 | |
| 				rightStyle.Render(""),
 | |
| 			)
 | |
| 		}
 | |
| 
 | |
| 		renderedRows = append(renderedRows, rendered)
 | |
| 	}
 | |
| 
 | |
| 	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.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.Debug(i18n.G("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])
 | |
| }
 | |
| 
 | |
| // BoldDirtyDefault ensures a dirty modifier is rendered in bold.
 | |
| func BoldDirtyDefault(v string) string {
 | |
| 	if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
 | |
| 		vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
 | |
| 		v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
 | |
| 	}
 | |
| 
 | |
| 	return v
 | |
| }
 | |
| 
 | |
| // AddDirtyMarker adds the dirty marker to a version string.
 | |
| func AddDirtyMarker(v string) string {
 | |
| 	return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT)
 | |
| }
 |