clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / main.go
main.go 8.4K
1// Command atlas is the desktop shell: one window over the application handler.2//3// Everything in this file is host wiring (issue #5 §3.4). The application is4// one pure http.Handler that touches no filesystem and opens no dialog; what5// is here is the three things a desktop makes of it -- a window to draw it in,6// an operating system to keep the library and the session records on, and a7// native panel to choose a bundle with. Move those three and the same handler8// is `atlas serve`, which is exactly what cmd/atlas/serve.go does in fewer9// lines still.10//11// Two things this shell deliberately does not have. There is no framework: the12// application it hosts consumed exactly one event subscription and one file13// dialog from the one it used to have, and both are here in plain sight. And14// there is no Wails runtime JavaScript in the page -- nothing is bound, no15// bindings JSON is generated, and the page hears that the library moved over16// the application's own SSE stream (issue #5 §4.6). The one place Wails would17// inject its runtime is a 200 text/html answer at a path ending in "/", which18// this application only ever gives on an empty library: every explorer page is19// at /v/<volume>/<world> and is served exactly as the headless host serves it.20//21// The build is a plain `go build`, not `wails build`, which is why there is no22// wails.json: the Wails CLI's job is scaffolding a Vite frontend and this tree23// has none. See the Makefile's `desktop` target and .github/workflows/release.yml24// for the recipe, tags and all.25package main2627import (28 "embed"29 "fmt"30 "io/fs"31 "log/slog"32 "os"33 "path/filepath"3435 "github.com/wailsapp/wails/v2"36 "github.com/wailsapp/wails/v2/pkg/options"37 "github.com/wailsapp/wails/v2/pkg/options/assetserver"3839 "github.com/FelineStateMachine/atlas/format/bundle"40 "github.com/FelineStateMachine/atlas/internal/app"41 "github.com/FelineStateMachine/atlas/internal/app/hostenv/oshost"42 "github.com/FelineStateMachine/atlas/internal/app/hostenv/wailshost"43 "github.com/FelineStateMachine/atlas/internal/logging"44)4546// The seam, embedded, so that the shipped application is one file. `make seam`47// writes render/dist/app.js and `make static` copies it here; a build that48// skipped both ships a static/ holding only its own README, /static/app.js49// answers 404, and the application works without the seam -- which is the50// deletability principle of issue #5 §3.2 standing up in the shipping binary51// rather than only in the test suite.52//53// The stylesheet system is not here. It is internal/app/assets, embedded by54// the application itself and served from /assets, because it is the55// application's own chrome and deleting the seam must not cost the page its56// appearance.57//58//go:embed static59var seam embed.FS6061// The included Earth volume: a real, ordinary .atlas bundle -- NASA's Blue62// Marble base map, composed by `make included-earth` and committed at63// included/ -- embedded so a first launch opens onto a world instead of an64// empty library. It is embedded here and only here: the desktop shell is the65// host that owns a library, where `atlas serve` and the CLI use exactly the66// registry the operator gives them. included/README.md is the provenance and67// the regeneration recipe.68//69//go:embed included/*.atlas70var included embed.FS7172// appIdentifier is the directory the application keeps its data under, on73// whichever configuration directory the machine calls its own. It is the74// identifier the pre-rewrite shell used, unchanged and on purpose: a reader's75// library is where it always was and this build finds it there.76//77// cmd/atlas/serve.go spells the same three constants for the headless host.78// They are duplicated rather than shared because they are each host's own79// account of where it looks, and a host that is not an operating system --80// the WASM one of §3.3 -- will have neither an environment nor a81// configuration directory to read.82const (83 appIdentifier = "dev.felinestatemachine.atlas"84 bundlesDirEnv = "ATLAS_BUNDLES_DIR"85 dataDirEnv = "ATLAS_DATA_DIR"86)8788func main() {89 if err := run(); err != nil {90 fmt.Fprintln(os.Stderr, "atlas:", err)91 os.Exit(1)92 }93}9495func run() error {96 if _, err := logging.Setup(logging.Options{}); err != nil {97 return err98 }99100 data, err := dataDir()101 if err != nil {102 return err103 }104 library := os.Getenv(bundlesDirEnv)105 if library == "" {106 library = filepath.Join(data, "bundles")107 }108 // The library is created rather than merely looked for: a first launch109 // should show a reader an empty library with a path they can drop a110 // bundle into, not an error about a directory nobody has made yet.111 if err := os.MkdirAll(library, 0o755); err != nil {112 return fmt.Errorf("creating the library at %s: %w", library, err)113 }114 // The included Earth build lands before the host is constructed, so the115 // host's first scan of the library already sees it. Installation goes116 // through format/bundle's own path -- validated, versioned, staged,117 // idempotent -- so an already-installed build is a no-op and another Earth118 // build in the library is left exactly where it is, side by side, for the119 // registry fold to order. A built-in that cannot be installed is a startup120 // error rather than a silent absence: the application would be shipping a121 // broken asset, and that has to be heard about.122 if err := installIncluded(library); err != nil {123 return fmt.Errorf("installing the included Earth volume: %w", err)124 }125126 // The window is declared before the host because the host holds its127 // picker and the window learns its context at startup, after the handler128 // is already serving. An import that races the window loses and is told129 // so (wailshost.Window.Pick).130 var window wailshost.Window131 host, err := oshost.New(oshost.Options{132 BundlesDir: library,133 SessionsDir: filepath.Join(data, "sessions"),134 Pick: window.Pick,135 })136 if err != nil {137 return err138 }139140 static, err := fs.Sub(seam, "static")141 if err != nil {142 return err143 }144 handler := app.New(host, app.Options{Static: static})145146 slog.Info("opening", logging.Op("desktop"), logging.Path(library),147 slog.Int("volumes", len(host.Volumes().Volumes())))148149 return wails.Run(&options.App{150 Title: "Atlas — World Explorer",151 // The application *is* the asset server. Every request the page makes152 // -- pages, partials, the /data plane, the events stream -- is served153 // by the same handler the headless host serves, over the webview's own154 // scheme instead of a socket. The one thing that scheme cannot carry155 // is a redirect, so the host walks those itself (redirects.go).156 AssetServer: &assetserver.Options{Handler: followRedirects(handler)},157 OnStartup: window.Opened,158 // A map wants every pixel it can have, so the window opens filling the159 // screen it lands on rather than at a size chosen here. Not fullscreen:160 // the menu bar and the dock stay, and the window is still a window.161 // Width and Height are what it returns to when unzoomed.162 WindowStartState: options.Maximised,163 Width: 1440,164 Height: 920,165 MinWidth: 900,166 MinHeight: 600,167 })168}169170// installIncluded installs every embedded bundle into the library. After it171// returns, the embedded copies are done with: opening, rendering, analysis and172// export all read the installed files through the same paths any imported173// bundle is read through.174func installIncluded(library string) error {175 names, err := fs.Glob(included, "included/*.atlas")176 if err != nil {177 return err178 }179 for _, name := range names {180 data, err := included.ReadFile(name)181 if err != nil {182 return fmt.Errorf("%s: %w", name, err)183 }184 installed, err := bundle.InstallBytes(library, data)185 if err != nil {186 return fmt.Errorf("%s: %w", name, err)187 }188 slog.Info("included volume installed", logging.Op("desktop"),189 logging.Volume(installed.Slug), logging.Stamp(bundle.ShortStamp(installed.Stamp)),190 logging.Path(installed.Locator))191 }192 return nil193}194195// dataDir is where the application keeps what is its own: the library of196// installed volumes and the session records that remember where a reader was.197// ATLAS_DATA_DIR points the whole of it somewhere else, which is how a198// development run reads and writes beside a checkout instead of in the199// reader's real library.200func dataDir() (string, error) {201 if fromEnv := os.Getenv(dataDirEnv); fromEnv != "" {202 return fromEnv, nil203 }204 base, err := os.UserConfigDir()205 if err != nil {206 return "", fmt.Errorf("no %s given and no configuration directory to fall back on: %w",207 dataDirEnv, err)208 }209 return filepath.Join(base, appIdentifier), nil210}