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