1package server
2
3import (
4 "fmt"
5 "path/filepath"
6 "sort"
7 "strings"
8
9 "github.com/alecthomas/chroma/lexers"
10 gansi "github.com/charmbracelet/glamour/ansi"
11 "github.com/charmbracelet/lipgloss"
12 appCfg "github.com/charmbracelet/soft-serve/internal/config"
13 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
14 "github.com/charmbracelet/wish"
15 gitwish "github.com/charmbracelet/wish/git"
16 "github.com/gliderlabs/ssh"
17 "github.com/go-git/go-git/v5/plumbing/filemode"
18 "github.com/go-git/go-git/v5/plumbing/object"
19 "github.com/muesli/termenv"
20)
21
22var (
23 lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
24 lineBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
25 dirnameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
26 filenameStyle = lipgloss.NewStyle()
27 filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
28)
29
30type entries []object.TreeEntry
31
32func (cl entries) Len() int { return len(cl) }
33func (cl entries) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
34func (cl entries) Less(i, j int) bool {
35 if cl[i].Mode == filemode.Dir && cl[j].Mode == filemode.Dir {
36 return cl[i].Name < cl[j].Name
37 } else if cl[i].Mode == filemode.Dir {
38 return true
39 } else if cl[j].Mode == filemode.Dir {
40 return false
41 } else {
42 return cl[i].Name < cl[j].Name
43 }
44}
45
46// softServeMiddleware is a middleware that handles displaying files with the
47// option of syntax highlighting and line numbers.
48func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
49 return func(sh ssh.Handler) ssh.Handler {
50 return func(s ssh.Session) {
51 _, _, active := s.Pty()
52 cmds := s.Command()
53 if !active && len(cmds) > 0 {
54 func() {
55 color := false
56 lineno := false
57 fp := filepath.Clean(cmds[0])
58 ps := strings.Split(fp, "/")
59 repo := ps[0]
60 if repo == "config" {
61 return
62 }
63 repoExists := false
64 for _, rp := range ac.Source.AllRepos() {
65 if rp.Name() == repo {
66 repoExists = true
67 }
68 }
69 if !repoExists {
70 s.Write([]byte("repository not found"))
71 s.Exit(1)
72 return
73 }
74 auth := ac.AuthRepo(repo, s.PublicKey())
75 if auth < gitwish.ReadOnlyAccess {
76 s.Write([]byte("unauthorized"))
77 s.Exit(1)
78 return
79 }
80 for _, op := range cmds[1:] {
81 if op == "-c" || op == "--color" {
82 color = true
83 } else if op == "-l" || op == "--lineno" || op == "--linenumber" {
84 lineno = true
85 }
86 }
87 rs, err := ac.Source.GetRepo(repo)
88 if err != nil {
89 _, _ = s.Write([]byte(err.Error()))
90 _ = s.Exit(1)
91 return
92 }
93 p := strings.Join(ps[1:], "/")
94 t, err := rs.LatestTree(p)
95 if err != nil && err != object.ErrDirectoryNotFound {
96 _, _ = s.Write([]byte(err.Error()))
97 _ = s.Exit(1)
98 return
99 }
100 if err == object.ErrDirectoryNotFound {
101 fc, err := rs.LatestFile(p)
102 if err != nil {
103 _, _ = s.Write([]byte(err.Error()))
104 _ = s.Exit(1)
105 return
106 }
107 if color {
108 ffc, err := withFormatting(fp, fc)
109 if err != nil {
110 s.Write([]byte(err.Error()))
111 s.Exit(1)
112 return
113 }
114 fc = ffc
115 }
116 if lineno {
117 fc = withLineNumber(fc, color)
118 }
119 s.Write([]byte(fc))
120 } else {
121 ents := entries(t.Entries)
122 sort.Sort(ents)
123 for _, e := range ents {
124 m, _ := e.Mode.ToOSFileMode()
125 if m == 0 {
126 s.Write([]byte(strings.Repeat(" ", 10)))
127 } else {
128 s.Write([]byte(filemodeStyle.Render(m.String())))
129 }
130 s.Write([]byte(" "))
131 if e.Mode.IsFile() {
132 s.Write([]byte(filenameStyle.Render(e.Name)))
133 } else {
134 s.Write([]byte(dirnameStyle.Render(e.Name)))
135 }
136 s.Write([]byte("\n"))
137 }
138 }
139 }()
140 }
141 sh(s)
142 }
143 }
144}
145
146func withLineNumber(s string, color bool) string {
147 lines := strings.Split(s, "\n")
148 // NB: len() is not a particularly safe way to count string width (because
149 // it's counting bytes instead of runes) but in this case it's okay
150 // because we're only dealing with digits, which are one byte each.
151 mll := len(fmt.Sprintf("%d", len(lines)))
152 for i, l := range lines {
153 digit := fmt.Sprintf("%*d", mll, i+1)
154 bar := "│"
155 if color {
156 digit = lineDigitStyle.Render(digit)
157 bar = lineBarStyle.Render(bar)
158 }
159 if i < len(lines)-1 || len(l) != 0 {
160 // If the final line was a newline we'll get an empty string for
161 // the final line, so drop the newline altogether.
162 lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
163 }
164 }
165 return strings.Join(lines, "\n")
166}
167
168func withFormatting(p, c string) (string, error) {
169 zero := uint(0)
170 lang := ""
171 lexer := lexers.Match(p)
172 if lexer != nil && lexer.Config() != nil {
173 lang = lexer.Config().Name
174 }
175 formatter := &gansi.CodeBlockElement{
176 Code: c,
177 Language: lang,
178 }
179 r := strings.Builder{}
180 styles := types.DefaultStyles()
181 styles.CodeBlock.Margin = &zero
182 rctx := gansi.NewRenderContext(gansi.Options{
183 Styles: styles,
184 ColorProfile: termenv.TrueColor,
185 })
186 err := formatter.Render(&r, rctx)
187 if err != nil {
188 return "", err
189 }
190 return r.String(), nil
191}