1// ===== font.go =====
  2//go:build js && wasm
  3
  4package main
  5
  6import (
  7	"encoding/base64"
  8	"errors"
  9	"io"
 10	"net/http"
 11	"regexp"
 12	"time"
 13)
 14
 15// The site's font, as its pages use it: /font.css holds an @font-face with
 16// the font inline (a data: URL). The store brings it to the terminal it runs
 17// in, where the host allows (progressive.Font), so it looks as /tui does.
 18
 19var (
 20	fontFamilyRe = regexp.MustCompile(`font-family:\s*['"]?([^'";]+)['"]?\s*;`)
 21	fontDataRe   = regexp.MustCompile(`base64,([A-Za-z0-9+/=]+)\)`)
 22)
 23
 24// siteFont reads the font of the store at origin.
 25func siteFont(origin string) (family string, data []byte, err error) {
 26	client := &http.Client{Timeout: 20 * time.Second}
 27	// "?cors": a URL no page has cached without the CORS header — /font.css
 28	// itself is cached for a day, and a copy from before the header broke the
 29	// store's fetch where the site had been visited.
 30	resp, err := client.Get(origin + "/font.css?cors") //nolint:gosec // the store the person asked for, where its catalog comes from
 31	if err != nil {
 32		return "", nil, err
 33	}
 34	defer resp.Body.Close() //nolint:errcheck // read-side close
 35	if resp.StatusCode != http.StatusOK {
 36		return "", nil, errors.New(resp.Status)
 37	}
 38	css, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
 39	if err != nil {
 40		return "", nil, err
 41	}
 42	fm, dm := fontFamilyRe.FindSubmatch(css), fontDataRe.FindSubmatch(css)
 43	if fm == nil || dm == nil {
 44		return "", nil, errors.New("no inline font in font.css")
 45	}
 46	data, err = base64.StdEncoding.DecodeString(string(dm[1]))
 47	return string(fm[1]), data, err
 48}
 49
 50
 51// ===== main.go =====
 52//go:build js && wasm
 53
 54// Package main wasm/store/main.go — the store TUI as a program of its own,
 55// for any websh: fetched into the shell's filesystem and run from the PATH,
 56// as a separate wasm process with a terminal of its own.
 57//
 58//	curl -o /bin/store https://magnetosphere.net/store.wasm
 59//	store                         # the store this page is served by
 60//	store https://magnetosphere.net
 61//
 62// It is the same TUI `m2 tui` runs on a desktop and /tui runs inside its
 63// page, drawn through childtty rather than either one's screen. Where the
 64// terminal shows placements it lays the site's logo, globe and card form over
 65// its cells, offered from this process (websh's package widget), and, where
 66// the host takes one, draws in the site's own font (font.go).
 67//
 68// Compiled at server startup as a WASMSRC drop-in ('wasm/store').
 69package main
 70
 71import (
 72	"fmt"
 73	"os"
 74	"strings"
 75	"syscall/js"
 76
 77	"github.com/0magnet/bottle/proc"
 78	"github.com/0magnet/websh/childtty"
 79	"github.com/0magnet/websh/progressive"
 80	"github.com/gdamore/tcell/v3"
 81
 82	"github.com/0magnet/m2/pkg/config"
 83	"github.com/0magnet/m2/pkg/tui"
 84)
 85
 86func main() {
 87	origin := proc.Getenv("M2_STORE")
 88	if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "http") {
 89		origin = os.Args[1]
 90	}
 91	if origin == "" {
 92		origin = js.Global().Get("location").Get("origin").String()
 93	}
 94	origin = strings.TrimRight(origin, "/")
 95	config.F.Storeurl = origin
 96	if err := tui.FetchSite(origin); err != nil {
 97		fmt.Fprintln(os.Stderr, "store: site info:", err)
 98	}
 99	catalog, err := tui.FetchCatalog(origin)
100	if err != nil {
101		fmt.Fprintln(os.Stderr, "store: catalog:", err)
102		os.Exit(1)
103	}
104	var sc tcell.Screen
105	if t, ok := childtty.Open(); ok {
106		// Its site's font, where the host takes one: the store then looks
107		// as /tui does, rather than in whatever monospace the host has.
108		if progressive.Current().Has("font") {
109			if family, data, err := siteFont(origin); err == nil {
110				for _, seq := range progressive.Font(family, 0, data) {
111					t.Write([]byte(seq)) //nolint:errcheck,gosec // the terminal; a font is a nicety
112				}
113			}
114		}
115		tui.StartPath = progressive.Current().LinkPath() // a link opens the store where it pointed
116		sc, err = tcell.NewTerminfoScreenFromTty(t)
117	} else {
118		sc, err = tcell.NewScreen()
119	}
120	if err == nil {
121		err = sc.Init()
122	}
123	if err != nil {
124		fmt.Fprintln(os.Stderr, "store:", err)
125		os.Exit(1)
126	}
127	if err := tui.RunOn(sc, catalog); err != nil {
128		fmt.Fprintln(os.Stderr, "store:", err)
129		os.Exit(1)
130	}
131	os.Exit(0) // TinyGo keeps a js program alive after main returns
132}
133
134