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