Replace the single file-structure.md with a stratified layout designed
for AI/agent skill consumption: tables and concrete identifiers over
prose, files loaded on demand, content separated by churn rate.
Layers:
- architecture/ stable structural reference (overview, source-map,
test-map, data-flow)
- patterns/ memory, commands, autocomplete, events, xmpp,
encryption, ui, plugins
- testing/ unit-tests, stubs, functional-tests, bench
- build/ local, docker, ci
- playbooks/ add-command, add-test, add-autocomplete,
add-event-handler, add-encryption
- gotchas.md append-only dated entries (seven seed entries)
- wip/ branch-specific notes; deleted on merge to master
Stable layers describe cproof on master only. In-flight feature
branches (currently feat/ai) get a single file under wip/.
INDEX.md is the entry map with churn labels; SKILL.md is the
always-loaded skill hint pointing to it.
3.0 KiB
Playbook: add an event handler
src/event/ mediates between src/xmpp/ (and locally-triggered actions)
and the rest of the codebase. Two directions:
- Server events (
sv_ev_*) — triggered by inbound stanzas. - Client events (
cl_ev_*) — triggered by local actions (UI, command).
See patterns/events.md for the conceptual split.
Decide direction
| Trigger | Direction | File |
|---|---|---|
Inbound stanza parsed in src/xmpp/. |
Server. | src/event/server_events.c/h |
| User typed a command, UI key, or internal timer. | Client. | src/event/client_events.c/h |
Add the handler — server example
src/event/server_events.h:
void sv_ev_foo_received(const char* const from, const char* const payload);
src/event/server_events.c:
void
sv_ev_foo_received(const char* const from, const char* const payload)
{
// 1. Update persistent state.
chatlog_msg_in(from, payload);
// 2. Push UI update.
ProfChatWin* win = wins_get_chat(from);
if (win) {
chatwin_incoming_msg(win, payload, NULL, FALSE);
} else {
cons_show_incoming_message(from, payload);
}
// 3. Plugin callbacks last — state must be consistent.
plugins_post_chat_message_received(from, payload);
}
Wire the trigger site
For a server event, the matching parser in src/xmpp/ calls the new
handler. For instance, if <foo> stanzas are parsed in
src/xmpp/message.c:
// ... after parsing the stanza ...
sv_ev_foo_received(from, payload);
For a client event, call from cmd_funcs.c (or wherever the user-facing
trigger lives).
Stub the handler in tests
tests/unittests/event/stub_*.c — add a stub for the new function so units
that exercise the trigger site link cleanly:
void
sv_ev_foo_received(const char* const from, const char* const payload)
{
// pass-through (or check_expected, depending on test needs)
}
If a test needs to verify the handler is called with specific args, switch
the stub to check_expected and the test queues expect_string / expect_value.
See testing/stubs.md.
Plugin hook (if applicable)
If the event should fire a plugin callback, add a dispatcher in
src/plugins/plugins.c/h (e.g. plugins_post_foo_received) and call it
last in the handler — after state and UI are consistent. Keep C and
Python plugin APIs in sync (c_api.h / python_api.h).
Unit test the handler
tests/unittests/test_server_events.c (or test_client_events.c) holds
event-side tests. Pattern:
void
test_sv_ev_foo_received_writes_chatlog(void** state)
{
expect_string(chatlog_msg_in, jid, "alice@example.com");
expect_string(chatlog_msg_in, msg, "hello");
sv_ev_foo_received("alice@example.com", "hello");
}
Conventions
- One handler, one event. Don't pile multiple unrelated events into the same function.
- Order inside the handler: state → UI → plugins. Never call plugins before state is committed.
sv_ev_*andcl_ev_*should be straight-line — no XMPP protocol parsing, no UI rendering. They orchestrate; they don't implement.