21.1 preferences — User Preferences Management

**Store and retrieve application preferences using the user's home directory.**

NanoLang Mascot

The preferences module provides a simple, file-backed persistence layer for application settings. It is designed around one concrete use case: saving and loading ordered lists of strings (such as recently opened files, playlist entries, or configuration items) to a file in the user's home directory. The module also provides helpers to locate the right place to store preferences on the current platform.

Quick Start


from "modules/preferences/preferences.nano" import nl_prefs_get_path,
                                                    nl_prefs_save_playlist,
                                                    nl_prefs_load_playlist

fn save_recent_files(files: array<string>) -> bool {
    let path: string = (nl_prefs_get_path "myapp")
    let count: int = (array_length files)
    let result: int = (nl_prefs_save_playlist path files count)
    return (== result 1)
}

shadow save_recent_files {
    let files: array<string> = ["/home/user/file1.txt", "/home/user/file2.txt"]
    # Use a temp path to avoid polluting real preferences in tests
    let result: bool = (save_recent_files files)
    assert result
}

fn load_recent_files() -> array<string> {
    let path: string = (nl_prefs_get_path "myapp")
    return (nl_prefs_load_playlist path)
}

shadow load_recent_files {
    let items: array<string> = (load_recent_files)
    # May be empty on a fresh system; just check it doesn't crash
    assert (>= (array_length items) 0)
}

Finding the Preferences File Path

nl_prefs_get_home() — User's home directory


nl_prefs_get_home() -> string

Returns the user's home directory (e.g. /Users/alice on macOS, /home/alice on Linux). Uses the HOME environment variable internally.


from "modules/preferences/preferences.nano" import nl_prefs_get_home

fn show_home() -> void {
    let home: string = (nl_prefs_get_home)
    (println (+ "Home: " home))
}

shadow show_home {
    let home: string = (nl_prefs_get_home)
    assert (> (str_length home) 0)
}

nl_prefs_get_path(app_name) — Standard preferences file path


nl_prefs_get_path(app_name: string) -> string

Returns the conventional path for your application's preferences file: ~/.{app_name}_prefs. For an app named "nanoamp" this would return something like /Users/alice/.nanoamp_prefs.


from "modules/preferences/preferences.nano" import nl_prefs_get_path

fn get_prefs_path() -> string {
    return (nl_prefs_get_path "myapp")
}

shadow get_prefs_path {
    let path: string = (get_prefs_path)
    assert (> (str_length path) 0)
    # Path should contain the app name
    assert (str_contains path "myapp")
}

This is the recommended way to locate preferences — do not hard-code a path, and do not use a relative path. Platform conventions and multi-user systems expect preferences to live in the user's home directory.

Saving Preferences

nl_prefs_save_playlist(filename, items, count) — Write a list to disk


nl_prefs_save_playlist(filename: string, items: array<string>, count: int) -> int

Saves an ordered list of strings to filename. Each string is written on its own line. Returns 1 on success, 0 on failure (e.g. permission error, disk full).

The count parameter must match array_length items. This is an explicit parameter rather than being derived automatically, for compatibility with the underlying C layer.


from "modules/preferences/preferences.nano" import nl_prefs_get_path, nl_prefs_save_playlist

fn save_playlist(tracks: array<string>) -> bool {
    let path: string = (nl_prefs_get_path "musicplayer")
    let n: int = (array_length tracks)
    let ok: int = (nl_prefs_save_playlist path tracks n)
    return (== ok 1)
}

shadow save_playlist {
    let tracks: array<string> = [
        "Beethoven - Symphony 5.flac",
        "Bach - Goldberg Variations.flac",
        "Mozart - Requiem.flac"
    ]
    let saved: bool = (save_playlist tracks)
    assert saved
}

**Saving an empty list:**


fn clear_playlist() -> bool {
    let path: string = (nl_prefs_get_path "musicplayer")
    let empty: array<string> = []
    let ok: int = (nl_prefs_save_playlist path empty 0)
    return (== ok 1)
}

shadow clear_playlist {
    assert (clear_playlist)
}

Loading Preferences

nl_prefs_load_playlist(filename) — Read a list from disk


nl_prefs_load_playlist(filename: string) -> array<string>

Reads a previously saved list from filename. Returns an array<string> with one element per line. Returns an empty array if the file does not exist or cannot be read — there is no error thrown.


from "modules/preferences/preferences.nano" import nl_prefs_get_path, nl_prefs_load_playlist

fn load_playlist() -> array<string> {
    let path: string = (nl_prefs_get_path "musicplayer")
    return (nl_prefs_load_playlist path)
}

shadow load_playlist {
    let tracks: array<string> = (load_playlist)
    # On a fresh system this may be empty; either is valid
    assert (>= (array_length tracks) 0)
}

**Checking if preferences exist:**


from "modules/preferences/preferences.nano" import nl_prefs_get_path, nl_prefs_load_playlist

fn has_saved_playlist() -> bool {
    let path: string = (nl_prefs_get_path "musicplayer")
    let tracks: array<string> = (nl_prefs_load_playlist path)
    return (> (array_length tracks) 0)
}

shadow has_saved_playlist {
    let result: bool = (has_saved_playlist)
    # Result depends on machine state — just verify it returns a bool
    assert (or result (not result))
}

Round-Trip Pattern

The most reliable way to use preferences is to always check that what you saved can be loaded back:


from "modules/preferences/preferences.nano" import nl_prefs_get_path,
                                                    nl_prefs_save_playlist,
                                                    nl_prefs_load_playlist

fn save_and_verify(app: string, items: array<string>) -> bool {
    let path: string = (nl_prefs_get_path app)
    let count: int = (array_length items)
    let ok: int = (nl_prefs_save_playlist path items count)
    if (== ok 0) {
        return false
    } else {
        let loaded: array<string> = (nl_prefs_load_playlist path)
        return (== (array_length loaded) count)
    }
}

shadow save_and_verify {
    let items: array<string> = ["alpha", "beta", "gamma"]
    assert (save_and_verify "test_roundtrip" items)

    let empty: array<string> = []
    assert (save_and_verify "test_roundtrip_empty" empty)
}

Complete Example: Music Player Preferences

This example shows a realistic preferences workflow for a music player application:


from "modules/preferences/preferences.nano" import nl_prefs_get_path,
                                                    nl_prefs_get_home,
                                                    nl_prefs_save_playlist,
                                                    nl_prefs_load_playlist

# --- Preference paths for this app ---

fn playlist_path() -> string {
    return (nl_prefs_get_path "nanoamp_playlist")
}

shadow playlist_path {
    let p: string = (playlist_path)
    assert (> (str_length p) 0)
}

fn settings_path() -> string {
    return (nl_prefs_get_path "nanoamp_settings")
}

shadow settings_path {
    let p: string = (settings_path)
    assert (> (str_length p) 0)
}

# --- Playlist management ---

fn save_current_playlist(tracks: array<string>) -> bool {
    let n: int = (array_length tracks)
    let ok: int = (nl_prefs_save_playlist (playlist_path) tracks n)
    return (== ok 1)
}

shadow save_current_playlist {
    let tracks: array<string> = ["track1.flac", "track2.flac"]
    assert (save_current_playlist tracks)
}

fn load_saved_playlist() -> array<string> {
    return (nl_prefs_load_playlist (playlist_path))
}

shadow load_saved_playlist {
    # Save something first so we have known content
    let tracks: array<string> = ["a.flac", "b.flac", "c.flac"]
    (save_current_playlist tracks)
    let loaded: array<string> = (load_saved_playlist)
    assert (== (array_length loaded) 3)
    assert (== (array_get loaded 0) "a.flac")
}

fn playlist_track_count() -> int {
    let tracks: array<string> = (load_saved_playlist)
    return (array_length tracks)
}

shadow playlist_track_count {
    let tracks: array<string> = ["x.mp3"]
    (save_current_playlist tracks)
    assert (== (playlist_track_count) 1)
}

# --- Settings management ---
# Settings are stored as "key=value" lines using the playlist mechanism

fn make_setting(key: string, value: string) -> string {
    return (+ key (+ "=" value))
}

shadow make_setting {
    assert (== (make_setting "theme" "dark") "theme=dark")
    assert (== (make_setting "volume" "80") "volume=80")
}

fn save_settings(theme: string, volume: int, shuffle: bool) -> bool {
    let vol_str: string = (int_to_string volume)
    let shuf_str: string = (cond (shuffle "true") (else "false"))
    let entries: array<string> = [
        (make_setting "theme" theme),
        (make_setting "volume" vol_str),
        (make_setting "shuffle" shuf_str)
    ]
    let ok: int = (nl_prefs_save_playlist (settings_path) entries 3)
    return (== ok 1)
}

shadow save_settings {
    assert (save_settings "dark" 75 true)
    assert (save_settings "light" 50 false)
}

fn load_setting_value(entries: array<string>, key: string) -> string {
    let n: int = (array_length entries)
    let search: string = (+ key "=")
    let search_len: int = (str_length search)
    let mut result: string = ""
    let mut i: int = 0
    while (< i n) {
        let entry: string = (array_get entries i)
        let entry_len: int = (str_length entry)
        if (and (>= entry_len search_len)
                (== (str_substring entry 0 search_len) search)) {
            let value_len: int = (- entry_len search_len)
            if (> value_len 0) {
                set result (str_substring entry search_len value_len)
            } else {
                (print "")
            }
        } else {
            (print "")
        }
        set i (+ i 1)
    }
    return result
}

shadow load_setting_value {
    let entries: array<string> = ["theme=dark", "volume=75", "shuffle=true"]
    assert (== (load_setting_value entries "theme") "dark")
    assert (== (load_setting_value entries "volume") "75")
    assert (== (load_setting_value entries "shuffle") "true")
    assert (== (load_setting_value entries "missing") "")
}

fn get_saved_theme() -> string {
    let entries: array<string> = (nl_prefs_load_playlist (settings_path))
    let theme: string = (load_setting_value entries "theme")
    if (== theme "") {
        return "dark"   # default
    } else {
        return theme
    }
}

shadow get_saved_theme {
    (save_settings "solarized" 60 false)
    assert (== (get_saved_theme) "solarized")
}

fn main() -> int {
    # Save initial settings
    (save_settings "dark" 80 true)

    # Save a playlist
    let playlist: array<string> = [
        "recordings/concert.flac",
        "recordings/symphony.flac",
        "recordings/sonata.flac"
    ]
    (save_current_playlist playlist)

    # Report what was saved
    (println (+ "Saved " (int_to_string (playlist_track_count)) " tracks"))
    (println (+ "Theme: " (get_saved_theme)))
    (println (+ "Prefs stored in: " (nl_prefs_get_home)))

    return 0
}

shadow main { assert true }

API Reference

FunctionSignatureDescription
nl_prefs_get_home() -> stringUser home directory
nl_prefs_get_path(app_name: string) -> string~/.{app_name}_prefs path
nl_prefs_save_playlist(filename: string, items: array<string>, count: int) -> intWrite list to file, returns 1 on success
nl_prefs_load_playlist(filename: string) -> array<string>Read list from file, returns empty array on failure

Notes and Limitations

  • **Format:** The file format is plain text, one item per line. There is no escaping for newlines within items — items should not contain literal newline characters.
  • **Atomic writes:** The current implementation writes directly to the target file. On systems where the process is killed mid-write, the file may be left in a partial state. For critical data, save to a temp file first and rename.
  • **No locking:** Multiple processes writing to the same file simultaneously may corrupt it. Use application-level coordination if concurrency is a concern.
  • **Encoding:** Files are written in the system's native encoding. Keep preference values to ASCII for maximum portability.

---

**Previous:** Chapter 21 Overview

**Next:** Part 4: Advanced Topics