1package cmd
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/alecthomas/chroma/lexers"
  8	gansi "github.com/charmbracelet/glamour/ansi"
  9	"github.com/charmbracelet/lipgloss"
 10	"github.com/charmbracelet/soft-serve/git"
 11	"github.com/charmbracelet/soft-serve/ui/common"
 12	"github.com/muesli/termenv"
 13	"github.com/spf13/cobra"
 14)
 15
 16var (
 17	lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
 18	lineBarStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
 19	dirnameStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
 20	filenameStyle  = lipgloss.NewStyle()
 21	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
 22)
 23
 24// blobCommand returns a command that prints the contents of a file.
 25func blobCommand() *cobra.Command {
 26	var linenumber bool
 27	var color bool
 28	var raw bool
 29
 30	cmd := &cobra.Command{
 31		Use:               "blob REPOSITORY [REFERENCE] [PATH]",
 32		Aliases:           []string{"cat", "show"},
 33		Short:             "Print out the contents of file at path.",
 34		Args:              cobra.RangeArgs(1, 3),
 35		PersistentPreRunE: checkIfReadable,
 36		RunE: func(cmd *cobra.Command, args []string) error {
 37			cfg, _ := fromContext(cmd)
 38			rn := args[0]
 39			ref := ""
 40			fp := ""
 41			switch len(args) {
 42			case 2:
 43				fp = args[1]
 44			case 3:
 45				ref = args[1]
 46				fp = args[2]
 47			}
 48
 49			repo, err := cfg.Backend.Repository(rn)
 50			if err != nil {
 51				return err
 52			}
 53
 54			r, err := repo.Open()
 55			if err != nil {
 56				return err
 57			}
 58
 59			if ref == "" {
 60				head, err := r.HEAD()
 61				if err != nil {
 62					return err
 63				}
 64				ref = head.Hash.String()
 65			}
 66
 67			tree, err := r.LsTree(ref)
 68			if err != nil {
 69				return err
 70			}
 71
 72			te, err := tree.TreeEntry(fp)
 73			if err != nil {
 74				return err
 75			}
 76
 77			if te.Type() != "blob" {
 78				return git.ErrFileNotFound
 79			}
 80
 81			bts, err := te.Contents()
 82			if err != nil {
 83				return err
 84			}
 85
 86			c := string(bts)
 87			isBin, _ := te.File().IsBinary()
 88			if isBin {
 89				if raw {
 90					cmd.Println(c)
 91				} else {
 92					return fmt.Errorf("binary file: use --raw to print")
 93				}
 94			} else {
 95				if color {
 96					c, err = withFormatting(fp, c)
 97					if err != nil {
 98						return err
 99					}
100				}
101
102				if linenumber {
103					c = withLineNumber(c, color)
104				}
105
106				cmd.Println(c)
107			}
108			return nil
109		},
110	}
111
112	cmd.Flags().BoolVarP(&raw, "raw", "r", false, "Print raw contents")
113	cmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
114	cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
115
116	return cmd
117}
118
119func withLineNumber(s string, color bool) string {
120	lines := strings.Split(s, "\n")
121	// NB: len() is not a particularly safe way to count string width (because
122	// it's counting bytes instead of runes) but in this case it's okay
123	// because we're only dealing with digits, which are one byte each.
124	mll := len(fmt.Sprintf("%d", len(lines)))
125	for i, l := range lines {
126		digit := fmt.Sprintf("%*d", mll, i+1)
127		bar := "│"
128		if color {
129			digit = lineDigitStyle.Render(digit)
130			bar = lineBarStyle.Render(bar)
131		}
132		if i < len(lines)-1 || len(l) != 0 {
133			// If the final line was a newline we'll get an empty string for
134			// the final line, so drop the newline altogether.
135			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
136		}
137	}
138	return strings.Join(lines, "\n")
139}
140
141func withFormatting(p, c string) (string, error) {
142	zero := uint(0)
143	lang := ""
144	lexer := lexers.Match(p)
145	if lexer != nil && lexer.Config() != nil {
146		lang = lexer.Config().Name
147	}
148	formatter := &gansi.CodeBlockElement{
149		Code:     c,
150		Language: lang,
151	}
152	r := strings.Builder{}
153	styles := common.StyleConfig()
154	styles.CodeBlock.Margin = &zero
155	rctx := gansi.NewRenderContext(gansi.Options{
156		Styles:       styles,
157		ColorProfile: termenv.TrueColor,
158	})
159	err := formatter.Render(&r, rctx)
160	if err != nil {
161		return "", err
162	}
163	return r.String(), nil
164}