OpenRadar is a Tauri 2 desktop app: a Rust binary that decodes weather data and a React 19 interface drawn with MapLibre GL, built with TypeScript and Vite. The shell stays small, and the map renders vector tiles and raster weather on the GPU.
Most of the weather this app draws arrives as a format a browser cannot open: GRIB2 grids, NEXRAD Level II volumes, NetCDF satellite files, PMTiles archives. Decoding those in Rust keeps the work off the thread drawing the map, and it means no service has to render the data first and hand over a picture somebody else has already made decisions about.
A decoded product reaches the map through a registered URI scheme, so it is an ordinary tile source rather than a special case in the timeline. Everything the native side fetches goes through one host allowlist and one bounded cache, which is what lets the app open on the last view with no network.
Every module in src-tauri/src/:
The network boundary. http is the one place a Rust-side fetch happens, and every address is checked against the allowlist first. cache is the disk cache behind the offline last view: entries are written beside their name and renamed into place, the format version is the directory name, and the budget is 2,048 entries or 256 MiB with the oldest going first. A hash collision is a miss rather than the wrong tile, because the address is written into the entry and checked on the way out. tiles serves the cached scheme to the webview, falling back to what was kept when a fetch fails. The cache can also be read and emptied from Settings, and emptying it touches only its own version directory: incident packs and replay bundles sit beside it and are downloads a reader asked for.
Radar. mrms decodes the national mosaic. Those files are GRIB2 with data representation template 41, which is a 16-bit PNG plus a linear scale, so the decode needs no GRIB library at all. It is a directory rather than a file, split the same way and for the same reason: products for the table and the two ramps each entry carries, decode for GRIB2 to a grid, listing for what the bucket holds, tiles for the address the map sends and the picture that answers it, cache for the decoded grids and the byte budget that bounds them, and window for the cells under a box that an export reads. level2 is single-site NEXRAD: it lists the recent and historical archive buckets, opens local Archive II files, decodes volumes and draws one sweep as a Web Mercator PNG laid over the site's own extent. It is a directory rather than a file, split by what a change touches: ramp for the colours and the projection, listing for what the archive holds, decode for bytes to a scan, sweep for choosing a cut, render and draw for the picture, sites for which radar can see a place, commands for what the frontend calls, and section for a vertical slice. chunks is the same radar a few minutes earlier, assembling the pieces of a volume the radar is still sweeping. level3 reads the products a site publishes ready-made, and tdwr is the FAA's airport radars, which publish only in that form. dealias unfolds velocity, and vad fits the wind a sweep is moving in so storm-relative velocity has something to subtract. shear works out the azimuthal derivative of that velocity by the published least squares method, which is rotation at the site's own resolution rather than the two-minute national grid, and finds the gates where a tornado debris signature sits beside a circulation. kdp takes the slope of the volume's differential phase, which is the field that says how hard it is raining without asking the radar to be calibrated or the beam to be unblocked, and which ships only as a finished Level III product. derive runs along the other axis: the column over each point of ground, sampled from every cut of a volume, which is where composite reflectivity, the echo top, the water the column holds and the hail it could be making all come from. It also looks for the three-body scatter spike, the faint flare big hail throws out behind itself, and that search goes one cut at a time because the flare sits behind the core on the cut that sees it rather than anywhere in a column. vwp runs that same fit at a set of heights rather than once for the sweep, which is a wind profile, and refuses a height whose ring it cannot vouch for rather than drawing a barb nobody measured. snowfall reads the office's national snowfall analysis, which is a single band of floating point depths in an LZW GeoTIFF on a plain geographic grid, and colours it: precipitation type says what is falling and this is the only thing that says how much of it landed. melting reads the bright band out of the volume's own highest cut: falling snow picks up a skin of meltwater before it collapses into a raindrop, and for those few hundred metres it is bright, it looks flat to the two polarisations, and nothing in the beam correlates with anything else in it. That band is the freezing level, and reading it here rather than from a sounding matters because the nearest sounding may be hours old and a hundred miles away while the band is in the volume on screen. echo_mask uses that band too: it decides which gates of a cut are echo that is not weather, by a correlation coefficient below what rain gives and a correlation that wanders from gate to gate, with a lower bar inside the melting layer and a strong core let back in when a higher cut sees a column over it. It changes the picture only, so the readout, the exports and the vertical slice never call it. cross_section cuts a volume along a line between two points. gates is where a range turns into a gate. The ICD puts first_gate_range_km at the centre of the first gate, the model's own reader truncates as though it were the near edge, and everything that samples a sweep goes through this one place, so the picture, the readout under the cursor, the vertical slice and the wind profile all agree about where a reading is. radar_status asks the NWS what each radar says about itself, which is how the picker knows a site is restarting rather than merely quiet. crash is the small second process that watches this one: a decoder walking off the end of a buffer takes the app down with no panic and no line in the log, and this leaves a minidump behind on the machine so the next launch has something to point at.
Everything else in the sky. lightning reads GOES flash centroids out of NetCDF-4, which is HDF5 underneath, for the window the map draws and, for a replayed storm, for every file over the replay (GOES-16's bucket before April 2025, GOES-19's after), keeping only the flashes near a watched place. gfs and hrrr decode the model fields the wind particles and the forecast tail are drawn from, including HRRR's near-surface smoke. grib holds the parts of that format all three GRIB readers agree on, so a convention like the sign-and-magnitude integers the scale factors are stored as is written down once rather than three times. probsevere reads the severe-probability model.
Leaving with something. exports writes the picture, the video and the GIF. data_export writes the readings rather than the picture, and geotiff is the single-band float raster half of that. bundles reads and writes the .orb replay file, whose layout is documented in full at the top of the module. hex writes the checksums the replay bundle, the data export and the incident packs carry, in one place, because they are verified against files readers already have on disk and a checksum that changed shape would reject all of them. palette holds the colour tables, and contrast is the colour-vision measurement they are held to; it is compiled into the tests only, because what ships is the ramps it vouches for rather than the arithmetic.
Kept on the machine. incident_packs owns durable offline basemaps, separately from the disposable cache: a bounded tile set is journaled with per-tile hashes, written to PMTiles, read back tile by tile and hashed before an atomic rename makes it selectable. journal is the reader's own record, one JSON row per line so a damaged line costs one entry rather than the file. settings_backup keeps a copy of the settings file before each write and puts it back when the live one will not parse, on the workspace's own first question rather than in the setup hook, which is what keeps it ahead of the store's read; because that file is read as a whole and one bad byte in it loses every place, palette and pack at once. safe_start writes a mark at startup and removes it the moment the workspace says it is up, so a window that dies before it draws, twice running, opens plain the third time rather than into the same crash; a crash an hour into an afternoon, or a machine restarted with the app in the tray, is not that and is not counted.
The desktop itself. tray owns the icon, its menu and the small glance window; glance is what that window reads. wallpaper writes one PNG and points the desktop at it, remembering what was there before so it can be put back. display holds the screen on while the full-screen view is showing, on the main thread, because the Win32 call that does it is per thread and that thread ends with the process. sound reads the alert sound a reader chose, refusing anything too large or of a kind the picker does not offer. host answers for the WebView2 runtime's version, which updates on Microsoft's schedule rather than with the app. window_geometry centres the window at the size the app opens at, which is the one piece of the arrangement that lives outside the settings file: the crash screen's Reset layout calls it so a window restored onto a monitor that is no longer there can be recovered.
Testing. fixture writes Level II volumes byte by byte to the published layout, so a test has a volume with known readings in it. A real one is ten megabytes, cannot be committed, and would make the weather part of the test.
src/components/MapViewport.tsx owns the MapLibre instance and the camera bridge. src/lib/overlays/ holds one adapter per non-radar layer, each owning its fetch, its layer specs and its popup text, driven from the viewport by src/hooks/useOverlays.ts. src/lib/providers/ holds the radar source chain, its rolling request budget and its per-source health. src/App.tsx wires the hooks together and lays out three pieces: the panes, everything the command bar opens, and the chrome around the map. It is a shell over src/hooks/, which is where the work is: useWorkspaceFeeds for everything the map draws that is not the radar picture, useWorkspaceReport for what the app can say about itself, useReplayBundles, useStormTrack, useGlanceWindow, useWallpaper and the rest, one file per subject. The split was by what a change touches, the same way the two native modules were split. src/lib/settings/ is what the workspace remembers: types for the shapes, which imports nothing at runtime so any leaf can read them, defaults for what a first run looks like, read for the two ways a value comes out of a file that may be older than this build, camera and sections for the parts with no module of their own, normalize for putting a whole file back together, restore for one handed back in, and store for where it lives between runs. Each section is normalised beside the module that owns the vocabulary it is written in, so watch.ts reads a watch and palette.ts reads a colour table.
Camera state is kept independent of radar playback, which is what lets a loop run while the camera moves. Settings are stored as readable JSON. The MapLibre worker stays on the app origin.
src/i18n/ holds the copy. English is the source and the other languages are typed against it, so a string added on one side and not the others is a build error rather than a blank label. Each translation is a chunk fetched on demand.
src/lib/theme.ts is the boundary between how the workspace looks and how the data looks. A theme can reach eleven chrome tokens and nothing else: not a reflectivity ramp, not a warning outline, not a hazard colour. That boundary is a test rather than a promise.
MapLibre 6 requires WebGL2 and has no software fallback, so the app checks for it before mounting and explains the failure rather than letting the renderer throw.
A rule a component applies inline can only be tested by a test that writes the rule out again, and a test that restates a rule passes against code that no longer follows it. Three did here before anybody noticed. So a rule of any weight is pulled out into a named function next to the code that calls it, exported, and driven directly by its test: which frames fall inside the loop, when an overlay is worth refetching, how long to wait before trying a stale forecast cycle again, which warnings pair with which layer.
That means a large number of exported symbols in src/ whose only caller outside their own file is a test. npm run check prints how many, along with how many are named only inside their own file, because a count written down here is a count nobody updates. They are not dead code and they are not an oversight. scripts/unused-exports.mjs, which npm run check runs, reports a symbol nothing in the tree names at all, and says nothing about one a test drives.
WPF with WebView2 would be Windows-only and still carry two runtimes. Avalonia has a weaker path for the raster and custom radar layers this needs. Electron adds a much larger desktop runtime. Qt brings licensing and distribution friction that does not help this project.