Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

btv.fs

btv.fs.stat(path)

btv.fs.stat(path) -> promise of { type, size, mtime, atime, mode } (follows links)

Defined in fs.lua.

btv.fs.lstat(path)

btv.fs.lstat(path) -> like stat but does NOT follow a symlink (type may be "link").

Defined in fs.lua.

btv.fs.exists(path)

btv.fs.exists(path) -> promise of a boolean. The one op that never rejects: a missing path (or any error) resolves false. Existence of the entry itself, so a dangling symlink is true. (The exists job resolves a bool rather than rejecting, so no reject-to-false mapping is needed in the wrapper.)

Defined in fs.lua.

btv.fs.readdir(path)

btv.fs.readdir(path) -> promise of { { name=, type="file"|"directory"|"link" }, … }, the entries directly under path (no "."/".."), each with its dirent kind in the SAME call (no per-entry stat). type is lstat-flavoured (a symlink reports "link").

Defined in fs.lua.

btv.fs.gitignore(text)

btv.fs.gitignore(text) -> a matcher for one ignore file’s contents (a .gitignore, .ignore, .rgignore or .git/info/exclude), plus the list of messages for lines it had to skip (an invalid glob — git and rg skip those too). The semantics are ripgrep’s own (the ignore crate), so negation, anchoring, directory-only patterns and ** decide exactly as rg does.

matcher:match(path, is_dir) -> "ignore", "whitelist" (a !negated line) or nil (no line matches). path is relative to the ignore file’s own directory and /-separated; is_dir matters for a build/-style pattern. The last matching line decides, as in git.

local m = btv.fs.gitignore("*.log\n!keep.log\nbuild/\n")
m:match("debug.log")       --> "ignore"
m:match("keep.log")        --> "whitelist"
m:match("build", true)     --> "ignore"
m:match("src/main.rs")     --> nil

Defined in fs.lua.

btv.fs.walk(dir, opts)

btv.fs.walk(dir[, opts]) -> promise of a LIST of file paths relative to dir, a recursive directory listing built from readdir. The transport-agnostic file enumeration the codebase reaches for when rg/fd aren’t available (the pure web client, where a spawn has no real shell) — it rides the same off-tick fs seam as every other btv.fs op, so it works against local disk, a daemon, and OPFS alike. The relative paths are spelled with the session’s separator (btv.utils.path_join), as rg / fd print them — so a file reads the same whichever enumerator found it. opts:

max        cap on files returned (default 50000) — a runaway guard on huge trees
hidden     include dotfiles / dotdirs (default false)
skip       set of directory basenames to prune (default { [".git"] = true })
gitignore  honour ignore files the way `rg` does (default false)

With gitignore = true the walk reads each directory’s .gitignore, .ignore and .rgignore as it descends (and those of the directories above dir), plus the repository’s info/exclude at a repository root, and prunes what they ignore — negation, anchoring and precedence as in rg, through btv.fs.gitignore. As in rg, a .gitignore counts only inside a git repository (a directory with a .git at or above it) and not above that repository’s root, while .ignore / .rgignore count anywhere. A nested repository (a submodule) starts afresh: the enclosing repository’s git rules stop at its root. The global git excludes file (core.excludesFile) is not read. Directories with no ignore file cost nothing extra.

An unreadable subdirectory is skipped (not fatal); an ignore file that exists but cannot be read, or is not a regular file, is skipped with a warning naming it (once a session), as rg warns and walks on. MUST be awaited inside btv.async.

Defined in fs.lua.

btv.fs.grep(dir, query, opts)

btv.fs.grep(dir, query[, opts]) -> promise of a LIST of matches, each { path = <rel>, row = <1-based lnum>, col = <1-based>, text = <line> }: a recursive, transport-agnostic plain-substring search. The fallback the grep picker reaches for when rg/grep aren’t available (the pure web client) — it rides the same off-tick fs seam as every other btv.fs op, so it works against local disk, a daemon, and OPFS. Walks dir (btv.fs.walk), reads each file (btv.fs.read_text), and matches query as a LITERAL substring per line. Binary / unreadable files (read_text rejects) are skipped. opts pass through to btv.fs.walk (max / hidden / skip / gitignore). MUST be awaited inside btv.async.

Defined in fs.lua.

btv.fs.read(path)

btv.fs.read(path) -> promise of the file’s RAW bytes (a Lua byte-string).

Defined in fs.lua.

btv.fs.read_text(path, opts)

btv.fs.read_text(path[, { encoding = "utf-8" }]) -> promise of decoded text. Decodes through the encoding seam and REJECTS (EILSEQ) on invalid input — never lossy replacement text. Use btv.fs.read for raw bytes.

Defined in fs.lua.

btv.fs.write(path, data)

btv.fs.write(path, data) -> promise (resolves nil). Truncates / creates.

Defined in fs.lua.

btv.fs.append(path, data)

btv.fs.append(path, data) -> promise (resolves nil). Creates if absent.

Defined in fs.lua.

btv.fs.mkdir(path, opts)

btv.fs.mkdir(path[, { recursive = false, mode = 0o755 }]) -> promise (resolves nil). mode is the Unix permission bits applied to every directory created (defaults to 0o755 in Rust when omitted; ignored off Unix) — pass it to keep a private data/state dir from being created world-readable.

Defined in fs.lua.

btv.fs.rename(from, to)

btv.fs.rename(from, to) -> promise (resolves nil).

Defined in fs.lua.

btv.fs.remove(path, opts)

btv.fs.remove(path[, { recursive = false }]) -> promise (resolves nil). A file is unlinked; a directory needs recursive unless already empty.

Defined in fs.lua.

btv.fs.copy(src, dst, opts)

btv.fs.copy(src, dst[, { recursive = false }]) -> promise (resolves nil). A file copies (overwriting); a directory needs recursive.

Defined in fs.lua.

btv.fs.realpath(path)

btv.fs.realpath(path) -> promise of the canonical absolute path (symlinks resolved).

Defined in fs.lua.

btv.fs.which(name)

btv.fs.which(name) -> promise of the absolute path of the executable name, or nil when nothing matches. A bare name is searched across $PATH; a name that already contains a / is taken as an explicit path and accepted only when it is an executable file. The async, transport-agnostic replacement for vim’s blocking executable() / exepath() — a build/tool lookup is I/O, so it rides the same off-tick btv.fs seam as every other op and works unchanged against local disk, a daemon, and a browser session.

It resolves nil rather than rejecting when the program is absent: “not installed” is a true answer, and only a transport failure is an error. That makes the two common shapes read naturally —

-- prefer a project-local binary, fall back to the one on $PATH
local local_bin = btv.utils.joinpath(root, "node_modules/.bin", "eslint")
local cmd = btv.await(btv.fs.which(local_bin)) or "eslint"

-- gate a feature on a tool being present
btv.fs.which("rg"):next(function(path)
  if path then use_ripgrep(path) end
end)

In a serverless browser session there are no executables at all, so every lookup resolves nil; with a daemon attached the daemon’s own $PATH answers — which is the right one, since that is where the language servers and tools actually run.

Defined in fs.lua.

watch:next()

:next() -> promise of the next change batch, nil at end (after :stop()), or a rejection carrying the watch error. SEQUENTIAL like the process Stream: one outstanding :next() at a time (what a for loop does); batches arriving between pulls buffer in _queue.

Defined in fs.lua.

watch:stop()

:stop() cancels the native watch and ends iteration (a parked :next resolves nil).

Defined in fs.lua.

btv.fs.watch(path, opts)

btv.fs.watch(path[, { recursive = false }]) -> a Watch you iterate with btv.await_each (the streaming sibling of the one-shot ops, same shape as btv.run_stream). Each step yields a COALESCED change batch:

{ kind = “create”|“modify”|“remove”|“rename”, paths = { <abspath>, … } }

A consumer that stops pulling while changes keep arriving buffers at most 1024 batches; the next change after that is replaced by one { kind = "overflow", paths = {} } batch (later ones are dropped until the consumer catches up), which says “changes were missed, rescan”.

bursts within a 10 ms window are merged in the server, one batch per kind (paths deduped within each). A burst that mixes kinds therefore yields one batch each rather than a single coarsened one: writing a new file is a "create" plus a "modify", and folding them together would lose the only signal that says the file is new. Want a longer settle? Wrap btv.utils.debounce on top.

btv.async(function() local w = btv.fs.watch(dir, { recursive = true }) for ev in btv.await_each(w) do redraw(ev.paths) end – until w:stop() end)()

The watch is armed where the session’s files are: locally in a local session, and on the daemon in a daemon session (native or browser) — so a remote project reports its own changes rather than this machine’s. A native daemon link re-arms its watches after a reconnect, so a live iterator survives an outage.

A watch that can’t arm (bad path, watch limit) or a session with no change source at all (serverless browser) REJECTS the first pull — fail loud, never a dead watch. :stop() cancels the watch and ends the iteration.

Defined in fs.lua.