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						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}