feat: display files middleware

Ayman Bagabas created

sshing with a filepath in a non-interactive terminal spits out the file
content with the option of syntax highlighting and line numbers

Change summary

server/middleware.go | 144 ++++++++++++++++++++++++++++++++++++++++++++++
server/server.go     |   1 
2 files changed, 145 insertions(+)

Detailed changes

server/middleware.go 🔗

@@ -0,0 +1,144 @@
+package server
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/alecthomas/chroma/lexers"
+	gansi "github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/lipgloss"
+	appCfg "github.com/charmbracelet/soft-serve/internal/config"
+	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
+	"github.com/charmbracelet/wish"
+	"github.com/charmbracelet/wish/git"
+	"github.com/gliderlabs/ssh"
+	gg "github.com/go-git/go-git/v5"
+	"github.com/muesli/termenv"
+)
+
+// softServeMiddleware is a middleware that handles displaying files with the
+// option of syntax highlighting and line numbers.
+func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
+	return func(sh ssh.Handler) ssh.Handler {
+		return func(s ssh.Session) {
+			_, _, active := s.Pty()
+			cmds := s.Command()
+			if !active && len(cmds) > 0 {
+				func() {
+					formatting := false
+					lineno := false
+					fp := filepath.Clean(cmds[0])
+					ps := strings.Split(fp, "/")
+					repo := ps[0]
+					repoExists := false
+					for _, rp := range ac.Source.AllRepos() {
+						if rp.Name == repo {
+							repoExists = true
+						}
+					}
+					if !repoExists {
+						return
+					}
+					auth := ac.AuthRepo(repo, s.PublicKey())
+					if auth < git.ReadOnlyAccess {
+						s.Write([]byte("unauthorized"))
+						s.Exit(1)
+						return
+					}
+					for _, op := range cmds[1:] {
+						if op == "formatting" {
+							formatting = true
+						} else if op == "lineno" || op == "linenumber" {
+							lineno = true
+						}
+					}
+					rs, err := ac.Source.GetRepo(repo)
+					if err != nil {
+						_, _ = s.Write([]byte(err.Error()))
+						_ = s.Exit(1)
+						return
+					}
+					fc, err := readFile(rs.Repository, strings.Join(ps[1:], "/"))
+					if err != nil {
+						_, _ = s.Write([]byte(err.Error()))
+						_ = s.Exit(1)
+						return
+					}
+					if formatting {
+						ffc, err := withFormatting(fp, fc)
+						if err != nil {
+							s.Write([]byte(err.Error()))
+							s.Exit(1)
+							return
+						}
+						fc = ffc
+					}
+					if lineno {
+						fc = withLineNumber(fc, formatting)
+					}
+					s.Write([]byte(fc))
+				}()
+			}
+			sh(s)
+		}
+	}
+}
+
+func readFile(r *gg.Repository, fp string) (string, error) {
+	l, err := r.Log(&gg.LogOptions{})
+	if err != nil {
+		return "", err
+	}
+	c, err := l.Next()
+	if err != nil {
+		return "", err
+	}
+	f, err := c.File(fp)
+	if err != nil {
+		return "", err
+	}
+	fc, err := f.Contents()
+	if err != nil {
+		return "", err
+	}
+	return fc, nil
+}
+
+func withLineNumber(s string, formatting bool) string {
+	st := lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
+	lines := strings.Split(s, "\n")
+	mll := fmt.Sprintf("%d", len(fmt.Sprintf("%d", len(lines))))
+	for i, l := range lines {
+		lines[i] = fmt.Sprintf("%-"+mll+"d │ %s", i+1, l)
+		if formatting {
+			lines[i] = st.Render(lines[i])
+		}
+	}
+	return strings.Join(lines, "\n")
+}
+
+func withFormatting(p, c string) (string, error) {
+	zero := uint(0)
+	lang := ""
+	lexer := lexers.Match(p)
+	if lexer != nil && lexer.Config() != nil {
+		lang = lexer.Config().Name
+	}
+	formatter := &gansi.CodeBlockElement{
+		Code:     c,
+		Language: lang,
+	}
+	r := strings.Builder{}
+	styles := types.DefaultStyles()
+	styles.CodeBlock.Margin = &zero
+	rctx := gansi.NewRenderContext(gansi.Options{
+		Styles:       styles,
+		ColorProfile: termenv.TrueColor,
+	})
+	err := formatter.Render(&r, rctx)
+	if err != nil {
+		return "", err
+	}
+	return r.String(), nil
+}

server/server.go 🔗

@@ -37,6 +37,7 @@ func NewServer(cfg *config.Config) *Server {
 	mw := []wish.Middleware{
 		rm.MiddlewareWithLogger(
 			cfg.ErrorLog,
+			softServeMiddleware(ac),
 			bm.Middleware(tui.SessionHandler(ac)),
 			gm.Middleware(cfg.RepoPath, ac),
 			lm.Middleware(),