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