middleware.go

  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						return
 70					}
 71					auth := ac.AuthRepo(repo, s.PublicKey())
 72					if auth < gitwish.ReadOnlyAccess {
 73						s.Write([]byte("unauthorized"))
 74						s.Exit(1)
 75						return
 76					}
 77					for _, op := range cmds[1:] {
 78						if op == "-c" || op == "--color" {
 79							color = true
 80						} else if op == "-l" || op == "--lineno" || op == "--linenumber" {
 81							lineno = true
 82						}
 83					}
 84					rs, err := ac.Source.GetRepo(repo)
 85					if err != nil {
 86						_, _ = s.Write([]byte(err.Error()))
 87						_ = s.Exit(1)
 88						return
 89					}
 90					p := strings.Join(ps[1:], "/")
 91					t, err := rs.LatestTree(p)
 92					if err != nil && err != object.ErrDirectoryNotFound {
 93						_, _ = s.Write([]byte(err.Error()))
 94						_ = s.Exit(1)
 95						return
 96					}
 97					if err == object.ErrDirectoryNotFound {
 98						fc, err := rs.LatestFile(p)
 99						if err != nil {
100							_, _ = s.Write([]byte(err.Error()))
101							_ = s.Exit(1)
102							return
103						}
104						if color {
105							ffc, err := withFormatting(fp, fc)
106							if err != nil {
107								s.Write([]byte(err.Error()))
108								s.Exit(1)
109								return
110							}
111							fc = ffc
112						}
113						if lineno {
114							fc = withLineNumber(fc, color)
115						}
116						s.Write([]byte(fc))
117					} else {
118						ents := entries(t.Entries)
119						sort.Sort(ents)
120						for _, e := range ents {
121							m, _ := e.Mode.ToOSFileMode()
122							if m == 0 {
123								s.Write([]byte(strings.Repeat(" ", 10)))
124							} else {
125								s.Write([]byte(filemodeStyle.Render(m.String())))
126							}
127							s.Write([]byte(" "))
128							if e.Mode.IsFile() {
129								s.Write([]byte(filenameStyle.Render(e.Name)))
130							} else {
131								s.Write([]byte(dirnameStyle.Render(e.Name)))
132							}
133							s.Write([]byte("\n"))
134						}
135					}
136				}()
137			}
138			sh(s)
139		}
140	}
141}
142
143func withLineNumber(s string, color bool) string {
144	lines := strings.Split(s, "\n")
145	mll := fmt.Sprintf("%d", len(fmt.Sprintf("%d", len(lines))))
146	for i, l := range lines {
147		lines[i] = fmt.Sprintf("%-"+mll+"d", i+1)
148		if color {
149			lines[i] = linenoStyle.Render(lines[i])
150		}
151		lines[i] += " │ " + l
152	}
153	return strings.Join(lines, "\n")
154}
155
156func withFormatting(p, c string) (string, error) {
157	zero := uint(0)
158	lang := ""
159	lexer := lexers.Match(p)
160	if lexer != nil && lexer.Config() != nil {
161		lang = lexer.Config().Name
162	}
163	formatter := &gansi.CodeBlockElement{
164		Code:     c,
165		Language: lang,
166	}
167	r := strings.Builder{}
168	styles := types.DefaultStyles()
169	styles.CodeBlock.Margin = &zero
170	rctx := gansi.NewRenderContext(gansi.Options{
171		Styles:       styles,
172		ColorProfile: termenv.TrueColor,
173	})
174	err := formatter.Render(&r, rctx)
175	if err != nil {
176		return "", err
177	}
178	return r.String(), nil
179}