cat.go

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