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