os_paths 1.0.0
os_paths: ^1.0.0 copied to clipboard
OS-specific standard directories (XDG on Linux/BSD, ~/Library on macOS, %APPDATA% on Windows) for Flutter and Dart apps.
os_paths #
OS-specific standard directories for Flutter and Dart — XDG on Linux/BSD,
~/Library on macOS, %APPDATA% on Windows.
Pure Dart: no plugin registration, no platform channels, no dependencies. The
same code works in a Flutter desktop app, a Flutter test, and a dart:io
command-line program.
Usage #
import 'package:os_paths/os_paths.dart';
final paths = OsPaths.instance;
paths.configHome; // ~/.config | ~/Library/Application Support | %APPDATA%
paths.dataHome; // ~/.local/share | ~/Library/Application Support | %APPDATA%
paths.cacheHome; // ~/.cache | ~/Library/Caches | %LOCALAPPDATA%
paths.stateHome; // ~/.local/state | ~/Library/Application Support | %LOCALAPPDATA%
paths.runtimeDir; // $XDG_RUNTIME_DIR | null | null
paths.documentsDir; // ~/Documents (localized via xdg-user-dirs)
Most applications want directories scoped to themselves:
import 'dart:io';
final paths = OsPaths.instance;
final app = paths.app('Fern Notes', organization: 'Lexmata');
final configDir = await app.ensureConfig(); // created if missing
final settings = File(paths.join(configDir.path, 'settings.json'));
await settings.writeAsString('{"theme":"dark"}');
| Linux | macOS | Windows | |
|---|---|---|---|
app.data |
~/.local/share/fern-notes |
~/Library/Application Support/Lexmata/Fern Notes |
%APPDATA%\Lexmata\Fern Notes |
app.config |
~/.config/fern-notes |
~/Library/Application Support/Lexmata/Fern Notes |
%APPDATA%\Lexmata\Fern Notes |
app.cache |
~/.cache/fern-notes |
~/Library/Caches/Lexmata/Fern Notes |
%LOCALAPPDATA%\Lexmata\Fern Notes\Cache |
app.logs |
~/.local/state/logs/fern-notes |
~/Library/Logs/Lexmata/Fern Notes |
%LOCALAPPDATA%\Lexmata\Fern Notes\Logs |
organization is ignored on the XDG platforms (Linux, the BSDs, Fuchsia),
where the convention is a single lowercase directory named after the
application. Windows has no cache or log
Known Folder, so those two get a subdirectory below the application's own
directory there — otherwise they would collide with app.state.
Application names are sanitized: illegal characters are stripped, Windows
reserved device names (CON, NUL, ...) are escaped, and a name can never
contribute more than one path segment or escape its base directory.
What is resolved #
| Getter | Linux / BSD | macOS | Windows |
|---|---|---|---|
home |
$HOME |
$HOME, else /Users/$USER (or /var/root) |
%USERPROFILE%, else %HOMEDRIVE%%HOMEPATH% |
dataHome |
$XDG_DATA_HOME, else ~/.local/share |
~/Library/Application Support |
%APPDATA% |
localDataHome |
same as dataHome |
same as dataHome |
%LOCALAPPDATA% |
configHome |
$XDG_CONFIG_HOME, else ~/.config |
~/Library/Application Support |
%APPDATA% |
cacheHome |
$XDG_CACHE_HOME, else ~/.cache |
~/Library/Caches |
%LOCALAPPDATA% |
stateHome |
$XDG_STATE_HOME, else ~/.local/state |
~/Library/Application Support |
%LOCALAPPDATA% |
binHome |
$XDG_BIN_HOME (non-standard extension), else ~/.local/bin |
~/Applications (application bundles, not on PATH) |
%LOCALAPPDATA%\Programs |
preferencesDir |
= configHome |
~/Library/Preferences |
= configHome |
logHome |
~/.local/state/logs |
~/Library/Logs |
%LOCALAPPDATA% |
runtimeDir |
$XDG_RUNTIME_DIR or null |
null |
null |
tempDir |
$TMPDIR, else /tmp |
$TMPDIR, else /tmp |
%TEMP%, else %TMP%, else %LOCALAPPDATA%\Temp |
dataDirs |
$XDG_DATA_DIRS |
/Library/Application Support |
%ProgramData% |
configDirs |
$XDG_CONFIG_DIRS |
/Library/Preferences |
%ProgramData% |
fontsDir |
~/.local/share/fonts |
~/Library/Fonts |
%LOCALAPPDATA%\Microsoft\Windows\Fonts |
User media — desktopDir, documentsDir, downloadsDir, musicDir,
picturesDir, videosDir, templatesDir, publicShareDir — hangs off the
home directory on macOS, and on Windows for everything except templatesDir
(%APPDATA%\Microsoft\Windows\Templates) and publicShareDir (%PUBLIC%).
On Linux the exported XDG_*_DIR variables win, then
$XDG_CONFIG_HOME/user-dirs.dirs (so localized names like ~/Bureau resolve
correctly), then ~/Desktop and friends.
runtimeDir is null wherever the platform has no such concept; fall back to
tempDir.
A value that is not an absolute path is ignored, as if the variable were unset
— the XDG specification requires this for XDG_*, and the same rule is applied
to every platform's directory variables, since a relative value would resolve
against the process's working directory instead of the user's profile. On
Windows "absolute" means a drive root (C:\) or a UNC prefix (\\server);
a drive-relative C:Users\ada is rejected. Every returned path has its
trailing separator stripped, so results compare and compose predictably.
These directories are not always distinct #
Not every platform separates these concepts:
| Collapses to one directory | |
|---|---|
| Windows | configHome = dataHome; cacheHome = stateHome = logHome = localDataHome |
| macOS | dataHome = configHome = stateHome |
Never assume two of them differ — in particular, never recursively delete one
to "reset" a single category. AppPaths does keep an application's cache and
logs in their own subdirectories on every platform.
Android, iOS, and unknown platforms #
These platforms hand out sandboxed paths at runtime, so nothing usable can be
derived from the environment. Getters that resolve a single directory throw
OsPathsException there; dataDirs/configDirs are empty and runtimeDir is
null, because a sandbox has no machine-wide directories. Wire the real
directories once at startup, and the rest of the API works unchanged:
import 'package:path_provider/path_provider.dart';
Future<void> initPaths() async {
// needsExplicitPaths, not isMobile: it also covers OsFamily.unknown, which
// would otherwise be left unconfigured.
if (!OsFamily.current.needsExplicitPaths) return;
OsPaths.instance = ExplicitPaths(
family: OsFamily.current,
home: (await getApplicationDocumentsDirectory()).path,
dataHome: (await getApplicationSupportDirectory()).path,
cacheHome: (await getTemporaryDirectory()).path,
documentsDir: (await getApplicationDocumentsDirectory()).path,
);
}
Isolates do not share
OsPaths.instance. Dart statics are per-isolate, so a resolver installed on the main isolate is invisible insideIsolate.spawnor Flutter'scompute()— a background isolate rebuilds the host resolver and every getter throws. Install one at the top of the isolate's entry point, or pass the directories you need across the boundary.
Testing #
Every platform's behaviour can be exercised from any host by supplying a family and an environment:
final paths = OsPaths(
family: OsFamily.windows,
environment: {'USERPROFILE': r'C:\Users\ada', 'APPDATA': r'C:\Users\ada\AppData\Roaming'},
);
expect(paths.app('Fern Notes').config, r'C:\Users\ada\AppData\Roaming\Fern Notes');
Reading user-dirs.dirs goes through an injectable FileReader, so tests never
touch the disk. Set OsPaths.instance to swap the resolver globally, and use
addTearDown(OsPaths.resetInstance) to restore the default.
Caveats #
- Windows Known Folders that the user has relocated (Documents redirected into
OneDrive, for example) are not detected — that needs
SHGetKnownFolderPath, which would require a plugin. Override the affected directory explicitly:ExplicitPaths(documentsDir: ..., ...). - No directory is created unless you call one of the
ensure*methods. Those throwFileSystemException(adart:ioerror, notOsPathsException) when creation fails, and they follow symbolic links — resolve the result and compare it against the expected base if that is part of your threat model. tempDirfalls back to/tmpwhen$TMPDIRis unset./tmpis shared and world-writable, andAppPaths.tempunder it is a predictable name; useDirectory.systemTemp.createTemp()when you need a private directory.- The web is not supported: this package imports
dart:io, which does not compile for web targets. parseUserDirsfollows the reference implementation and silently ignores entries it cannot resolve (relative values,~,${HOME}, unterminated quotes), falling back to the English defaults.user-dirs.dirsis read once per resolver and cached, including a miss. Construct a newOsPathsto pick up later changes.
Conformance #
- Relative values in environment variables are ignored, as the XDG specification requires, rather than returned as-is.
- Windows
cacheHome/logHomeare%LOCALAPPDATA%— there is no cache or log Known Folder — andAppPathsnestsCacheandLogsbelow the application directory so they stay distinct fromstate. - Application names are sanitized so they cannot escape their base directory, contribute more than one path segment, or collide with a Windows reserved device name.
user-dirs.dirsparsing follows thexdg-user-dirsreference implementation, including its backslash escapes and its silent skipping of entries that cannot be resolved.
License #
MIT — see LICENSE.
Copyright (c) 2026 Joseph R. Quinn.