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}