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/git"
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// blobCommand returns a command that prints the contents of a file.
25func blobCommand() *cobra.Command {
26 var linenumber bool
27 var color bool
28 var raw bool
29
30 cmd := &cobra.Command{
31 Use: "blob REPOSITORY [REFERENCE] [PATH]",
32 Aliases: []string{"cat", "show"},
33 Short: "Print out the contents of file at path.",
34 Args: cobra.RangeArgs(1, 3),
35 PersistentPreRunE: checkIfReadable,
36 RunE: func(cmd *cobra.Command, args []string) error {
37 cfg, _ := fromContext(cmd)
38 rn := args[0]
39 ref := ""
40 fp := ""
41 switch len(args) {
42 case 2:
43 fp = args[1]
44 case 3:
45 ref = args[1]
46 fp = args[2]
47 }
48
49 repo, err := cfg.Backend.Repository(rn)
50 if err != nil {
51 return err
52 }
53
54 r, err := repo.Open()
55 if err != nil {
56 return err
57 }
58
59 if ref == "" {
60 head, err := r.HEAD()
61 if err != nil {
62 return err
63 }
64 ref = head.Hash.String()
65 }
66
67 tree, err := r.LsTree(ref)
68 if err != nil {
69 return err
70 }
71
72 te, err := tree.TreeEntry(fp)
73 if err != nil {
74 return err
75 }
76
77 if te.Type() != "blob" {
78 return git.ErrFileNotFound
79 }
80
81 bts, err := te.Contents()
82 if err != nil {
83 return err
84 }
85
86 c := string(bts)
87 isBin, _ := te.File().IsBinary()
88 if isBin {
89 if raw {
90 cmd.Println(c)
91 } else {
92 return fmt.Errorf("binary file: use --raw to print")
93 }
94 } else {
95 if color {
96 c, err = withFormatting(fp, c)
97 if err != nil {
98 return err
99 }
100 }
101
102 if linenumber {
103 c = withLineNumber(c, color)
104 }
105
106 cmd.Println(c)
107 }
108 return nil
109 },
110 }
111
112 cmd.Flags().BoolVarP(&raw, "raw", "r", false, "Print raw contents")
113 cmd.Flags().BoolVarP(&linenumber, "linenumber", "l", false, "Print line numbers")
114 cmd.Flags().BoolVarP(&color, "color", "c", false, "Colorize output")
115
116 return cmd
117}
118
119func withLineNumber(s string, color bool) string {
120 lines := strings.Split(s, "\n")
121 // NB: len() is not a particularly safe way to count string width (because
122 // it's counting bytes instead of runes) but in this case it's okay
123 // because we're only dealing with digits, which are one byte each.
124 mll := len(fmt.Sprintf("%d", len(lines)))
125 for i, l := range lines {
126 digit := fmt.Sprintf("%*d", mll, i+1)
127 bar := "│"
128 if color {
129 digit = lineDigitStyle.Render(digit)
130 bar = lineBarStyle.Render(bar)
131 }
132 if i < len(lines)-1 || len(l) != 0 {
133 // If the final line was a newline we'll get an empty string for
134 // the final line, so drop the newline altogether.
135 lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
136 }
137 }
138 return strings.Join(lines, "\n")
139}
140
141func withFormatting(p, c string) (string, error) {
142 zero := uint(0)
143 lang := ""
144 lexer := lexers.Match(p)
145 if lexer != nil && lexer.Config() != nil {
146 lang = lexer.Config().Name
147 }
148 formatter := &gansi.CodeBlockElement{
149 Code: c,
150 Language: lang,
151 }
152 r := strings.Builder{}
153 styles := common.StyleConfig()
154 styles.CodeBlock.Margin = &zero
155 rctx := gansi.NewRenderContext(gansi.Options{
156 Styles: styles,
157 ColorProfile: termenv.TrueColor,
158 })
159 err := formatter.Render(&r, rctx)
160 if err != nil {
161 return "", err
162 }
163 return r.String(), nil
164}