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}