Expand description
The app framework: a resource, its actions, and views bound to it.
An app author writes three things:
- a resource type — a
#[derive(Model)]record (or any type implementingResourceby hand, such as a timeline or a rich-text document); - its actions —
#[action]methods in a#[resource]impl: typed, replayable, and exposed to the palette, agents and scripts; - views — a function from a
ViewCxto widgets, where every value shown is read from the resource and every edit is an action.
run hosts that on the toolkit’s component runtime, so without writing
any of it the app gets persistence and sync of its record, collaboration
and presence, versioned history (a History mode that renders past versions
with the app’s own view and restores any of them), per-author undo,
permissions enforced on every action (controls of forbidden actions disable
themselves, and a role change takes effect while the app runs), the action
palette and agent/script door, and search indexing.
#[derive(Model, Default)]
pub struct Note { pub body: String }
#[resource(kind = "example.note", label = "Note")]
impl Note {
/// Edit the note.
#[action]
pub fn edit(&mut self, text: String) { self.body = text; }
}
struct NoteApp;
impl ResourceApp for NoteApp {
type Resource = Note;
const ID: &'static str = "example.note";
const TITLE: &'static str = "Note";
fn view(cx: &mut ViewCx<Note>) -> Element {
Box::new(Column::new().push(Box::new(
cx.input(|note| note.body.clone(), Note::EDIT).multiline(true),
)))
}
}
fn main() { tilo_app::framework::run::<NoteApp>() }§Test the public workflow
testing::TestApp mounts the real app on an isolated profile. Start with
actions to discover the participant’s
complete action contracts, then call_on
using the discovered address and JSON arguments. Test a refusal before
call_confirmed_on for a destructive
operation, and verify that a live role change still refuses a saved address.
The runnable resource-journey example exercises that path alongside a bound
input control, authored task identity, undo and reopen without a display.
§Extension contracts
Custom parts do not leave the framework. Each contract asks only for what the framework cannot know:
- A custom resource type implements
Resource:readits state from a document (the same call renders history), name its replayableActiontype, and optionally declare named actions. Its own merge semantics live in itsAction::apply. - A custom widget or renderer implements the toolkit’s
Widgetfor pixels and input, plusResourceSurfaceto take part:presentthe resource’s state, submit edits through itsDispatcher, and report where its user is withResourceSelection. Built from aViewCx, the same surface renders past versions read-only in History mode. - Extending an existing widget — adding one hook to a toolkit widget,
a view or a surface — implements
Decoratorinstead ofWidget: name the wrapped widget and override only the hooks that change; every otherWidgetmethod, including the ones containers must forward, reaches the wrapped widget by default. - A resource nested in another — a note’s rich-text body, a draft’s
message, a saved query’s SQL — is a child resource: a resource type of
its own stored at a place in its parent’s document, whose edits the
parent carries in one of its own actions (
EditBody { page, edit }). The parent publishes it fromchildrenwithpublish_child; a view or surface edits it throughRecordEnv::child, aDispatcherof the child type that wraps each edit into the parent’s action. The child is addressed, permissioned, undone (by its own coalescing) and named in history like a root, and applies as the parent. Rich text istilo_rich_text::RichTextFieldover such a dispatcher; plain text isPlainText, whoseTextControlbinds aTextInputorCodeEditorso every keystroke is a splice action. - Handing a result to another app — a picture to place, a file to
attach — is an effect declared
content: it answersEffected::contentwith the hash of bytes in the device’s content store, its declaration’s result is atilo.contentresource, and an operation that takes content (ActionSpec::prompted_content) receives it as its input. The caller chains the two by those types and never reads the producing app’s document. - Local view state is a
LocalState: never persisted, never shared, never undone. A surface whose window should reopen as it was left (the open note, the caret) also providesApplicationViewState: the root saves and restores it with the window. - Launching on something — a path or URL on the command line, a file
a host hands over — asks the app which record that is
(
ResourceApp::launch_record: a file’s own record, one derived from it, a minted one) and which of the resource’s actions the launch takes there (ResourceApp::launch, through the same door as every control), or which of its effects, for a launch that shows something (a link to one item) rather than editing (Invoke::effect); the view reads the same launch (RecordEnv::launch) to show what those actions made.ResourceApp::OPENSnames the media types it is handed andResourceApp::ARGUMENTShow many arguments it takes. A handed file’s bytes are kept in the launch’s content store (LaunchInput::content), which the actions name by hash, so the record never carries the file. The app’s own Open is the same launch in the window it runs in (RecordEnv::open_here,SurfaceCx::opener): the window shows the record that launch names in place of its own, so a document has one record however it was opened. An app that offers private windows (ResourceApp::PRIVATE_WINDOWS,--private) runs one on a profile thrown away with the process, attached to no runtime, and its view knows (RecordEnv::is_private). A command the binary runs instead of a window (--emit-photos DIR) is aCommandinResourceApp::commands; a helper the binary also is (a privileged writer started with its own command line) is aHelperinResourceApp::helpers, asked before anything of the app starts; and the app’s messages are itsResourceApp::catalog, loaded in every host that mounts it. A launch that continues playback another device or app handed over says where it stood (LaunchInput::handoff): the media is what the launch names, the position and play state are the view’s to restore. The app’s own options — how the window runs rather than what it opens, such as the layout an on-screen keyboard starts in — areResourceApp::OPTIONSandResourceApp::FLAGS, read withRecordEnv::launch_option. - A system surface — the session’s on-screen keyboard — declares its
window’s role in
ResourceApp::BEHAVIOUR(WindowRole::InputSurface): the window announces the app’s id to the compositor and no headless drive operates it. The connections it serves the session through (an input method, a virtual keyboard) are the surface’s own, opened in a live run (RecordEnv::mode) and woken throughSurfaceCx::waker. - Mounted in another process — the shell hosting a terminal tab in
its own — the app is the same component, on a record the host opens
for it, publishing into the host’s palette and agent door. What only
the host can provide (the session’s process supervisor) it lends
(
HostContext::lend); the view takes it withRecordEnv::take_lentand otherwise uses its own. - Who is editing is
RecordEnv::mefor the reader of a view, andActionCx::authorinside an action: the same id, pinned with the action so every replica reads it. The resource state stays the same for everyone; a view compares the authors it records withme, and an action signs what it writes with its author rather than carrying one in its payload. What a view shows for an id is the name the host’s roster has for it (RecordEnv::people): the document keeps ids, so a name that changes or arrives late reads right everywhere. - A brand-new record starts with the resource’s
initialize, its genesis: the same change on every device, so two people creating the same record converge on one. What depends on who creates it or where (the creator as owner, a first-run import of existing settings) isResourceApp::seed: actions applied once after the genesis, by the creator, that are nobody’s undo step. - Which changes are undo steps is declared by the action
(
tilo_actions::Action::change_kind,#[action(external)]): content from outside — fetched rows, a process’s output — syncs to everyone but no undo takes it back. Values applied immediately that continue one another (Resource::coalesces) are one step — a word typed, a slider dragged — until the surface settles the gesture. - Settings — preferences that follow a person or stay on a device,
never seen by collaborators — are a typed
SettingsSchemawhoseScopesays how far they travel:Personalroams across the person’s own devices,DeviceLocalnever leaves this one,Sharedreads a per-user section every app follows (locale,text_assistance).RecordEnv::settingshand out aSettingkept current while the view lives; a write from another window or device reaches it on the next frame. Files and secrets come from the same context (RecordEnv::storage,SurfaceCx::secrets,SurfaceCx::files); a secret that belongs to one document item (a profile’s password) issecrets().item(purpose, id), so the document names the item and the secret stays on the device. - The person’s own data — what they watched, the words they taught
the keyboard, where they stopped reading: data that grows, has history
and is undone, but belongs to the person rather than the room — is a
resource kept in the person’s own room (
RecordEnv::person). ThePersonRecordis one record per kind on all of the person’s devices, edited with the resource’s actions, attributed, in its history and undone per step, and synced to the person’s devices only — never to a collaborator, whatever room the app is open in. A preference is still a setting. - Effects — work with no document delta (reload, back up now,
connect, run a query) — are a typed vocabulary:
#[derive(Effect)]on an enum, one variant per effect. A view answers it withViewCx::effects, a custom surface withResourceSurface::effect(impl ResourceSurface<R, MyEffect>); the framework publishes it beside the resource’s actions, with the availability and labels the handler reports now (EffectStatus), and every caller — a button (ViewCx::effect_button,Dispatcher::effect), the palette, an agent, a script — reaches the handler as a typed value. Long work is a job started through theEffectCx(thread,spawn,reply): it reports progress, may be cancelled, and finishes on the UI thread, changing the document with ordinary actions and answering its caller. Importing and exporting is such a job: the host’s file chooser opens throughEffectCx::choose_fileandEffectCx::save_file, the same on every host, a browser page included. An effect whose harm depends on its arguments (formatting the system disk) classifies each invocation (#[effect(risk = "…")]), and asks for confirmation only when it is destructive. A surface withholds one of the resource’s own actions while its live state forbids it (ResourceSurface::availability). Effects never run in History mode. A variant that only changes what its caller sees (open, reveal, show) is#[effect(view)], published as a View operation and answered by the same handler. - Operations on one item — open this snippet, reveal this tag,
connect this connection, run this saved query — are effects bound
to the item: the resource’s
childrencarry.with_action(MyEffect::Open { id }.bound()), a live rowLiveRow::effect. The palette, an agent or a script invokes it on the item through the same door as every other action, and the handler ofMyEffectreceives that value. An effect only ever offered this way is#[effect(row)], so the resource does not also offer it with an id to type. No view or surface answers a rawWidget::start_operationfor it. - Jobs nobody invoked — a route fetched when the destination
changes, a feed refreshed when it opens — start through the view’s or
surface’s
Jobs(ViewCx::jobs,SurfaceCx::jobs), under a key the latest request takes over, and finish like an effect’s. - Projections — an outside consequence an action recorded in the
document (an outbox, a filesystem operation, a device’s configuration)
— are drained with [
ViewCx::drain]: the framework runs the drain in the live app after every change, on every participant’s device, and the drain acknowledges each entry with an action. - Files of the mounted filesystem an app shows (a file manager’s
selected file) have a history of their own, kept by the filesystem’s
projection rather than the app’s document. The app declares it
(
ResourceApp::FILE_HISTORY) and its surface names the file (SurfaceCx::file_history); History offers it beside the resource’s. - Run mode. A view is built for a window someone uses, a past version
in History, or a screenshot (
RunMode, fromRecordEnv::mode). Only a live view starts work that reaches outside the process (a mail engine, a socket, a journal reader, a hardware probe): History rebuilds the view for every version it previews, and a screenshot must come out the same on every machine. - Edits bound to where the user is — the selection, the caret, the
playhead, the folder shown — are typed builders of the resource’s
action (
ActionDecl), published on the resource beside its declarations:ResourceSurface::bound_declarationsfrom a surface’s view state,ViewCx::bind_declarationsfrom a view’s local state, andResource::bound_declarationsfrom the resource state itself. - Live external rows — processes, devices, containers, journal
entries, fetched catalogue rows, running jobs — are
LiveRows fromResourceSurface::live_rows, merged by id into the resource’s published children. A row carries only view and effect operations (an effect bound to the row withLiveRow::effect) and may say how its entity stands right now (LiveRow::presentation). An edit of the resource bound to a row (rename this file) is one ofResourceSurface::row_declarations, published on the row. Neither rows nor children need to be published whole: a bounded page is published and the rest resolve by id (ResourceSurface::live_row,Resource::child). - One search source — what the app’s search finds is the resource’s
entries (
Resource::search) and, for what a surface shows that the resource does not hold (a page’s text, a book’s outline, processes), the surface’s own (ResourceSurface::search_entries), indexed together. An entry says how its hit reads — its result group, subtitle, icon and the action it invokes — so the app needs no search provider beside it. - One published tree — the palette, agents and scripts see the
resource’s publication and nothing beside it that changes the document:
a document action a content widget publishes through its own
PublishesResourcesis dropped (a debug build stops at it). - A participant without a window — an agent, a service, a script —
acts through the app’s own view mounted over the record and never
drawn (
AttachedView): the same door as a person, checked against its permissions, attributed to it (an agent acting for its person) and recorded in history with the task it belongs to. - Screenshot scenarios (
--shots --scenario NAME, every one with--shots-dir DIR, each atResourceApp::shot_viewport) are prepared in three parts. What the resource does not hold is the scenario’sFixture(ResourceApp::fixture), prepared before the view is built: typed stand-ins for live sources that views and surfaces read withRecordEnv::fixture, scratch files, content-store blobs, versions other people wrote at other times, and collaborators shown where they are. The resource is staged with actions (ResourceApp::scenario), and view state where it lives, once the actions are laid out:ViewCx::on_stagefor a view’s local state,ResourceSurface::stagefor a custom surface’s. What a person does in the scene (typing, a drag held over its target) isResourceApp::scenario_input, driven through the window’s own event path. The capture then waits until no surface reports unfinished visual work (an async page render, a decode).stageruns a scenario on any harness, andTestApp::capturecomposes the exact frame--shotswrites in a test.
Modules§
- content
- Record content, canonical hashing and picture headers. Use the store
borrowed through
SurfaceCx::contentorLaunchInput::contentso the host binds reads and writes to the resource’s content lane. - dev_
reload - The symbols a
thinapp library exports sotilo-app devcan run new code inside the windows the process already has. - history
- The resource’s history as actions every framework app answers, so the people, scripts and agents working on it can look back, compare and take back through the same door they edit through:
- testing
- Testing a framework app the way it runs: the real component runtime, storage, permissions, undo and history, without a window.
Structs§
- Actions
- The actions a resource publishes, for binding controls that are built outside the view function: rows a table, list or tree creates on demand. Cheap to clone; clones share the published descriptor.
- Attached
View - One record held in its app’s view, attached and never drawn. See the module docs.
- Chosen
File - A file the person chose with the host’s chooser
(
EffectCx::choose_file). - Command
- A command the app’s binary runs instead of opening a window: a developer utility (writing fixtures, staging a repository) that shares the app’s code but is not part of using it.
- Dispatch
Group - An open
Dispatcher::begin_group: every edit submitted until it drops becomes one action. - Dispatcher
- Submits a view’s edits. Cloneable; clones share one queue.
- Document
Version - The resource as it was at a past version a view asked for: its
document there, the heads that name it and the name a caller gives it
back (
EffectCx::version). - Each
- Rows for the items of a collection, in collection order, built by
ViewCx::each. - Effect
Cx - What an effect handler, a job’s completion and a projection drain receive: the resource, its dispatcher, and the means to start a job.
- Effect
Reply - The answer of an effect, given later from wherever its work settles — a
reply from another service, a callback, a worker.
Send. Dropping it unanswered fails the effect. - Effect
Status - What an effect, or one of the resource’s own actions, looks like to callers right now, when it differs from its declaration: unavailable while the engine is offline, labelled “Stop recording” while recording.
- Effects
Handle - A registered effect handler (
ViewCx::effects). - File
History - The History scope of
ResourceApp::FILE_HISTORY: the files of the mounted filesystem an app shows. - Fixture
- The scene of one screenshot scenario that its actions cannot stage:
handed to
ResourceApp::fixturebefore the view is built. - Form
- The parameters of the verb
Pas they are being filled in (ViewCx::form). Clones share the values. - Form
Submit - A form’s submission (
Form::submit). - Helper
- A helper the app’s binary is besides the app: a privileged writer, a
staging step, a packaging probe. Asked first, with the process’s whole
command line, before anything of the app starts (no window, no session,
no portal), it answers
Nonefor a command line that is not its own, else runs and answers the exit code the process ends with. A refused command line is an error, which ends the process with code 1. - JobProgress
- Where a running job last said it is, as its
Progressreported it. - Jobs
- Work a view or surface starts on its own, with nobody invoking an
effect: a route fetched when the destination changes, a feed refreshed
when it opens. Each job runs under a key; starting another under a key
still running stops the earlier one, so the latest request wins. A job
is started like an effect’s (
EffectCx::thread,EffectCx::spawn) and finishes the same way, on the UI thread, with ordinary actions; a surface’s completion gets&mutthe surface. Jobs run only in a window someone uses: a past version and a capture start none. - Landed
- A submission the door applied (
Dispatcher::landed). - Launch
Input - What a launch handed the app: the arguments of its command line or of an “open with” (paths, URLs), the app’s own options it named, the file a host lent it, the content store where the app keeps what a launch hands it, and where playback of the media it names stood on the device or app that handed it over.
- Live
- A shared handle to the current state of one resource.
- LiveRow
- One live external entity a surface publishes beneath its resource, with the view and effect operations it offers (inspect a process, stop a container).
- Open
Here - Opens what a launch naming some arguments opens in the window an app runs
in, in place of the record it shows: what
RecordEnv::open_heredoes, kept by a custom surface for the input that later asks for it (a file chosen in a picker). - Open
View - An
AttachedViewabout to open (AttachedView::opening): whom it is for, what they may do, and what the host lends the view. - Past
Version - A past version drawn by the app’s own view, exactly as History mode’s
preview draws it: read-only, over the frozen document. Built by
HeadlessApp::past_version. - People
- The names the host knows for the participants of the window’s room. Cloneable; clones read the same roster.
- Person
Record - A resource of the person’s own, kept in their own room: what they watched, the words they taught the keyboard, where they stopped reading. It follows them to their other devices and never reaches a collaborator, whatever room the app is open in.
- Progress
- A running job’s channel back to the app: report progress, notice
cancellation. Cloneable and
Send, for the worker. - Projection
- A registered projection drain (
ViewCx::drain), as the view or surface that registered it holds it. - Record
Env - The record, run and storage a view or surface works in. See the module docs.
- Records
- The records an app keeps besides the one it shows, declared in
ResourceApp::records: each is opened with the window and published beside the app’s resource, so the palette and agents reach it whether or not a view has shown it, and the app’s committed surface lists it. - Refused
- An edit or effect the door refused (
Dispatcher::refused). - SeedCx
- What
ResourceApp::seedmay read: who is creating the record, and the app’s host — its settings, files and secrets on this device — when the record is created in a window. A participant without one (an agent, a room service:seed_record) creates it with no host: what a seed imports from the device is the device’s own to seed. - Setting
- One typed settings value, kept current while the view lives: a write
from this window, another window, the Settings app or another of the
person’s devices (for
Scope::Personal) reachesSelf::stateon the next frame. - Surface
Cx - What a surface receives when attached.
- Text
Control - A text field or code editor over collaborative text — a
PlainTextresource or child (a SQL buffer, a report note), or a text field of a record at itsPath(Self::at) — whose every edit is a splice throughdispatcher. - ViewCx
- Everything a view needs: reads of the resource state, bindings of controls to actions, local state, and custom surfaces.
Enums§
- Effected
- What an effect handler (or a job’s completion) answers.
- NoEffect
- The vocabulary of a surface that has no effects.
- Resource
Selection - Where a participant is inside a resource, in the resource’s own terms, so it means the same thing on every collaborator’s screen.
- RunMode
- What a view is built for. See the module docs.
Constants§
- SURFACE_
FLAG - The flag that makes an app’s binary print its
surface_reportinstead of running: whattilo-app surfaceandtilo-app checkread.
Traits§
- Action
Control - A toolkit control whose committed value
Ican invoke an action. - Bindable
- What a control shows and changes: a field of the record, by its
Path, or a field of a form (ViewCx::form), by the path into the verb’s parameters.Mtells the two apart and is always inferred. - Edits
Text - A text control a
TextControldrives: it shows the text object it is bound to and keeps the user’s edits local, for its owner to submit. - Effect
- A typed vocabulary of effects. Derive it (
#[derive(Effect)]); see the module docs. - Host
Delivery - Delivering an admitted invocation to a widget, as its host does. Answers
how the widget started it — done at once with its output, or pending to
finish later — or
Nonewhen nothing in the widget answers it. - Other
Record - A record other than the app’s own that views and surfaces edit: the
person’s own record of
P(PersonRecord), or a settings sectionPthat is a resource model (Setting). - Resource
App - A framework app: one resource type and a view of it.
- Resource
Surface - A custom widget or renderer — a timeline, a canvas, a GPU viewport, a page layout engine — taking part in a resource.
- Submit
- Something a dispatcher submits and a button is bound to: a field edit
made at a path (
p.favorite.set(true)), a verb with its arguments, or an invocation built some other way. No app writes an action id or an argument map.
Functions§
- definition
- The toolkit application definition of
A, created once per process. - definition_
launch_ spec - The launch specification of any application definition: the standard component host, record and launch modes, without a resource type’s capture scenarios.
- host_
link - The link to the resource
requestnames inapp’srecord, for a room’s host that answersshare-linkitself on the records it runs headless: the link an app’s root mints, under the host’s ownlocator. - hosted_
kinds - The resource type of every framework app this process has the definition
of (
definition): what a host that holds records of those apps without mounting them — an agent, a room service — reads, publishes and edits them by. Look one up by the kind a record names ([Kinds::get]) or by its app ([Kinds::hosted_by]). - launch_
spec - The launch specification of
A, for apps that add options to it. - reserved_
action_ ids - The ids of the actions the framework publishes on every app’s resource
beside the app’s own: the generic
apply,groupandfield-edit, the viewsopenandread, history, and the presentation operations. An app verb, effect or view under one of these ids shadows the framework’s or is shadowed by it, sotilo-app checkrefuses it. - resource_
kind A’s resource type as a kind a host can look up: hosted byA, and an open resource named asAnames it (ResourceApp::label). Every framework app gets its kind from here, so a host working with whatever a room holds reads, publishes and replays it without any of the app’s own code.- run
- Run
A: the live window, or--shots,--headlessand the other standard launch modes. Needs the native runner (the defaultnativefeature); a host without it driveslaunch_specitself. - run_
helper - Run the first of
A’s helpers thatargsis for, answering the exit code the process ends with;Nonewhen the command line is the app’s own. - seed_
record - Seed a record of
kindits genesis just created, asmecreating it ondevicewithout a window: the app’sResourceApp::seedwith no host, applied as in a window. A kind no app registered has nothing to seed. - seed_
record_ in_ room - Seed a new room record in its host-selected creation scope, without lending ambient device services. The route supplies context, not permission.
- stage
- Stage
scenario(Nonefor a plain--shots) on a runningA: apply its resource actions through the host’s door, then let its views and surfaces stage their view state, and run the frames that settles. - time_
frames - How long each of
framesframes ofA’s capture ofscenariotakes to sync, lay out and draw, after one frame that warms the text caches: what a--benchcommand reports. Where no native runner is linked (a browser page) there is no headless capture to time. - with_
surface - Visit the surface of type
Sthe root of a resource app ofRshows: for a test, or a host that mounted the app, reaching the surface between frames (App::with_component_rootlends the root of a mounted one).Nonewhenrootis not such a root or shows noS.
Type Aliases§
- Effect
Answer - What a handler or a job’s completion returns: the answer, or why the effect failed.
Derive Macros§
- Effect
- Derive
Effect: an enum’s variants become the app’s published effects — work with no document delta, invoked by buttons, the palette, agents and scripts alike. Seetilo_resource::effect.