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