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