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 appCfg "github.com/charmbracelet/soft-serve/internal/config"
12 "github.com/charmbracelet/soft-serve/pkg/git"
13 "github.com/charmbracelet/soft-serve/pkg/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 }
51 }
52 if !repoExists {
53 s.Write([]byte("repository not found"))
54 s.Exit(1)
55 return
56 }
57 auth := ac.AuthRepo(repo, s.PublicKey())
58 if auth < gitwish.ReadOnlyAccess {
59 s.Write([]byte("unauthorized"))
60 s.Exit(1)
61 return
62 }
63 for _, op := range cmds[1:] {
64 if op == "-c" || op == "--color" {
65 color = true
66 } else if op == "-l" || op == "--lineno" || op == "--linenumber" {
67 lineno = true
68 }
69 }
70 rs, err := ac.Source.GetRepo(repo)
71 if err != nil {
72 _, _ = s.Write([]byte(err.Error()))
73 _ = s.Exit(1)
74 return
75 }
76 ref, err := rs.HEAD()
77 if err != nil {
78 _, _ = s.Write([]byte(err.Error()))
79 _ = s.Exit(1)
80 return
81 }
82 p := strings.Join(ps[1:], "/")
83 t, err := rs.Tree(ref, p)
84 if err != nil && err != ggit.ErrRevisionNotExist {
85 _, _ = s.Write([]byte(err.Error()))
86 _ = s.Exit(1)
87 return
88 }
89 if err == ggit.ErrRevisionNotExist {
90 _, _ = s.Write([]byte(git.ErrFileNotFound.Error()))
91 _ = s.Exit(1)
92 return
93 }
94 ents, err := t.Entries()
95 if err != nil {
96 fc, _, err := rs.LatestFile(p)
97 if err != nil {
98 _, _ = s.Write([]byte(err.Error()))
99 _ = s.Exit(1)
100 return
101 }
102 if color {
103 ffc, err := withFormatting(fp, fc)
104 if err != nil {
105 s.Write([]byte(err.Error()))
106 s.Exit(1)
107 return
108 }
109 fc = ffc
110 }
111 if lineno {
112 fc = withLineNumber(fc, color)
113 }
114 s.Write([]byte(fc))
115 } else {
116 ents.Sort()
117 for _, e := range ents {
118 m := e.Mode()
119 if m == 0 {
120 s.Write([]byte(strings.Repeat(" ", 10)))
121 } else {
122 s.Write([]byte(filemodeStyle.Render(m.String())))
123 }
124 s.Write([]byte(" "))
125 if !e.IsTree() {
126 s.Write([]byte(filenameStyle.Render(e.Name())))
127 } else {
128 s.Write([]byte(dirnameStyle.Render(e.Name())))
129 }
130 s.Write([]byte("\n"))
131 }
132 }
133 }()
134 }
135 sh(s)
136 }
137 }
138}
139
140func withLineNumber(s string, color bool) string {
141 lines := strings.Split(s, "\n")
142 // NB: len() is not a particularly safe way to count string width (because
143 // it's counting bytes instead of runes) but in this case it's okay
144 // because we're only dealing with digits, which are one byte each.
145 mll := len(fmt.Sprintf("%d", len(lines)))
146 for i, l := range lines {
147 digit := fmt.Sprintf("%*d", mll, i+1)
148 bar := "│"
149 if color {
150 digit = lineDigitStyle.Render(digit)
151 bar = lineBarStyle.Render(bar)
152 }
153 if i < len(lines)-1 || len(l) != 0 {
154 // If the final line was a newline we'll get an empty string for
155 // the final line, so drop the newline altogether.
156 lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
157 }
158 }
159 return strings.Join(lines, "\n")
160}
161
162func withFormatting(p, c string) (string, error) {
163 zero := uint(0)
164 lang := ""
165 lexer := lexers.Match(p)
166 if lexer != nil && lexer.Config() != nil {
167 lang = lexer.Config().Name
168 }
169 formatter := &gansi.CodeBlockElement{
170 Code: c,
171 Language: lang,
172 }
173 r := strings.Builder{}
174 styles := common.DefaultStyles()
175 styles.CodeBlock.Margin = &zero
176 rctx := gansi.NewRenderContext(gansi.Options{
177 Styles: styles,
178 ColorProfile: termenv.TrueColor,
179 })
180 err := formatter.Render(&r, rctx)
181 if err != nil {
182 return "", err
183 }
184 return r.String(), nil
185}