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/internal/tui/bubbles/git/types"
 13	"github.com/charmbracelet/wish"
 14	"github.com/charmbracelet/wish/git"
 15	"github.com/gliderlabs/ssh"
 16	gg "github.com/go-git/go-git/v5"
 17	"github.com/muesli/termenv"
 18)
 19
 20var (
 21	linenoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
 22)
 23
 24// softServeMiddleware is a middleware that handles displaying files with the
 25// option of syntax highlighting and line numbers.
 26func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
 27	return func(sh ssh.Handler) ssh.Handler {
 28		return func(s ssh.Session) {
 29			_, _, active := s.Pty()
 30			cmds := s.Command()
 31			if !active && len(cmds) > 0 {
 32				func() {
 33					color := false
 34					lineno := false
 35					fp := filepath.Clean(cmds[0])
 36					ps := strings.Split(fp, "/")
 37					repo := ps[0]
 38					if repo == "config" {
 39						return
 40					}
 41					repoExists := false
 42					for _, rp := range ac.Source.AllRepos() {
 43						if rp.Name() == repo {
 44							repoExists = true
 45						}
 46					}
 47					if !repoExists {
 48						return
 49					}
 50					auth := ac.AuthRepo(repo, s.PublicKey())
 51					if auth < git.ReadOnlyAccess {
 52						s.Write([]byte("unauthorized"))
 53						s.Exit(1)
 54						return
 55					}
 56					for _, op := range cmds[1:] {
 57						if op == "-c" || op == "--color" {
 58							color = true
 59						} else if op == "-l" || op == "--lineno" || op == "--linenumber" {
 60							lineno = true
 61						}
 62					}
 63					rs, err := ac.Source.GetRepo(repo)
 64					if err != nil {
 65						_, _ = s.Write([]byte(err.Error()))
 66						_ = s.Exit(1)
 67						return
 68					}
 69					fc, err := readFile(rs.Repository(), strings.Join(ps[1:], "/"))
 70					if err != nil {
 71						_, _ = s.Write([]byte(err.Error()))
 72						_ = s.Exit(1)
 73						return
 74					}
 75					if color {
 76						ffc, err := withFormatting(fp, fc)
 77						if err != nil {
 78							s.Write([]byte(err.Error()))
 79							s.Exit(1)
 80							return
 81						}
 82						fc = ffc
 83					}
 84					if lineno {
 85						fc = withLineNumber(fc, color)
 86					}
 87					s.Write([]byte(fc))
 88				}()
 89			}
 90			sh(s)
 91		}
 92	}
 93}
 94
 95func readFile(r *gg.Repository, fp string) (string, error) {
 96	l, err := r.Log(&gg.LogOptions{})
 97	if err != nil {
 98		return "", err
 99	}
100	c, err := l.Next()
101	if err != nil {
102		return "", err
103	}
104	f, err := c.File(fp)
105	if err != nil {
106		return "", err
107	}
108	fc, err := f.Contents()
109	if err != nil {
110		return "", err
111	}
112	return fc, nil
113}
114
115func withLineNumber(s string, color bool) string {
116	lines := strings.Split(s, "\n")
117	mll := fmt.Sprintf("%d", len(fmt.Sprintf("%d", len(lines))))
118	for i, l := range lines {
119		lines[i] = fmt.Sprintf("%-"+mll+"d", i+1)
120		if color {
121			lines[i] = linenoStyle.Render(lines[i])
122		}
123		lines[i] += " │ " + l
124	}
125	return strings.Join(lines, "\n")
126}
127
128func withFormatting(p, c string) (string, error) {
129	zero := uint(0)
130	lang := ""
131	lexer := lexers.Match(p)
132	if lexer != nil && lexer.Config() != nil {
133		lang = lexer.Config().Name
134	}
135	formatter := &gansi.CodeBlockElement{
136		Code:     c,
137		Language: lang,
138	}
139	r := strings.Builder{}
140	styles := types.DefaultStyles()
141	styles.CodeBlock.Margin = &zero
142	rctx := gansi.NewRenderContext(gansi.Options{
143		Styles:       styles,
144		ColorProfile: termenv.TrueColor,
145	})
146	err := formatter.Render(&r, rctx)
147	if err != nil {
148		return "", err
149	}
150	return r.String(), nil
151}