1// Package wailshost is the window-shaped half of the desktop host: the native2// file picker, and the window handle a picker has to be asked for through.3//4// It is the one package in the application that knows what a webview is. The5// handler asks [hostenv.Hostenv.PickFile] for a file; the OS host turns that6// into an [oshost.Picker] call; this turns that call into a WKOpenPanel, a GTK7// chooser or a Windows common dialog, depending on what the binary was built8// for. Everything else the desktop host needs -- the library of .atlas files,9// the session records -- is [oshost]'s and is the same code the headless host10// runs, so the two hosts differ by exactly this file (issue #5 ยง3.3).11//12// It lives under hostenv because that is where the contract says host13// implementations live: depcheck's hostenv analyzer permits the toolkit import14// below here and nowhere else in internal/app.15package wailshost1617import (18 "context"19 "fmt"20 "io"21 "os"22 "path/filepath"23 "sync/atomic"2425 wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"2627 "github.com/FelineStateMachine/atlas/internal/app/hostenv"28)2930// Window is the live window, or the absence of one.31//32// A Wails context exists only between startup and shutdown, and a native33// dialog can only be asked for through it. [Window.Opened] takes it at34// startup -- it is the shell's OnStartup, spelled to be passed directly --35// and [Window.Pick] reads it. The zero value is a host whose window has not36// opened yet, which is a state a request can genuinely arrive in: the page is37// served over the same asset server that the window is still starting.38type Window struct {39 live atomic.Pointer[context.Context]40}4142// Opened records the window's context. It is the shell's OnStartup.43func (w *Window) Opened(ctx context.Context) { w.live.Store(&ctx) }4445// Pick puts the native file picker in front of the reader and opens what they46// chose.47//48// The request's own context is deliberately unused: a native panel is modal49// on the window, not on the HTTP request that raised it, and a reader who50// navigates away mid-dialog should get their file picker dismissed by51// dismissing it rather than by a cancelled fetch. What the panel needs is the52// window's context, which is the one this holds.53//54// The three answers of the contract are all reachable: a file, ErrNoSelection55// when the reader closed the panel with nothing chosen, and ErrNotAvailable56// before there is a window to be modal on -- refused rather than queued,57// because "there is no window yet" is an answer and a hung import is not.58func (w *Window) Pick(context.Context) (io.ReadCloser, string, error) {59 held := w.live.Load()60 if held == nil {61 return nil, "", fmt.Errorf("choosing a bundle: %w", hostenv.ErrNotAvailable)62 }63 chosen, err := wailsruntime.OpenFileDialog(*held, wailsruntime.OpenDialogOptions{64 Title: "Add a volume",65 Filters: []wailsruntime.FileFilter{66 {DisplayName: "Atlas volumes (*.atlas)", Pattern: "*.atlas"},67 },68 })69 if err != nil {70 return nil, "", fmt.Errorf("choosing a bundle: %w", err)71 }72 if chosen == "" {73 return nil, "", hostenv.ErrNoSelection74 }75 file, err := os.Open(chosen)76 if err != nil {77 return nil, "", fmt.Errorf("opening the chosen bundle: %w", err)78 }79 // The store is handed a stream and the name it was offered under, never a80 // path: the name is what the installed file is called, and where it came81 // from is the picker's business and ends here.82 return file, filepath.Base(chosen), nil83}