UX-foundation.nvim is the schema-v1 runtime for the UX Neovim styling
ecosystem. It validates callback-free manifests, owns complete highlight and
structural targets, resolves declared/saved/session values, applies previews
transactionally, and persists canonical non-executable JSON profiles.
This repository is the canonical home of the accepted schema-v1 contract and its architecture decisions.
- Neovim 0.12.2 or newer; older releases are unclaimed
- no plugin-manager dependency
bluff is the default and only long-lived branch. Pin a release tag for
an immutable installation, or let your plugin manager retain an exact tested
bluff commit in its lockfile.
With lazy.nvim:
{
"777lotto/UX-foundation.nvim",
branch = "bluff",
opts = {},
}Calling any facade method initializes Foundation lazily. Explicit setup is recommended when selecting storage or lifecycle behavior:
require("ux_foundation").setup({
-- profile = profile_table, -- highest-priority startup selection
-- profile_path = "/path/to/profile.json",
-- storage_dir = vim.fn.stdpath("data") .. "/ux-styling/profiles",
-- load_active = true,
-- lifecycle = true,
-- core = true,
})An explicit profile wins over profile_path; either wins over the active
profile marker. By default, Foundation reads
stdpath("data")/ux-styling/active-profile.json. An absent marker means no
selected profile. Set load_active=false for an isolated process or test.
With core=true (the default), Foundation registers the native editor canvas,
base syntax groups, standard Neovim Tree-sitter captures, standard LSP semantic
token types/modifiers, and its managed semantic roles. Every native/capture
group is observed and exposes links, colors, blend, and all supported highlight
attributes. Registration alone does not rewrite the active colorscheme; only a
saved or session override changes an observed group.
All runtime errors returned by the facade have this shape:
{ code = "stable_code", message = "human-readable message", details = {} }setup() returns the module and raises a formatted setup error for invalid
options or an explicitly selected invalid profile. Mutating methods otherwise
return value, nil on success and nil/false, error on failure.
local foundation = require("ux_foundation")
foundation.setup(opts?) -- module; idempotent
foundation.state() -- defensive runtime summary
foundation._reset_for_tests() -- restore targets and teardown
foundation.register(manifest, implementation?, existing_handle?)
-- handle | nil, error
foundation.register_many({
{ manifest = manifest, implementation = implementation, existing_handle = optional_handle },
}) -- handle[] | nil, error
foundation.refresh(handle) -- true | false, error
foundation.refresh_many(handles) -- true | false, error
foundation.unregister(handle) -- true | false, error
foundation.registrations() -- registration[]
foundation.list_properties() -- inspection[] | nil, error
foundation.inspect(property_id) -- inspection | nil, error
foundation.inspect_raw_highlight(group, ns?) -- raw inspection | nil, error
foundation.load_profile(profile_table) -- true | false, error
foundation.load_profile_file(path) -- true | false, error
foundation.current_profile() -- defensive profile table
foundation.save_profile(path?) -- path | nil, error
local tx, err = foundation.begin_transaction() -- transaction | nil, errorregistrations() returns sorted defensive records suitable for a UI model:
{
{
manifest = callback_free_manifest,
availability = {
component_id = {
available = true,
reason = nil,
capabilities = {},
},
},
},
}Implementation callbacks and registration handles are intentionally absent
from that payload. state().registrations is a separate human-oriented
summary.
An inspection includes the property ID, type, target, activation mode,
availability, declared_raw, saved_raw, session_raw, selected_raw, the
resolved value, and layer/token/link provenance. Raw highlight inspection
returns normalized raw and effective definitions, a followed link_chain,
requested and definition namespaces, explicit namespace-zero fallback,
ownership, and whether the group is a virtual raw target.
Raw inspection also returns fields, keyed by raw field name. Each field record
is a defensive property-like inspection:
inspection.fields.fg = {
declared_raw = typed_value,
saved_raw = typed_value_or_nil,
session_raw = { op = "set", value = typed_value }, -- or remove_override/nil
selected_raw = typed_value,
resolved = typed_value,
provenance = { layer = "declared|saved|session", source = {}, token_chain = {}, link_chain = {} },
target_raw = {},
target_effective = {},
availability = { available = true },
activation = "immediate",
raw_highlight = { group = "Group", field = "fg" },
}Only one write transaction may be active. Every successful stage is previewed and journaled; a setter or rerender failure restores all captured targets and does not advance history.
local tx = assert(foundation.begin_transaction())
assert(tx:stage(property_id, typed_value))
assert(tx:stage_raw("DiagnosticWarn", "fg", {
kind = "rgb",
value = "#EED49F",
}))
assert(tx:load_profile(imported_profile))
assert(tx:load_profile_file("/path/to/import.json"))
assert(tx:undo())
assert(tx:redo())
assert(tx:reset(property_id)) -- also accepts plugin/component/state scopes
assert(tx:revert()) -- exact live opening targets, adapters, and layers
local status = tx:status()
-- Choose one close path:
assert(tx:commit()) -- close; session remains unsaved
-- local path = assert(tx:save()) -- atomically save/promote, then closestage, stage_raw, load_profile, load_profile_file, undo, redo,
reset, revert, and commit return true or false, error. save(path?)
returns the written path or nil, error; status() returns
{active, undo, redo, session_entries, raw_session_entries}. commit() never
writes a profile. reset("profile") includes registered and raw overrides;
{kind="raw"} resets every raw override.
Transaction-scoped profile imports replace saved/raw-saved layers and profile metadata as one journaled step, while clearing session layers. Undo or revert restores the prior profile; revert also restores the exact live highlight and adapter transaction baseline. That baseline begins as the live opening state and rebases after ColorScheme or late target availability so the opening logical layers survive over new target defaults. Invalid imports and apply failures leave the transaction and history intact.
tx:save() and foundation.save_profile() while a transaction is active both
promote its current profile and session layers, atomically write canonical
JSON, and close only after the complete save succeeds. A write failure leaves
the transaction, preview, history, and opening snapshot active. Outside a
transaction, save_profile() promotes a previously committed session. A
successful default save also atomically updates the active-profile marker; an
explicit export path does not change startup selection. If that marker update
fails, Foundation restores the profile destination's previous bytes or absence
before reporting failure. File-backed profiles are limited to 4 MiB and saves
use an exclusively created, randomized sibling temporary file before rename.
Boolean highlight edits synchronize the corresponding nested cterm flag so
terminal rendering matches the inspected value. Exact revert and failure
rollback preserve the captured cterm definition.
A saved raw-highlight entry whose group is currently absent remains preserved and inactive. Foundation retries it on lifecycle replay, so an optional plugin can define the group later without making profile startup fail. Interactive raw editing still accepts existing groups only.
A manifest contains data only. Executable behavior is supplied separately:
local handle, err = foundation.register(manifest, {
adapters = {
bufferline = {
probe = function(ctx) end, -- availability record
get = function(ctx, key) end, -- declared value
snapshot = function(ctx, sorted_keys) end, -- rollback snapshot
apply = function(ctx, sorted_changes) end, -- true or false, error
restore = function(ctx, snapshot) end, -- true or false, error
rerender = function(ctx, changed_keys) end,-- true or false, error
},
},
})Catalogs can publish multiple manifests under one registry/runtime checkpoint and one final profile application:
local handles, err = foundation.register_many({
{ manifest = first_manifest, implementation = first_implementation },
{
manifest = replacement_manifest,
implementation = replacement_implementation,
existing_handle = replaced_handle,
},
})
local ok, refresh_err = foundation.refresh_many(handles)register_many() validates every entry before callbacks, resolves ownership
and semantic tokens against the final registry, captures every registration
baseline, and applies the active profile once. refresh_many() validates and
deduplicates active handles before reprobe, then applies once and emits one
availability event containing only plugins whose availability changed. Either
operation rolls its complete registry, runtime, transaction-extension, and
physical apply boundary back on failure. Empty batches are successful no-ops.
Each change is {key, property_id, value, activation}. Structural setters run
before final compiled highlight writes so public setup APIs that regenerate
highlights cannot erase profile colors; rerender runs once afterward.
Adapters must use public target APIs and must make previewable mutations fully
reversible. probe() and capability callbacks return only JSON-safe
{available, reason?, capabilities?} data; executable, cyclic,
metatable-backed, or otherwise non-JSON capability values reject atomically.
Reload-only properties are validated and inspected but not previewed.
Unregister restores both highlight and structural registration-lifetime baselines before removing ownership, then immediately reapplies compatible raw profile layers. A restore or reapply failure rolls the complete transition back.
Foundation owns one replaceable UXFoundationLifecycle augroup. ColorScheme
events coalesce into one scheduled recapture and replay. Optional targets use
refresh(handle) after they load; registration itself remains plugin-manager
agnostic. When availability changes during a transaction, the close baseline is
the opening logical profile over the new target defaults, not the current
preview.
Post-publication User events are UXFoundationRegistryChanged,
UXFoundationApplied, UXFoundationProfileChanged,
UXFoundationAvailabilityChanged, and UXFoundationApplyFailed. Event data
contains contract_version, generation, reason, changed_ids, and an
error for failures.
Run :checkhealth ux_foundation for version, contract, and active-profile
diagnostics.
nvim --headless --clean -l scripts/check-lua.lua .
UX_FOUNDATION_TEST_FILE=tests/unit.lua \
nvim --headless -u tests/minimal_init.lua -l scripts/run-test.lua
UX_FOUNDATION_TEST_FILE=tests/core_catalog.lua \
nvim --headless -u tests/minimal_init.lua -l scripts/run-test.lua
UX_FOUNDATION_TEST_FILE=tests/register_many.lua \
nvim --headless -u tests/minimal_init.lua -l scripts/run-test.lua
UX_FOUNDATION_TEST_FILE=tests/smoke.lua \
nvim --headless -u tests/minimal_init.lua -l scripts/run-test.lua
nvim --headless -u tests/minimal_init.lua -c "helptags doc" -c quit
git diff --check
git diff --exit-code -- doc/tagsTests isolate all XDG directories and do not load or mutate the live Neovim configuration.
Successful push CI on the current bluff head automatically publishes a
versioned GitHub Release and notifies nvim-config with that exact commit.
CI creates unsigned tags, starting at v0.1.0 and incrementing the patch
version. Failed or superseded runs do not publish. The existing
NVIM_CONFIG_DISPATCH_TOKEN repository secret enables notification; a missing
secret fails visibly after publication. See release automation
for retry behavior, versioning, and the distinction between plugin releases
and configuration adoption.
MIT. See LICENSE.