mapterm
A map in your terminal, with the pan, zoom and styling of a web map, that also works over SSH.
Get it with cargo install mapterm (other ways).
https://github.com/user-attachments/assets/8bf59b7f-3156-4820-aeeb-2b138fc5bc2b
- Any vector tiles: OpenFreeMap out of the box (no key), PMTiles files locally or over HTTP, XYZ URLs and TileJSON, drawn in braille with cell-color fills and labels that don't collide.
- MapLibre styles: liberty, positron and the other OpenFreeMap styles, or your own, with expressions, filters, fonts and point icons.
- Imagery: satellite and aerial layers (OSM, Esri, USGS, NASA GIBS, and keyed providers such as Vantor, Planet and Mapbox) as real images in kitty-graphics terminals, or as cell colors anywhere else.
- Your data: GeoJSON, CSV, GPX, KML and FlatGeobuf, from a file or a URL, drawn over the map, with clustering for large point sets.
- Search and inspect: find a place and fly there; point at anything to see its features and properties, and copy them as JSON.
- Scriptable:
--snapshotprints a frame as text, ANSI or HTML, and--pickprints the features at a spot as JSON.
The same code is a library: a ratatui widget with a MapLibre GL style API (sources, style layers, expressions, events and feature state, feature queries) that you can drop into your own terminal apps.
Docs, a tour and the full guide: mapterm.com
Install
With Cargo (Rust 1.89 or newer, from rustup.rs):
On Linux (x86-64) you can also download a signed binary from
Releases; the release notes show how to
verify it. To use the library in your own app: cargo add mapterm.
Try it
Without --style, mapterm draws the source with its own terminal style (OpenMapTiles
or Protomaps schema, detected from the tiles; --light or t for the light theme).
MapLibre styles draw what a terminal can show: colors, fills, lines, labels (bold and
italic fonts), filters and zoom ranges, and point icons as one-cell symbols (✚ hospital,
✈ airport, ⋔ restaurant, ◡ café, Ⓑ bus stop, ...). Wide lines draw two or three
braille dots thick and dashed lines dotted; imagery opacity blends with the
background in cell-color mode; blur, patterns and 3D are skipped. The
built-in style shows points of interest from zoom 15, more as you zoom in.
Controls
They follow the MapLibre GL / Mapbox GL defaults:
- Scroll / two-finger swipe up and down: zoom toward the cursor
- Drag: pan, and a fling keeps gliding (inertia)
- Double-click: zoom in; shift+double-click: zoom out
- Shift+drag: box zoom
- Arrows (or
hjkl) pan,+/-zoom to the next level, both eased /searches for a place: type, press Enter, and the map flies there (MapLibre's flyTo curve). Several matches open a list (↑/↓, Enter or click); Esc clears the marker. Searches go to Photon, an OpenStreetMap geocoder, only when you press Enter;MAPTERM_GEOCODER=nominatimor the URL of your own Photon or Nominatim server switches.mswitches the map style: the built-in one, then OpenFreeMap's liberty, positron, bright, dark and fiord (a--styleyou started with comes first). Imagery stays underneath and your GeoJSON layers on top.ccopies the coordinates under the pointer (or the map center) aslat, lonto the clipboard (OSC 52).- Terminal extras: sideways swipes pan;
iinspect,vvector overlay,nlabels,tlight/dark theme (built-in style),qquit
GeoJSON layers
--geojson FILE|URL draws your data over the map, as many layers as you like:
Besides GeoJSON it reads CSV/TSV with latitude/longitude columns (lat,
latitude, lon, lng, longitude, any case) or a WKT geometry column, GPX
(waypoints, tracks, routes), KML (placemarks with their names, descriptions
and extended data; unzip KMZ first) and FlatGeobuf (lon/lat). The format comes
from the extension, else the content. Files and URLs up to 512 MB.
- Points, lines and polygons (including Multi* and GeometryCollection), each layer in its own color with a legend in the status bar; polygon interiors are tinted (left clear over a raster layer).
- Features with a
name(ortitle/label) property are labeled. - Without
--at, the map starts zoomed to fit all layers. - Inspect mode (
i) lists your features first, with all their properties, and--snapshot ... --pick COL,ROWprints them as JSON lines. - Big files stay fast: features are spatially indexed, and a layer with more
than 1,000 points clusters them (
◉ 152) until you zoom in, as MapLibre'scluster: truedoes.
Raster layers
--raster puts an image tile layer under the vector map, by provider name or URL:
--raster also takes a PMTiles archive of PNG, JPEG or WebP tiles, as a local
file or an https:// URL read with range requests: a single file of your own
imagery that works offline.
| Name | Imagery | Key |
|---|---|---|
osm |
OpenStreetMap standard map (light use only) | none |
esri |
Esri World Imagery, global satellite | none |
usgs |
USGS National Map imagery, US only, public domain | none |
eox |
Sentinel-2 cloudless 2024, global, ~10 m (CC BY-NC-SA) | none |
gibs |
NASA GIBS daily satellite imagery, ~250 m (--date, default yesterday) |
none |
esri-light, esri-dark |
Esri Gray Canvas, quiet backgrounds for overlays | none |
vantor |
Vantor (Maxar) Vivid basemap | VANTOR_API_KEY |
planet |
Planet basemaps (your subscription's mosaics) | PL_API_KEY + a mosaic |
mapbox |
Mapbox Satellite, Retina tiles | MAPBOX_ACCESS_TOKEN |
maptiler |
MapTiler Satellite | MAPTILER_KEY |
API keys never go on the command line: they come from the provider's environment variable or from a command that prints the key, such as your password manager's CLI:
Keys are only added to requests (Vantor's as the maxar-api-key header) and are
blanked out of errors. Keyed providers don't cache tiles on disk unless you pass
--raster-cache, since commercial licenses often limit storing them. For any
other service, use its URL with {key} where the key goes (filled from
MAPTERM_RASTER_KEY or --raster-key-cmd), or for header-based keys
--raster-header 'Name: {key}' (e.g. 'Authorization: Bearer {key}'). Write {key}
rather than the key itself: anything on the command line shows up in ps and in your
shell history.
Planet serves each customer's named mosaics: list the ones your key can use
with mapterm --raster-list-mosaics, then pick one with
--raster planet --raster-param mosaic=NAME (or PLANET_MOSAIC). Planet only
accepts the key in the tile URL; mapterm still never prints it.
--raster-param NAME=VALUE fills any other {NAME} in a custom URL; for GIBS,
--raster-param layer=VIIRS_SNPP_CorrectedReflectance_TrueColor picks another
layer (JPEG layers at 250 m).
- URLs use
{z},{x}and{y}in any order; for TMS services (rows counted from the bottom) use{-y}or add--tms. - The status bar credits the imagery: Esri, USGS, OpenStreetMap and EOX are
known; give anything else with
--raster-attribution TEXT. - On terminals with the kitty graphics protocol (Ghostty, kitty, WezTerm, and
through herdr) tiles are real images underneath the text, so vector labels,
lines, markers and inspect draw on top. Each tile is sent once; panning and
zooming only move it. Elsewhere the layer becomes cell background colors.
MAPTERM_GRAPHICS=kittyorcellsoverrides the guess. - mapterm keeps at most 48 MiB of decoded images in the terminal (herdr allows
64 MiB per pane), freeing tiles that are off screen first, so the terminal never
drops one that is showing.
MAPTERM_KITTY_BUDGET_MBchanges it. vcycles the vector overlay: lines and labels, labels only, off. Area fills are always off over a raster layer.- Tiles are cached on disk for 30 days and fetched at most two at a time. Respect each tile server's usage policy: OpenStreetMap's own tile servers allow only light use, with attribution.
Inspect
Press i to switch to inspect mode, and i (or Esc) again to leave it.
- Hover shows the top feature under the pointer in the status bar.
- Click lists every feature at that spot, drawn or not (roads, street names, buildings, parks, landcover, points of interest...), most specific first, and highlights the selected one on the map.
- The panel shows the style layer that draws the feature (or that none does), its source layer, geometry type, center, length or area, vertex count, source tile, and all its properties.
- Click an entry or press Tab / Shift+Tab to switch between them;
ycopies the selected feature as JSON to the clipboard (OSC 52).
For scripts, --pick prints the same information as JSON lines:
What it reads
- PMTiles v3, from a local file or over HTTP range requests (own reader)
- XYZ URL templates (
…/{z}/{x}/{y}.pbf) and TileJSON endpoints - Vector tiles in the OpenMapTiles schema (OpenFreeMap, MapTiler) or the Protomaps basemap schema, in dark and light styles
Every download has a 30 s timeout and an 8 MB cap, decompression is capped at
32 MB, and HTTP tiles are cached on disk for 30 days (~/.cache/mapterm).
Rendering
Roads, rivers and boundaries are braille lines (2x4 dots per character); water, parks and buildings fill the cell background; labels avoid each other. While sharper tiles load, lower-zoom tiles stand in for them.
Use it in your own app
mapterm is also a library that works like MapLibre GL: a Map holds named
sources and an ordered list of style layers that say how to draw them.
let mut map = from_style.await?; // any MapLibre style
map.add_source?;
map.add_layer?;
map.add_layer?;
map.load_tiles.await?;
frame.render_widget;
let under = map.query_rendered_features;
- Styles:
Map::from_styletakes an OpenFreeMap name (liberty,positron,bright,dark,fiord), a style URL or file, ormapterm-dark/mapterm-light, built-in styles tuned for terminal cells. Style layers can also be built in Rust withStyleLayer. - Change the map with
set_data,set_paint,set_layout,set_filter,set_visibilityandmove_layer. Filters and paint values are MapLibre expressions (get,match,case,interpolate,step, ... and the legacy filter syntax). - A terminal cell can't show line widths, blur or fonts, so only colors,
opacity (blended with the background), filters, zoom ranges,
text-field, bold and italic fonts,text-anchorandsymbol-sort-keyare used. Themapterm:glyphlayout property sets the character drawn at points. - GeoJSON sources are spatially indexed, and
with_cluster(orcluster: truein a style) clusters points; clusters carrypoint_countandpoint_count_abbreviated, andcluster_expansion_zoomsays where they split. events::Eventsturns the pointer and clicks into MapLibre-style layer events (Click,Enter,Leavefor the layers you name) and can set ahoverfeature state, so a layer styled with["feature-state", "hover"]lights up under the pointer.set_feature_stateworks for any feature with an id (GeoJSONid, vector tile ids, or a source'spromoteIdproperty).- Anything else is a
CustomLayer:draw(lines, fills, markers and labels through aDrawContext) andpick. Markers and labels from every layer are placed together, so they never overlap. Controllergives MapLibre-style mouse and keyboard handling and camera moves (fly_to,ease_to,fit_bounds), andtake_clickreports clicks for your own popups and panels.geocode::Geocodersearches places.
examples/hello_map.rs builds a basemap from a few
style layers and adds a pin in about 50 lines, and
examples/style.rs shows any style
(cargo run --release --example style -- dark). src/bin/mapterm.rs is the
full viewer built on the same Map (background tile loading with
tile_requests / insert_loaded, kitty images with kitty_commands, and
query_features for inspect).
Experimental: smooth scrolling
Terminals move content a whole character at a time. As an experiment, mapterm
can move the map by single pixels on terminals that support an experimental
smooth-scroll protocol. Today that is only
Ghostty Smooth, a personal
fork of Ghostty (see its
SMOOTH.md), and
the herdr fork relays it
from herdr panes. Both are experimental and may change.
It is off unless you turn it on with an environment variable:
MAPTERM_SMOOTH=shift: dragging follows the pointer pixel by pixel; scrolling still zooms.MAPTERM_SMOOTH=1: two-finger up/down pans the map smoothly instead of zooming.
With either set, s toggles it while mapterm runs. Terminals without the
protocol ignore it and behave as usual.
The animation at the top of this page and the full video were recorded this way:
real mapterm in Ghostty Smooth with MAPTERM_SMOOTH=shift. In other terminals the map
moves the same way, but a whole character at a time.
License
MIT. Map data © OpenStreetMap contributors.