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/internal/tui/bubbles/git/types"
13 "github.com/charmbracelet/wish"
14 "github.com/charmbracelet/wish/git"
15 "github.com/gliderlabs/ssh"
16 gg "github.com/go-git/go-git/v5"
17 "github.com/muesli/termenv"
18)
19
20var (
21 linenoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
22)
23
24// softServeMiddleware is a middleware that handles displaying files with the
25// option of syntax highlighting and line numbers.
26func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
27 return func(sh ssh.Handler) ssh.Handler {
28 return func(s ssh.Session) {
29 _, _, active := s.Pty()
30 cmds := s.Command()
31 if !active && len(cmds) > 0 {
32 func() {
33 color := false
34 lineno := false
35 fp := filepath.Clean(cmds[0])
36 ps := strings.Split(fp, "/")
37 repo := ps[0]
38 if repo == "config" {
39 return
40 }
41 repoExists := false
42 for _, rp := range ac.Source.AllRepos() {
43 if rp.Name() == repo {
44 repoExists = true
45 }
46 }
47 if !repoExists {
48 return
49 }
50 auth := ac.AuthRepo(repo, s.PublicKey())
51 if auth < git.ReadOnlyAccess {
52 s.Write([]byte("unauthorized"))
53 s.Exit(1)
54 return
55 }
56 for _, op := range cmds[1:] {
57 if op == "-c" || op == "--color" {
58 color = true
59 } else if op == "-l" || op == "--lineno" || op == "--linenumber" {
60 lineno = true
61 }
62 }
63 rs, err := ac.Source.GetRepo(repo)
64 if err != nil {
65 _, _ = s.Write([]byte(err.Error()))
66 _ = s.Exit(1)
67 return
68 }
69 fc, err := readFile(rs.Repository(), strings.Join(ps[1:], "/"))
70 if err != nil {
71 _, _ = s.Write([]byte(err.Error()))
72 _ = s.Exit(1)
73 return
74 }
75 if color {
76 ffc, err := withFormatting(fp, fc)
77 if err != nil {
78 s.Write([]byte(err.Error()))
79 s.Exit(1)
80 return
81 }
82 fc = ffc
83 }
84 if lineno {
85 fc = withLineNumber(fc, color)
86 }
87 s.Write([]byte(fc))
88 }()
89 }
90 sh(s)
91 }
92 }
93}
94
95func readFile(r *gg.Repository, fp string) (string, error) {
96 l, err := r.Log(&gg.LogOptions{})
97 if err != nil {
98 return "", err
99 }
100 c, err := l.Next()
101 if err != nil {
102 return "", err
103 }
104 f, err := c.File(fp)
105 if err != nil {
106 return "", err
107 }
108 fc, err := f.Contents()
109 if err != nil {
110 return "", err
111 }
112 return fc, nil
113}
114
115func withLineNumber(s string, color bool) string {
116 lines := strings.Split(s, "\n")
117 mll := fmt.Sprintf("%d", len(fmt.Sprintf("%d", len(lines))))
118 for i, l := range lines {
119 lines[i] = fmt.Sprintf("%-"+mll+"d", i+1)
120 if color {
121 lines[i] = linenoStyle.Render(lines[i])
122 }
123 lines[i] += " │ " + l
124 }
125 return strings.Join(lines, "\n")
126}
127
128func withFormatting(p, c string) (string, error) {
129 zero := uint(0)
130 lang := ""
131 lexer := lexers.Match(p)
132 if lexer != nil && lexer.Config() != nil {
133 lang = lexer.Config().Name
134 }
135 formatter := &gansi.CodeBlockElement{
136 Code: c,
137 Language: lang,
138 }
139 r := strings.Builder{}
140 styles := types.DefaultStyles()
141 styles.CodeBlock.Margin = &zero
142 rctx := gansi.NewRenderContext(gansi.Options{
143 Styles: styles,
144 ColorProfile: termenv.TrueColor,
145 })
146 err := formatter.Render(&r, rctx)
147 if err != nil {
148 return "", err
149 }
150 return r.String(), nil
151}