A small, cross-platform configuration path resolver and storage library for Nim.
configy answers one question for any Nim application or library:
"Where do I put my config data on this platform, and how do I read/write it safely?"
It resolves a predictable, vendor-namespaced directory per platform, creates it on first use, and provides thin storage helpers for JSON, raw binary, and typed generics — with optional Snappy compression on all three. It is plumbing, not policy.
Paths follow the XDG Base Directory Specification
on all desktop targets. dep is optional — omit it to put files directly under the app directory.
| Platform | With dep | Without dep |
|---|---|---|
| Desktop (Linux/macOS) | ~/.config/<vendor>/<app>/<dep>/ |
~/.config/<vendor>/<app>/ |
| Windows | ~\.config\<vendor>\<app>\<dep>\ |
~\.config\<vendor>\<app>\ |
| Nintendo 3DS | sdmc:/config/<vendor>/<app>/<dep>/ |
sdmc:/config/<vendor>/<app>/ |
| PSP | ms0:/PSP/config/<vendor>/<app>/<dep>/ |
ms0:/PSP/config/<vendor>/<app>/ |
| PS Vita | ux0:data/config/<vendor>/<app>/<dep>/ |
ux0:data/config/<vendor>/<app>/ |
| WebAssembly | localStorage key prefix config/<vendor>/<app>/<dep>/ (stubbed) |
config/<vendor>/<app>/ (stubbed) |
<vendor>— your organization/project namespace, set at compile time (required).<app>— the consuming application name, supplied at call time.<dep>— the library or feature name (optional), supplied at call time.
Overload note:
configDir(app, x)treatsxas a dep (a sub-directory).configFile(app, x)treatsxas a filename (the file itself). Both accept(string, string)but the second argument means different things. A three-arg call that accidentally loses an argument compiles silently to a different path — make sure you intend the dep-less form when using two positional arguments.Name collision: do not use the same name as both a dep-less filename and a dep (e.g.
configFile(app, "x")andconfigDir(app, "x")under the same app). A filesystem cannot hold both a file and a directory namedx—ensureConfigDirwill raiseConfigIOErrorat runtime if you try.
On all desktop targets (Linux, macOS, Windows), $XDG_CONFIG_HOME is respected when it
is set, non-empty, and an absolute path. If any of those conditions fails, configy
falls back to ~/.config/. This env-var resolution rule follows the XDG Base Directory
Specification. Note that configy nests files under <vendor>/<app>/ (and optionally
<dep>/) within the XDG config root rather than the spec's conventional flat
$XDG_CONFIG_HOME/<app>/ layout — the extra namespacing prevents vendor collisions.
configy uses ~/.config/ on Windows instead of the conventional %APPDATA%. This is an
intentional choice for cross-platform CLI tools that want a single, predictable path shape
on all desktops. If you need %APPDATA% behavior on Windows, set XDG_CONFIG_HOME=%APPDATA%
in your environment.
The vendor namespace is required and set at compile time:
nim c -d:configyVendor=myorg myapp.nimconfigy has no built-in default. The build fails with a clear error if
-d:configyVendor is not provided.
# Compile with: nim c -d:configyVendor=myorg myapp.nim
# (myorg is <vendor> from the path tables above — set at compile time)
import configy
import std/options
# ── Typed generic (most ergonomic) ───────────────────────────────────────────
type Settings = object
volume: float
fullscreen: bool
# With dep — file lives at ~/.config/myorg/myapp/mylib/settings.json
if isWritable():
writeConfig("myapp", "mylib", "settings.json",
Settings(volume: 0.8, fullscreen: true), compress = true)
let s = readConfig[Settings]("myapp", "mylib", "settings.json")
# Without dep — file lives at ~/.config/myorg/myapp/settings.json
if isWritable():
writeConfig("myapp", "settings.json", Settings(volume: 0.8, fullscreen: true))
let s2 = readConfig[Settings]("myapp", "settings.json")
# ── JsonNode (flexible, schema-free) ─────────────────────────────────────────
if isWritable():
# pretty=true for human-editable files; default is compact
writeConfigJson("myapp", "mylib", "raw.json", %*{"x": 1})
writeConfigJson("myapp", "config.json", %*{"x": 1}) # dep-less form
let j = readConfigJson("myapp", "mylib", "raw.json") # auto-detects compression
# ── Binary (consumer owns serialization) ─────────────────────────────────────
if isWritable():
writeConfigBytes("myapp", "mylib", "data.bin", myBytes, compress = true)
let b = readConfigBytes("myapp", "mylib", "data.bin") # auto-detects compressionAll read procs auto-detect compression via a magic byte — \x00 = raw, \x01 = Snappy
— so the reader never needs to know how a file was written. Mixed compressed/uncompressed
files in the same directory work correctly.
# Pure resolvers (paths.nim) — compute a path, never touch disk.
# Safe on every target including read-only consoles and WASM:
let dir = configDir("myapp", "mylib") # ~/.config/myorg/myapp/mylib/
let dir2 = configDir("myapp") # ~/.config/myorg/myapp/ (dep-less)
let path = configFile("myapp", "mylib", "settings.json")
let path2 = configFile("myapp", "settings.json") # dep-less, positional
# Creating helpers (fs.nim / store.nim) — create the dir where writable, no-op where not.
# Never raise on a read-only target:
let made = ensureConfigDir("myapp", "mylib")
let made2 = ensureConfigDir("myapp")
let file = ensureConfigFile("myapp", "mylib", "settings.json")
let file2 = ensureConfigFile("myapp", "settings.json")| Platform | Resolve path | Create dir | Read | Write | Compress | Notes |
|---|---|---|---|---|---|---|
| Desktop | yes | yes | yes | yes | yes | Full support; XDG-compliant |
| Windows | yes | yes | yes | yes | yes | ~/.config/ root (XDG-literal, not APPDATA) |
| PS Vita | yes | yes | yes | yes | yes | ux0:data writable; read hw-verified, writes Vita3K-verified (v0.3.0) |
| Nintendo 3DS | yes | yes | yes | yes | yes | sdmc:/ writable; writes hardware-verified (v0.4.0) |
| PSP | yes | yes* | yes | yes* | yes* | *Write-capability gated; verify SDK |
| WebAssembly | yes | n/a | stub | stub | stub† | localStorage; v2 impl |
† v2 routes binary/compression through base64-encoded localStorage values.
Path resolution is always available — computing a path is pure string work. Writing is a
separate capability gated by isWritable().
ConfigError ← catch this for any configy failure
ConfigPathError ← bad app/dep/filename, or HOME/USERPROFILE unset with no usable XDG_CONFIG_HOME
ConfigIOError ← filesystem operation failed (wraps OSError/IOError)
ConfigParseError ← file existed but was malformed (bad magic byte, corrupt
Snappy payload, invalid JSON, or failed to(T) conversion)
ConfigUnsupportedError ← write on a read-only target (3DS/PSP v1, WASM v1)
Missing files on read return none(), not an exception.
- Minimal pure-Nim dependencies. One external dep:
supersnappy(pure Nim, compiles on all targets†). Stdlib:std/os,std/json,std/options. - Self-describing files. A magic byte prefix (
\x00raw /\x01Snappy) makes every file self-describing — reads always auto-detect, no compile-time flags needed. - Pure vs. creating procs are clearly named.
configDir/configFile(pure, never touch disk);ensureConfigDir/ensureConfigFile(create where writable). - No global mutable state. Every proc takes its inputs as parameters.
- Compile-time platform gating. All
when defined(...)checks centralized insrc/configy/capabilities.nim. Adding a new platform = one file change. - Compact by default.
writeConfigJsondefaults to compact JSON (pretty=false), consistent withstd/json. Passpretty=truefor human-editable files. - Exceptions, not
Result. A smallConfigErrorhierarchy wrapsstd/osandstd/json. Genuine queries (isWritable, file-exists, reads) returnboolorOption[T]. - ARC-compatible. Plain value types; no cycles, no custom GC hooks.
† supersnappy compilation on devkitARM (3DS), PSPDEV, and VitaSDK should be verified against each toolchain before shipping to those targets.
v0.2.0 changes path roots for all platforms. Existing config files stored under the old scheme must be moved manually — configy does not auto-migrate. Old and new roots:
| Platform | Old root | New root |
|---|---|---|
| Linux/macOS | ~/.<vendor>/config/ |
~/.config/<vendor>/ |
| Windows | %APPDATA%\<vendor>\config\ |
~\.config\<vendor>\ |
| 3DS | sdmc:/<vendor>/config/ |
sdmc:/config/<vendor>/ |
| PSP | ms0:/PSP/<vendor>/config/ |
ms0:/PSP/config/<vendor>/ |
| Vita | ux0:data/<vendor>/config/ |
ux0:data/config/<vendor>/ |
| WASM | <vendor>/config/ |
config/<vendor>/ |
Add to your *.nimble file:
requires "configy >= 0.1.0"
Or install directly:
nimble install configynimble testThe test suite covers path resolution, all magic-byte error cases, JSON/binary/typed
round-trips with and without Snappy compression, and the full ConfigError hierarchy.
CI runs desktop tests on Linux, macOS, and Windows. It also runs platform-define
compile checks for 3DS, PSP, Vita, and WebAssembly — these verify that the
when defined(...) branches in the library compile cleanly on the host OS, but are
not full cross-compiles (actual cross-compilation requires devkitARM, PSPDEV,
VitaSDK, or Emscripten, which are not available in standard CI).