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/proto"
 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// CatCommand returns a command that prints the contents of a file.
 25func CatCommand() *cobra.Command {
 26	var linenumber bool
 27	var color bool
 28
 29	catCmd := &cobra.Command{
 30		Use:   "cat PATH",
 31		Short: "Outputs the contents of the file at path.",
 32		Args:  cobra.ExactArgs(1),
 33		RunE: func(cmd *cobra.Command, args []string) error {
 34			cfg, s := fromContext(cmd)
 35			ps := strings.Split(args[0], "/")
 36			rn := strings.TrimSuffix(ps[0], ".git")
 37			fp := strings.Join(ps[1:], "/")
 38			auth := cfg.AuthRepo(rn, s.PublicKey())
 39			if auth < proto.ReadOnlyAccess {
 40				return ErrUnauthorized
 41			}
 42			var repo proto.Repository
 43			repoExists := false
 44			repos, err := cfg.ListRepos()
 45			if err != nil {
 46				return err
 47			}
 48			for _, rp := range repos {
 49				if rp.Name() == rn {
 50					re, err := rp.Open()
 51					if err != nil {
 52						continue
 53					}
 54					repoExists = true
 55					repo = re
 56					break
 57				}
 58			}
 59			if !repoExists {
 60				return ErrRepoNotFound
 61			}
 62			c, _, err := proto.LatestFile(repo, fp)
 63			if err != nil {
 64				return err
 65			}
 66			if color {
 67				c, err = withFormatting(fp, c)
 68				if err != nil {
 69					return err
 70				}
 71			}
 72			if linenumber {
 73				c = withLineNumber(c, color)
 74			}
 75			fmt.Fprint(s, c)
 76			return nil
 77		},
 78	}
 79	catCmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
 80	catCmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
 81
 82	return catCmd
 83}
 84
 85func withLineNumber(s string, color bool) string {
 86	lines := strings.Split(s, "\n")
 87	// NB: len() is not a particularly safe way to count string width (because
 88	// it's counting bytes instead of runes) but in this case it's okay
 89	// because we're only dealing with digits, which are one byte each.
 90	mll := len(fmt.Sprintf("%d", len(lines)))
 91	for i, l := range lines {
 92		digit := fmt.Sprintf("%*d", mll, i+1)
 93		bar := "│"
 94		if color {
 95			digit = lineDigitStyle.Render(digit)
 96			bar = lineBarStyle.Render(bar)
 97		}
 98		if i < len(lines)-1 || len(l) != 0 {
 99			// If the final line was a newline we'll get an empty string for
100			// the final line, so drop the newline altogether.
101			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
102		}
103	}
104	return strings.Join(lines, "\n")
105}
106
107func withFormatting(p, c string) (string, error) {
108	zero := uint(0)
109	lang := ""
110	lexer := lexers.Match(p)
111	if lexer != nil && lexer.Config() != nil {
112		lang = lexer.Config().Name
113	}
114	formatter := &gansi.CodeBlockElement{
115		Code:     c,
116		Language: lang,
117	}
118	r := strings.Builder{}
119	styles := common.StyleConfig()
120	styles.CodeBlock.Margin = &zero
121	rctx := gansi.NewRenderContext(gansi.Options{
122		Styles:       styles,
123		ColorProfile: termenv.TrueColor,
124	})
125	err := formatter.Render(&r, rctx)
126	if err != nil {
127		return "", err
128	}
129	return r.String(), nil
130}