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	lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
 24	lineBarStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("236"))
 25	dirnameStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("#00AAFF"))
 26	filenameStyle  = lipgloss.NewStyle()
 27	filemodeStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
 28)
 29
 30type entries []object.TreeEntry
 31
 32func (cl entries) Len() int      { return len(cl) }
 33func (cl entries) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 34func (cl entries) Less(i, j int) bool {
 35	if cl[i].Mode == filemode.Dir && cl[j].Mode == filemode.Dir {
 36		return cl[i].Name < cl[j].Name
 37	} else if cl[i].Mode == filemode.Dir {
 38		return true
 39	} else if cl[j].Mode == filemode.Dir {
 40		return false
 41	} else {
 42		return cl[i].Name < cl[j].Name
 43	}
 44}
 45
 46// softServeMiddleware is a middleware that handles displaying files with the
 47// option of syntax highlighting and line numbers.
 48func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
 49	return func(sh ssh.Handler) ssh.Handler {
 50		return func(s ssh.Session) {
 51			_, _, active := s.Pty()
 52			cmds := s.Command()
 53			if !active && len(cmds) > 0 {
 54				func() {
 55					color := false
 56					lineno := false
 57					fp := filepath.Clean(cmds[0])
 58					ps := strings.Split(fp, "/")
 59					repo := ps[0]
 60					if repo == "config" {
 61						return
 62					}
 63					repoExists := false
 64					for _, rp := range ac.Source.AllRepos() {
 65						if rp.Name() == repo {
 66							repoExists = true
 67						}
 68					}
 69					if !repoExists {
 70						s.Write([]byte("repository not found"))
 71						s.Exit(1)
 72						return
 73					}
 74					auth := ac.AuthRepo(repo, s.PublicKey())
 75					if auth < gitwish.ReadOnlyAccess {
 76						s.Write([]byte("unauthorized"))
 77						s.Exit(1)
 78						return
 79					}
 80					for _, op := range cmds[1:] {
 81						if op == "-c" || op == "--color" {
 82							color = true
 83						} else if op == "-l" || op == "--lineno" || op == "--linenumber" {
 84							lineno = true
 85						}
 86					}
 87					rs, err := ac.Source.GetRepo(repo)
 88					if err != nil {
 89						_, _ = s.Write([]byte(err.Error()))
 90						_ = s.Exit(1)
 91						return
 92					}
 93					p := strings.Join(ps[1:], "/")
 94					t, err := rs.LatestTree(p)
 95					if err != nil && err != object.ErrDirectoryNotFound {
 96						_, _ = s.Write([]byte(err.Error()))
 97						_ = s.Exit(1)
 98						return
 99					}
100					if err == object.ErrDirectoryNotFound {
101						fc, err := rs.LatestFile(p)
102						if err != nil {
103							_, _ = s.Write([]byte(err.Error()))
104							_ = s.Exit(1)
105							return
106						}
107						if color {
108							ffc, err := withFormatting(fp, fc)
109							if err != nil {
110								s.Write([]byte(err.Error()))
111								s.Exit(1)
112								return
113							}
114							fc = ffc
115						}
116						if lineno {
117							fc = withLineNumber(fc, color)
118						}
119						s.Write([]byte(fc))
120					} else {
121						ents := entries(t.Entries)
122						sort.Sort(ents)
123						for _, e := range ents {
124							m, _ := e.Mode.ToOSFileMode()
125							if m == 0 {
126								s.Write([]byte(strings.Repeat(" ", 10)))
127							} else {
128								s.Write([]byte(filemodeStyle.Render(m.String())))
129							}
130							s.Write([]byte(" "))
131							if e.Mode.IsFile() {
132								s.Write([]byte(filenameStyle.Render(e.Name)))
133							} else {
134								s.Write([]byte(dirnameStyle.Render(e.Name)))
135							}
136							s.Write([]byte("\n"))
137						}
138					}
139				}()
140			}
141			sh(s)
142		}
143	}
144}
145
146func withLineNumber(s string, color bool) string {
147	lines := strings.Split(s, "\n")
148	// NB: len() is not a particularly safe way to count string width (because
149	// it's counting bytes instead of runes) but in this case it's okay
150	// because we're only dealing with digits, which are one byte each.
151	mll := len(fmt.Sprintf("%d", len(lines)))
152	for i, l := range lines {
153		digit := fmt.Sprintf("%*d", mll, i)
154		bar := "│"
155		if color {
156			digit = lineDigitStyle.Render(digit)
157			bar = lineBarStyle.Render(bar)
158		}
159		if i < len(lines)-1 || len(l) != 0 {
160			// If the final line was a newline we'll get an empty string for
161			// the final line, so drop the newline altogether.
162			lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l)
163		}
164	}
165	return strings.Join(lines, "\n")
166}
167
168func withFormatting(p, c string) (string, error) {
169	zero := uint(0)
170	lang := ""
171	lexer := lexers.Match(p)
172	if lexer != nil && lexer.Config() != nil {
173		lang = lexer.Config().Name
174	}
175	formatter := &gansi.CodeBlockElement{
176		Code:     c,
177		Language: lang,
178	}
179	r := strings.Builder{}
180	styles := types.DefaultStyles()
181	styles.CodeBlock.Margin = &zero
182	rctx := gansi.NewRenderContext(gansi.Options{
183		Styles:       styles,
184		ColorProfile: termenv.TrueColor,
185	})
186	err := formatter.Render(&r, rctx)
187	if err != nil {
188		return "", err
189	}
190	return r.String(), nil
191}