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