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