README
¶
ntcharts-osm — Terminal OpenStreetMap widget for Bubble Tea
ntcharts-osm is a Bubble Tea widget that renders OpenStreetMap tiles in the terminal. It pairs flopp/go-staticmaps for tile fetching with ntcharts/v2/picture for image rendering — half-block glyphs anywhere, full-resolution Kitty graphics on terminals that support them (Kitty, Ghostty, WezTerm).

Quickstart
package main
import (
"fmt"
"os"
tea "charm.land/bubbletea/v2"
"github.com/NimbleMarkets/ntcharts-osm/mapview"
)
type model struct{ mv mapview.Model }
func (m model) Init() tea.Cmd { return m.mv.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if k, ok := msg.(tea.KeyMsg); ok && (k.String() == "q" || k.String() == "ctrl+c") {
return m, tea.Quit
}
if sz, ok := msg.(tea.WindowSizeMsg); ok {
m.mv.SetSize(sz.Width, sz.Height)
}
var cmd tea.Cmd
m.mv, cmd = m.mv.Update(msg)
return m, cmd
}
func (m model) View() tea.View { return m.mv.View() }
func main() {
mv := mapview.New(0, 0)
mv.SetLatLng(40.6892, -74.0445, 13) // Statue of Liberty
if _, err := tea.NewProgram(model{mv: mv}).Run(); err != nil {
fmt.Println(err); os.Exit(1)
}
}
Pan with arrows or hjkl, zoom with +/-. The widget owns those bindings — no parent wiring needed.
Demo
A fuller single-pane demo lives at examples/mapview — adds tile-style cycling and Glyph/Kitty mode toggling.
task build-ex-mapview
./bin/ntcharts-osm-mapview
Tile styles
mapview.SetStyle(...) switches between nine tile providers from flopp/go-staticmaps: Wikimedia, OpenStreetMaps, OpenTopoMap, OpenCycleMap, CartoLight, CartoDark, StamenToner, StamenTerrain, ArcgisWorldImagery. Each comes with the upstream provider's terms-of-use and attribution requirements — read them before shipping a public app.
Render modes
| Mode | What it does | Where it works |
|---|---|---|
mapview.RenderGlyph (default) |
Half-block ANSI from pixterm/ansimage, via ntcharts/v2/picture |
Any modern terminal |
mapview.RenderKitty |
Full-resolution image via Kitty graphics protocol | Kitty, Ghostty, WezTerm |
mv.SetRenderMode(mode) returns a tea.Cmd that re-renders at the new mode. Toggling away from Kitty automatically deletes the uploaded image so no ghost stays in the terminal.
Bubble Tea version
Targets Bubble Tea v2 (charm.land/bubbletea/v2). No v1 backport.
Known caveats
- Tile fetching is synchronous per render. Each render builds its own
*sm.Contextinside the dispatched goroutine and tags the result with a generation counter, so rapid pan / zoom / resize fires safely-parallel renders and stale results are dropped. Composited images are kept in a small per-Model LRU keyed on(lat, lng, zoom, cols, rows, style, oversample, markers)— revisiting a state hits the cache synchronously (no goroutine, no Loading overlay). Default cap is 16 entries; tune viamapview.NewWithConfig(Config{CacheCap: N})(-1disables caching). - Server-side supersample (
Config.Oversample). Raises the source-image pixel density without changing visible geographic coverage —Oversample: N(powers of 2) renders the same area atN×per-cell resolution and+log2(N)OSM tile zoom, so Kitty terminals can downscale a sharper source.1(default) keeps current behavior;2is a noticeable boost;4is hi-DPI quality at ~16× the tile fetches. Capped so the effective tile zoom never exceeds 19. Glyph mode pays the cost without visible benefit. - Client-side optical zoom (
Config.OpticalZoom/mv.SetOpticalZoom(n)). Magnifies the cached source image by cropping the center1/2^nof each axis and letting the renderer scale it back up to the cell rectangle. No network, no tile-render goroutine — switching is instant. Pixelated at high N (it's digital zoom in spirit even though we call it optical), but useful for going past the OSM tile-zoom ceiling or just inspecting fine detail without re-fetching. Composes withOversample: e.g.Oversample: 2+OpticalZoom: 1is a 2× zoomed view rendered from a 2× supersampled source. - Aspect-ratio guardrail (
Config.MaxAspectRatio/Config.LetterboxColor). Maps display correctly at any cell-rect AR — every pixel sits at its true geographic location — but at extreme ARs (a 100×5 status bar, say) viewers find the result disorienting because their mental model of "a map" is roughly square.MaxAspectRatio: 3.0lets the cell rect's AR range up to 3:1 in either direction; beyond that, the map portion is rendered at the boundary AR centered in the cell rect, with the rest filled byLetterboxColor(default opaque black;color.Transparentshows the terminal background through the bars). The letterbox is composed into the source image before picture renders, so Glyph and Kitty show identical letterbox geometry — toggling render modes doesn't change layout. Default0keeps the current "fill the cell rect at any AR" behavior; opt in when your layout might land in extreme territory. - No built-in API key handling. Tile providers that require a key (e.g. Thunderforest) are not bundled — add a custom
tileProviderif you need one. - Geocoding uses Nominatim with no caching. Respect their usage policy for production traffic.
License
Many thanks to the OpenStreetMap Foundation (donate).
MIT License — Copyright (c) 2026 Neomantra Corp.
Made with ❤ and 🔥 by the team behind Nimble.Markets.