Skip to content

Repository files navigation

UX-foundation.nvim

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.

Requirements

  • Neovim 0.12.2 or newer; older releases are unclaimed
  • no plugin-manager dependency

Installation

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.

Public facade

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, error

registrations() 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" },
}

Transactions

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 close

stage, 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.

Registration and adapters

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.

Lifecycle and events

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.

Development

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/tags

Tests 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.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages