btv.plugins
btv.plugins.disabled()
btv.plugins.disabled() -> whether the plugin manager is off in this editor: an app
embedding it turned it off, so plugins are neither declared, loaded, installed nor
updated through it. Every other btv.plugins call then raises E5805; plugins under
the config’s pack/*/start/ still load.
Defined in plugins.lua.
btv.plugins.on_change(fn)
M.on_change(fn) — subscribe to manager state changes (task transitions, plugin loads). Returns an unsubscribe function. The manager UI re-renders on each call.
Defined in plugins.lua.
btv.plugins.load(name)
Load a plugin by name: dependencies first, then put it on the runtimepath, source
its plugin/ scripts, and run its config. Idempotent (a second call is a
no-op) and cycle-safe (a _loading guard breaks dependency loops). Returns a
promise resolving true when newly loaded, false if already loaded / in
flight; REJECTS (loud) if the plugin is not installed.
Defined in plugins.lua.
btv.plugins.on_loaded(name, fn)
btv.plugins.on_loaded(name, fn) — run fn once name is fully loaded (its
plugin/ scripts sourced and its config finished, an async config awaited).
fn is called with the plugin name. Returns an unsubscribe function.
This is the hook to reach for when your config must run after someone else’s
plugin — it is race-free in the direction the raw PluginLoaded event is not.
Registering btv.on("PluginLoaded", { pattern = name }, …) only ever hears a
load that happens later, so the same code silently does nothing when the
plugin turns out to be eager and already loaded (a plain ordering flip in your
config, or a spec someone else made non-lazy). on_loaded closes that: if the
plugin is already loaded it calls fn immediately, otherwise it waits for
the plugin’s PluginLoaded. Either way fn runs exactly once.
The plugin need not be declared yet — on_loaded before the btv.plugins{…}
that declares it is fine, and is the usual shape in an init.lua.
btv.plugins.on_loaded("bemtvi-lspconfig", function()
require("bemtvi-lspconfig").setup()
end)
For “every eager plugin is ready” (not one named plugin) use the PluginsLoaded
event instead: btv.on("PluginsLoaded", {}, fn).
Defined in plugins.lua.
btv.plugins.add(specs)
Register one or more plugin specs. Accepts a single spec (string or table) or a
list of them. Each is normalized and stored; a lazy spec’s triggers are armed
(and its init run) immediately, while an enabled eager spec that is already on
disk begins loading. Returns the name of the LAST spec added (so a single-spec
add — as dependency registration uses — yields that dependency’s name).
The FIRST declaration of a name arms it; re-declaring the same name (a config
re-sourced, or a shared dependency re-added through a second plugin) updates the
stored spec without re-arming — re-arming would double-register every trigger
(autocmd / command / keymap) and re-run init.
Defined in plugins.lua.
btv.plugins.setup_manager(opts)
btv.plugins.setup_manager{ root=, config=, system=, lockfile=, github= }: override the
install root (a test points it at a temp dir), the config dir, the system-plugin dir,
the lockfile path (default <config>/bemtvi-lock.json), or the shorthand URL template.
Merges onto the defaults. (Named setup_manager, not setup, so it reads distinctly from a
plugin’s own require("plugin").setup{} and never looks like the call that
declares plugins — that is btv.plugins{...} / btv.plugins.add.)
Defined in plugins.lua.
btv.plugins.locked()
btv.plugins.locked() — a synchronous snapshot of the last read/written lockfile.
Cheap (no disk); call _read_lock() first if the file may have changed underneath.
Defined in plugins.lua.
btv.plugins.lock(opts)
btv.plugins.lock() — record every managed, installed plugin’s current commit to the
lockfile. Resolves the written table. Writes only when the resolved set differs from
what is already on disk, so a no-op sync leaves the file (and the user’s git status)
untouched.
opts.plugins (a name, or a list of names — plus their dependencies) records just
those plugins, carrying every other entry over verbatim: :PluginLock alpha pins the
one plugin you have finished testing without also freezing everything else at
whatever it happens to sit on today.
opts.realized (internal) narrows the re-record the same way for the plugins a verb
just put in place — the difference being that a realized plugin’s spec tag is
ATTESTED as the pin that produced its commit, which a hand-typed :PluginLock cannot
claim. See resolve_lock.
Defined in plugins.lua.
btv.plugins.restore(opts)
btv.plugins.restore() — move every managed plugin to the commit the lockfile records:
the “undo the update that broke my editor” verb, and the reason recording commits is
worth anything. Resolves
{ restored = { name, … }, current = { name, … }, failed = { { name, message }, … } }
so a caller can tell the three outcomes apart. A plugin already at its locked commit is
current (never re-checked-out); a dev dir plugin, an uninstalled one, and one with no
lock entry are skipped silently (there is nothing recorded to restore them to).
A commit that cannot be reached even after unshallowing lands in failed and the
checkout is left exactly as it was — a partial rollback that CLAIMED success would be
worse than none, since the user would stop looking for the real problem. The promise
still resolves (so the other plugins’ outcomes survive) and :PluginRestore reports
the failures loud.
opts.plugins (a name, or a list of names — plus their dependencies) rolls back only
those plugins: the update that broke your editor is usually one plugin, and there is
no reason to un-do the twelve that are working.
Defined in plugins.lua.
btv.plugins.install(opts)
Install every declared, enabled plugin that is missing. Returns a promise of the count installed; activates any newly-present eager plugins as it finishes.
A failure aborts the run LOUD (a half-installed tree must never look complete), but the lockfile is recorded either way: the plugins that did land are on disk, and a lockfile that no longer describes the tree is precisely the silent disagreement it exists to prevent. The rejection is re-raised after the record is written.
Only the FRESH clones are recorded (they land exactly on their pin / lock entry, so
their HEAD is evidence). A plugin that was already present is left as the lockfile has
it: install does not check where it sits, so overwriting its entry from the checkout
would throw away a lockfile the user just pulled in — the very thing the next sync
pass is about to realize.
opts.plugins (a name, or a list of names) installs only those — plus their
dependencies, since a plugin whose dependency is missing does not load. Every other
declared plugin is left exactly as it is, lockfile entry included.
Defined in plugins.lua.
btv.plugins.update(opts)
Fast-forward every installed, unpinned plugin, ADVANCING past the lockfile (and
re-recording it) — the cargo update half. opts.advance = false instead reproduces
the lock, moving a locked plugin onto (or leaving it at) the recorded commit; that is
what sync passes. Returns a promise of the count of plugins whose checkout MOVED.
Records the lock even when a plugin fails, for the reason install does.
opts.plugins (a name, or a list of names — plus their dependencies) updates only
those: the usual reason to update at all is one plugin whose fix you are waiting for,
and pulling in eleven other people’s changes at the same time is how a working editor
becomes a bisect. Every other plugin keeps its checkout AND its lockfile entry.
Defined in plugins.lua.
btv.plugins.sync(opts)
Install the missing, then update the rest — the one-shot “make the world match
my declarations”. Returns a promise of the count NEWLY INSTALLED (so a UI can tell
whether a fresh clone landed and prompt to restart).
A lockfile makes this the “realize my declared state” verb, NOT an update: it installs
what is missing at the locked commits, MOVES an existing checkout onto them when it has
drifted (pull a colleague’s newer lockfile and sync, and you get the tree it names —
reproducing the lock only for clones that happen to be missing would honour the file by
accident of what is on disk), realizes any commit/tag pin the spec has changed, and
fast-forwards only what nothing pins. Advancing PAST the lock is :PluginUpdate’s job —
if sync advanced, the lockfile would be honoured for the instant between the install and
the update pass and then thrown away, which is no lockfile at all.
(Locking is inherited from the two halves — install records the fresh clones and
update records the rest, after convergence. No third write here.)
opts.plugins (a name, or a list of names — plus their dependencies) realizes only
those, by handing the same scope to both halves.
Defined in plugins.lua.
btv.plugins.clean(opts)
Remove cloned plugin directories under the install root that no plugin declares
(a dev dir plugin lives outside the root and is never touched). Returns a
promise of the removed names.
opts.plugins (a name, or a list of names) instead removes EXACTLY those clones,
whether or not a spec still declares them — the “this checkout is wrong, give me a
fresh one” verb (remove, then install). Unlike every other scoped verb it does not
pull in dependencies: deleting a checkout you did not name is destruction by
inference. A local dir plugin fails LOUD instead of being deleted — that is your
own working tree, not a managed clone. A plugin still declared keeps its
btv.shada.plugin data (it is coming back); only a name nothing declares any more has
its namespace forgotten.
Defined in plugins.lua.
btv.plugins.list_system()
A synchronous snapshot of the system tier: { { name=, dir= }, … }, sorted by name.
Defined in plugins.lua.
btv.plugins.system(spec)
btv.plugins.system(spec) — inject a plugin into the system tier: clone/copy it into the
system dir (via the local-always seam) and load it into the current session, so it takes
effect now AND is re-seeded by the client into every future session (the VS Code
“install a connector” move). spec is a normal plugin spec (string shorthand / table).
Returns a promise of the plugin’s name. Callable form for init.lua.
Defined in plugins.lua.
btv.plugins.promote(name)
btv.plugins.promote(name) — promote an already-declared managed plugin into the system
tier: clone its on-disk checkout into the system dir and register it, so it persists into
every future session. Loads the system copy now unless the plugin is already loaded this
session. REJECTS (loud) for an unknown plugin. Returns a promise of the name.
Defined in plugins.lua.
btv.plugins.recommend(specs)
Register the recommended set (replacing any prior). A recommended spec may carry
every normal field EXCEPT a config/init function — those must be a STRING
of Lua source, because the set is serialized into the user’s plugins.lua and a
function cannot be written out. Fails loud on a function hook rather than
silently dropping it.
Defined in plugins.lua.
btv.plugins.bootstrap()
First-run flow: on a fresh setup (the user has declared no plugins of their own,
a recommended set exists, and we have not asked before), open the WELCOME view — a
floating checklist (prelude/plugins_ui.lua → M.ui.welcome) explaining that bemtvi
ships minimal and offering the recommended set pre-ticked, each item untickable.
The chosen subset is written to the user’s config and installed; an empty / skipped
choice does nothing. Returns a promise. Wired to VimEnter, and callable
directly. Re-entrant-safe (_prompting guard) and asks only once (the marker,
written before showing the view so a cancel still never nags again).
Defined in plugins.lua.
btv.plugins.list()
A synchronous snapshot of the declared set (declaration order): each
{ name, url, dir, lazy, pinned, loaded, locked }. Cheap (no disk) — loaded reflects
the in-memory load state and locked the commit the LAST-READ lockfile records (nil
when unlocked). Use status() for the disk-checked installed / head / drifted.
Defined in plugins.lua.
btv.plugins.status()
Like list() but disk-checked — a promise, since the reads are off-tick. Adds
installed, the checked-out head commit, and drifted (the checkout is at a different
commit than the lockfile records). Drift is the signal that the tree no longer matches
what the config’s lockfile promises, so :PluginList and the dashboard can say so
instead of leaving the user to compare shas by hand.
Reads the lockfile from disk first, so drift is computed against the CURRENT file (the user may have just checked out a different one) rather than whatever was last read.
Defined in plugins.lua.