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