middleware.go

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