tp/README.md

13 KiB

TP

TP 1.0 is a standalone retained/reactive text runtime for Emacs. It projects declarative properties, reactive data, and stable text objects onto strings and buffers while owning text-property composition, exact dependency tracking, retained identity, marker-backed mounts, diffing, transactions, rollback, and final buffer publication.

TP does not depend on Ebox or ECSS. It does not implement CSS selectors, stylesheets, specificity, cascade winners, Box, Flex, Grid, measurement, or layout. A CSS consumer may compute final declarations with ECSS and publish them through TP, but TP itself only understands Emacs text properties and generic retained text surfaces.

Chinese documentation: README_CN.md.

Complete public API reference: API-REFERENCE.md (中文). It is the symbol-level usage index for the current TP 1.0 implementation; this README remains the conceptual quick start.

Requirements

  • Emacs 28.1 or newer.
  • No third-party runtime dependency.
(add-to-list 'load-path "/path/to/tp")
(require 'tp)

Choose the smallest public entry point

Need API Live runtime?
Return a propertized string tp-propertize No
Apply declarations once to an existing range tp-apply No
Reactively decorate existing host text tp-watch Yes, properties capability
Own retained text content tp-surface-mount / tp-surface-update Yes, content capability
Inspect or remove a retained publication tp-surface-report / tp-surface-inspect / tp-surface-unmount Yes

The one-shot and retained APIs use the same property-policy and projection semantics. One-shot calls deliberately create no object, binding, marker, subscription, or surface state.

Static properties

(let* ((callback (lambda (_window _object _position) "Open"))
       (text
        (tp-propertize
         "Hello"
         (list 'face '(:foreground "white" :background "navy")
               'help-echo callback
               'keymap nil))))
  text)

Explicit nil and an absent property are different. In the example above, keymap is present with value nil. Function values are literal data, so TP preserves callback instead of calling it.

To modify an existing range without changing its text:

(with-current-buffer (get-buffer-create "*tp-demo*")
  (erase-buffer)
  (insert "abcdef")
  (tp-apply (current-buffer) 2 5 '(face italic)))

The established tp-set, tp-reset, tp-add, tp-remove, tp-clear, tp-get, tp-at, tp-member, match, regexp, search, navigation, interval, and native query APIs remain the direct façade for ordinary text-property work.

Reusable declaration recipes

define-tp and define-tps define reusable recipes that expand to direct Emacs properties. They are definition-time conveniences, not live mounted layers, and they never write identity or provenance into displayed text.

(define-tp link-style (foreground)
  `(face (:foreground ,foreground :weight bold)
    mouse-face highlight
    help-echo "Open item"))

(tp-set "Project" '(link-style "#58a6ff"))

Use tp-computed when a property value must be evaluated. Every ordinary function object remains literal.

(defvar my-height 1.2)

(define-tp sized-label ()
  `(face ,(tp-computed (lambda () (list :height my-height)))))

The computed function runs in the current binding/prepare context, so tp-signal-read and tp-binding-read establish exact dependencies. Its returned value is normalized and projected once, then treated as literal data.

Reactive existing text

tp-watch decorates a marker-backed host range without owning or replacing its text:

(let ((online (tp-signal-create nil)))
  (with-current-buffer (get-buffer-create "*tp-status*")
    (erase-buffer)
    (insert "offline")
    (tp-watch
     (current-buffer) 1 8
     (lambda ()
       (list 'face
             (list :foreground
                   (if (tp-signal-read online) "green" "red")))))))

A signal records the binding that actually reads it. Updating one signal invalidates only its subscribers; conditional computations automatically stop subscribing to sources that the new branch no longer reads. Equal writes are no-ops, and repeated writes inside tp-with-transaction recompute each affected binding at most once.

tp-watch returns the underlying surface handle. Pass it to tp-surface-inspect, tp-surface-report, or tp-surface-unmount.

Retained content

A retained producer receives a prepare context, ensures stable objects before producing output, installs any object-local bindings, and returns a pure surface plan:

(let* ((status (tp-signal-create "ready"))
       (producer
        (lambda (context)
          (let* ((object (tp-object-ensure context nil 'status 'text))
                 (binding
                  (tp-bind object '(demo . status)
                           (lambda () (tp-signal-read status)))))
            (tp-surface-plan-create
             :key 'status
             :kind 'text
             :text (tp-binding-read binding)
             :props '(face bold)
             :capability 'content))))
       (buffer (get-buffer-create "*tp-retained*"))
       (surface
        (tp-surface-mount buffer producer '(:capability content))))
  (tp-signal-set status "done")
  (tp-surface-report surface))

Plan fields are key, kind, text, props, children, tags, and capability. Plans contain no markers, buffer positions, patch operations, producer closures, or client continuations. Normal constructors defensively copy caller-owned strings and property data; candidate-local producers may use the explicit -owned plan/result constructors when they transfer every nested value and stop exposing it. An owned result must be created for the active prepare context and is consumed once.

Stable identity is surface-local. tp-object-ensure reconciles by parent, sibling key, and kind; tp-object-resolve returns a live opaque handle by key path. tp-surface-update-scoped authorizes a full candidate update against one or more retained objects and rejects output changes outside their current mount ranges unless the caller explicitly selects root fallback.

tp-surface-materialize-string uses the same producer and plan semantics without creating a live surface. Its candidate objects, bindings, subscriptions, and anchors are released before it returns.

Host ranges and property ownership

The properties capability uses opaque range anchors. A producer creates or receives a tp-range-anchor-create handle and attaches it to an object with tp-object-attach-range. Markers follow host edits; the plan itself remains position-free.

TP records the host baseline and each TP contribution per property interval. Overlapping contributions are composed through the registered property policy. If external code changes a property after TP publishes it, the next update reports tp-property-conflict instead of overwriting the external value. tp-range-rebase explicitly accepts the current host value as the new baseline. Unmount restores only values still owned by TP and preserves conflicting host edits.

Transactions and failures

tp-with-transaction batches signal writes and all affected surfaces. TP prepares every candidate first, then publishes surfaces in stable order. A compute, validation, buffer write, marker/index publication, or transaction participant failure restores signal values, binding values and dependencies, text, properties, markers, indexes, plans, client state, revisions, and previous reports together.

Observers run only after a successful commit. Their failures are recorded and do not roll back an already committed transaction. A buffer killed during publication remains killed; rollback never recreates it.

TP's default structured route drives the single live publication from exact transaction-scoped batch entries, one frozen participant vector, and the candidate-bound final accept. The same journals, surface snapshots, and change group are shared rather than copied. Set tp-transaction-execution-route to v1 before an outer transaction to use the complete legacy writer; the route is frozen for that transaction and the two writers are mutually exclusive. Shadow mode remains read-only compatibility evidence over whichever single route was selected; it never opens a second change group or writes a buffer twice. Generic opaque authority markers are bounded and whitelist-validated before final accept, then reverse-restored before ordinary rollback on partial apply or accept failure. tp-with-transaction still returns its body value, and internal outcomes remain observational side-channel evidence.

ETAF integration uses the same boundary through one opaque transaction participant. ETAF prepares its immutable generation and Ebox candidate before TP accepts the transaction; participant publish, TP final accept, and post-accept cleanup are ordered explicitly. A participant failure restores the previous generation and client state, while a post-accept observer failure is contained as a diagnostic. ETAF's runtime fixed-point guard records each effect's input/version tuple for one flush and stops repeated or over-bound cycles instead of spinning.

Property policies

tp-define-property-policy registers generic semantics for a final Emacs text property:

  • normalization and validation;
  • equality for no-op detection;
  • contribution merging;
  • projection to the final property value;
  • explicit presence, including present nil.

tp-register-text-property supplies the default policy for a native property. face contributions use merge semantics; other properties use their registered policy. tp-merge-declarations, tp-define-style, and tp-style-declarations operate only on direct declarations. They do not implement CSS cascade.

Public runtime families

Family Main public APIs
Core inspection and debug tp-debug-*, tp-intervals, tp-intervals-map, tp-plist, tp-text-snapshot, tp-empty-p
Property policy and declarations tp-define-property-policy, tp-register-text-property, tp-text-declarations, tp-computed, tp-resolve-value, tp-merge-declarations, tp-define-style, tp-style-declarations
Static recipes define-tp/tp-define-layer, define-tps/define-tp-group/tp-define-group, layer/group queries, undefine/reset/describe
Signals and bindings signal create/read/peek/set/dispose, binding install/read/dispose, tp-variable-signal, tp-with-transaction, tp-transaction-participate, tp-transaction-participate-v2, read-only tp-transaction-active-p, tp-runtime-manifest, counters/reset
Objects and plans plan/result constructors, tp-object-ensure, retain/reuse, fragment/content-range attachment, resolve, mounted/mounts
Host ranges tp-range-anchor-create, tp-range-anchor-live-p, tp-object-attach-range, tp-range-rebase
Surfaces mount/update/scoped update, materialize, live/revision/client-state, at-point, report/report-summary/inspect, unmount
Direct façade tp-propertize, tp-apply, tp-watch, tp-set, tp-reset, tp-add, tp-remove, tp-clear, tp-get, tp-at, tp-member
Search and navigation tp-match-*, tp-regexp-*, tp-search, tp-search-map, tp-forward*, tp-backward*
Native query and mutation tp-lookup, tp-property-change, tp-property-any, tp-property-not-all, tp-with-mutation-policy
Palette and display helpers palette definition/lookups, built-in recipes, tp-palette-show, tp-pop-to-buffer, tp-switch-to-buffer, tp-display-buffer-mode

See the complete API reference for signatures, return shapes, examples, and the full module index. See API semantics for ownership, lifecycle, error, and return contracts, and architecture for module boundaries and transaction flow.

TP 1.0 migration

TP 1.0 removes the 0.3 managed stack/renderer runtime instead of hiding it behind compatibility branches. Removed behavior includes tp-render.el, tp-stack.el, stack mutation APIs, tp-text, $variable declarations, layer-to-buffer registries, scan-driven refresh, managed attach/detach/diagnostics, and inline tp-name/tp-layers/tp-meta runtime storage.

Use direct recipes for reusable static declarations, tp-watch for reactive properties on existing text, and retained content surfaces for reactive text or structured UI. TP does not automatically scan historical propertized text to reconstruct runtime identity.

Examples

These examples use only TP public APIs and are tested without Ebox or ECSS on the load path.

Verification

make test
make test-shuffled SHUFFLE_SEED=20260806
make doctest
make compile-all WERROR=t
make checkdoc
make package-lint
make diff-check

The structural tests also prove that normal signal and surface updates do not scan buffer-list or search displayed text for identity, equal results do not publish a new revision, and unmount/kill cleanup releases subscriptions and marker-backed runtime state.

License

GPL-3.0-or-later. See LICENSE.