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