Packages

save

Keep small named pieces of text, such as settings and scores, between runs of a program.

import "std:save"

std:save gives a program a folder of its own where it can keep named items between runs: settings, a high score, a token, the last thing the user opened. Each item is a file, written safely so a crash can't leave half of one. Use it for small per-user data. For anything you need to search or update in parts, use db, and for files you choose the location of, use fs.

Items are text. To store a number, a list or a record, turn it into text with json on the way in and back on the way out.

Open a store and keep an item#

save.open(appName) returns the store for an app, creating its folder under ~/.osl/apps when needed. set stores text under a name and get reads it back:

main.osl
import "std:save"

auto store = save.open("docs-save-basics").unwrap()

// Start clean, because this example runs more than once.
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

store.set("name", "Ada").unwrap()
log store.get("name").unwrap()

store.set("name", "Grace").unwrap()
log store.get("name").unwrap()

log store.dir().endsWith("docs-save-basics")
Click the code to edit it

Run it twice with the cleanup loop deleted: the old value is still there on the second run, because it lives on disk.

Setting a name again replaces the old value. The write goes to a temporary file first and is renamed into place, so a reader sees the old text or the new text and never a mixture.

Pick an app name that is yours alone. Two programs that open the same name share a store.

Names and files#

A name without an extension is stored as name.txt, so "score" and "score.txt" are the same item. Give a name an extension when the content has one, such as settings.json. keys() lists what is stored, sorted, and path(name) tells you which file holds an item, so other tools can open it:

main.osl
import "std:save"
import "std:fs"

auto store = save.open("docs-save-keys").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

store.set("note", "buy milk").unwrap()
store.set("theme.json", `{"dark": true}`).unwrap()

log store.keys().unwrap()
log store.has("note") store.has("missing")
log fs.readFile(store.path("note")).unwrap()
Click the code to edit it

Missing items#

get and remove fail with a NotFoundError for an item that was never stored. That is a normal first-run situation, so give it a default with unwrapOr, or branch on has:

main.osl
import "std:save"

auto store = save.open("docs-save-results").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

string name = store.get("name").unwrapOr("stranger")
log "hello," name

log store.get("name").unwrapErr().kind

store.set("name", "Ada").unwrap()
store.remove("name").unwrap()
log store.has("name")
log store.remove("name").isErr()
Click the code to edit it

Saving structured data#

Combine the store with std:json and a struct to keep a settings record. Parsing checks the stored text against the struct, so a hand-edited file with a wrong type is an error you can handle:

main.osl
import "std:save"
import "std:json"

struct Settings (
  string theme = "light"
  int fontSize = 14
)

auto store = save.open("docs-save-json").unwrap()
for key in store.keys().unwrap() (
  store.remove(key).unwrap()
)

def load(*save.Store store) Settings (
  string text = store.get("settings.json").unwrapOr("{}")
  return json.parse<Settings>(text).unwrapOr(Settings())
)

Settings s = load(store)
log s.theme s.fontSize

s.theme = "dark"
s.fontSize = 16
store.set("settings.json", json.stringify(s)).unwrap()

Settings again = load(store)
log again.theme again.fontSize
log store.get("settings.json").unwrap()
Click the code to edit it

The loader falls back to a default Settings() in two cases: nothing is stored yet, or what is stored can't be parsed. A program that must not lose bad data should keep a copy before overwriting it.

Quick check

You call store.get("score") on a first run, before anything was saved. What do you get?

Your turn

Visit counter

Count visits across runs. visit() reads visits (0 when missing), adds one, saves it and returns the new count. Call it three times.

main.osl
Ctrl Enter runs
Expected output
1 2 3

API reference#

20 members, generated from the std:save source of OSL 9.3.0.

Functions

#save.all() array

Deprecated. Deprecated: use save.open(appName) and store.keys() instead.

all lists the shared store's file names.

#save.create() *save.Save

Deprecated. Deprecated: use save.open(appName) instead.

create returns a new, uninitialised store.

#save.exists(string filename) boolean

Deprecated. Deprecated: use save.open(appName) and store.has(name) instead.

exists reports whether the shared store has an item.

#save.getItem(string filename) object

Deprecated. Deprecated: use save.open(appName) and store.get(name) instead.

getItem returns {app, filename, exists, data, size} from the shared store.

#save.init(string appName) boolean

Deprecated. Deprecated: use save.open(appName) instead.

init points the package's shared store at an app and reports whether it worked.

#save.open(string appName) result<*save.Store, error>

open returns the store for an app, creating its directory ~/.osl/apps/<appName> if needed. Throws ValueError when appName isn't a plain file name; fails with NotFoundError when the home directory isn't known, PermissionError or IOError.

#save.setItem(string filename, any value) string

Deprecated. Deprecated: use save.open(appName) and store.set(name, text) instead.

setItem stores a value in the shared store and returns its path, or "".

*save.Store

#store.dir() string

dir returns the store's directory.

#store.get(string name) result<string, error>

get returns the text stored under name. Fails with NotFoundError when there is no such item. Throws ValueError for an invalid name.

#store.has(string name) boolean

has reports whether an item is stored under name. Invalid names have no item.

#store.keys() result<string[], error>

keys lists the stored items' file names (such as "score.txt"), sorted. Fails with NotFoundError when the store's directory has been deleted.

#store.path(string name) string

path returns the file an item is stored in. A name without an extension is stored as name.txt, so "score" and "score.txt" are the same item. Throws ValueError for a name with slashes or a trailing ".".

#store.remove(string name) result<unit, error>

remove deletes an item. Fails with NotFoundError when there is no such item. Throws ValueError for an invalid name.

#store.set(string name, string text) result<unit, error>

set stores text under name, replacing any earlier value atomically. Throws ValueError for an invalid name; fails with PermissionError or IOError.

*save.Save

#save.all() array

Deprecated. Deprecated: use store.keys() instead.

all lists stored file names, or [] when they can't be read.

#save.create() *save.Save

Deprecated. Deprecated: use save.open(appName) instead.

create returns a new, uninitialised store.

#save.exists(string filename) boolean

Deprecated. Deprecated: use store.has(name) instead.

exists reports whether an item is stored.

#save.getItem(string filename) object

Deprecated. Deprecated: use store.get(name) and store.has(name) instead.

getItem returns {app, filename, exists, data, size} for an item.

#save.init(string appName) boolean

Deprecated. Deprecated: use save.open(appName) instead.

init points the store at an app's directory and reports whether it worked.

#save.setItem(string filename, any value) string

Deprecated. Deprecated: use store.set(name, text) instead.

setItem stores a value and returns its path, or "".