docs: split context into layered, agent-oriented files
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.
This commit is contained in:
97
patterns/autocomplete.md
Normal file
97
patterns/autocomplete.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Autocomplete
|
||||
|
||||
## The `Autocomplete` type
|
||||
|
||||
`src/tools/autocomplete.h` defines an opaque `Autocomplete` (GList-backed
|
||||
sorted set with a stateful "current iterator" for cycling through matches).
|
||||
|
||||
| Function | Purpose |
|
||||
|---|---|
|
||||
| `autocomplete_new()` | Create empty AC. |
|
||||
| `autocomplete_clear(ac)` | Drop all entries (keep object). |
|
||||
| `autocomplete_free(ac)` | Destroy. |
|
||||
| `autocomplete_add(ac, item)` | Insert sorted. |
|
||||
| `autocomplete_add_unsorted(ac, item, reversed)` | Append, no sort. |
|
||||
| `autocomplete_add_all(ac, items)` | Bulk add from `char**`. |
|
||||
| `autocomplete_update(ac, items)` | Clear + add_all. |
|
||||
| `autocomplete_remove(ac, item)` | Drop one. |
|
||||
| `autocomplete_remove_all(ac, items)` | Drop many. |
|
||||
| `autocomplete_complete(ac, search, quote, previous)` | Cycle to next/previous match. |
|
||||
| `autocomplete_reset(ac)` | Reset cycle position. |
|
||||
| `autocomplete_contains(ac, value)` | Membership. |
|
||||
|
||||
## Two completion flavours used in commands
|
||||
|
||||
In `src/command/cmd_ac.c`:
|
||||
|
||||
```c
|
||||
autocomplete_param_with_ac(input, "/cmd sub", static_ac, quote, previous);
|
||||
autocomplete_param_with_func(input, "/cmd sub", callback, previous, ctx);
|
||||
```
|
||||
|
||||
| When to use | Choose |
|
||||
|---|---|
|
||||
| Fixed token set known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete` (allocated in `cmd_ac_init`, freed in `cmd_ac_uninit`). |
|
||||
| Token set is dynamic / queried from state. | `autocomplete_param_with_func` + a callback `char* fn(const char* search, gboolean previous, void* ctx)`. |
|
||||
|
||||
## Static Autocompletes
|
||||
|
||||
Pattern (from `cmd_ac.c`):
|
||||
|
||||
```c
|
||||
// File-scope:
|
||||
static Autocomplete account_ac;
|
||||
|
||||
// In cmd_ac_init():
|
||||
account_ac = autocomplete_new();
|
||||
autocomplete_add(account_ac, "list");
|
||||
autocomplete_add(account_ac, "show");
|
||||
autocomplete_add(account_ac, "add");
|
||||
// ...
|
||||
|
||||
// Registered with:
|
||||
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
|
||||
```
|
||||
|
||||
Static `Autocomplete` instances are added to a file-scope free-list array
|
||||
near the top of `cmd_ac.c` so `cmd_ac_uninit` can call `autocomplete_free`
|
||||
on each at shutdown. **Add new static ACs to that free-list.**
|
||||
|
||||
## Function-callback completers
|
||||
|
||||
```c
|
||||
char* roster_contact_autocomplete(const char* const search_str,
|
||||
gboolean previous, void* context);
|
||||
```
|
||||
|
||||
- Stateless across invocations — no module-level `_last_match` globals.
|
||||
- Returns a freshly-allocated `char*` (caller frees), or `NULL` when no more
|
||||
matches.
|
||||
- `previous` cycles backward (Shift+Tab).
|
||||
- `context` is whatever was passed in `autocomplete_param_with_func`'s last
|
||||
arg (often `NULL`).
|
||||
|
||||
`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) is a clean
|
||||
canonical example: it delegates straight to `autocomplete_complete` against
|
||||
a roster-owned `Autocomplete` object — no callback-local state.
|
||||
|
||||
**Avoid the legacy "store previous match in a static" pattern.** Some older
|
||||
completers do it; do not copy them. Keeping callbacks stateless makes
|
||||
shift-tab cycling and concurrent completers safe.
|
||||
|
||||
## Per-command dispatcher
|
||||
|
||||
Every command's autocomplete is a single static function in `cmd_ac.c`:
|
||||
|
||||
```c
|
||||
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
```
|
||||
|
||||
Registered in `cmd_ac_init`:
|
||||
|
||||
```c
|
||||
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
|
||||
```
|
||||
|
||||
The dispatcher inspects the input prefix and picks the right
|
||||
`autocomplete_param_with_*` call.
|
||||
109
patterns/commands.md
Normal file
109
patterns/commands.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Commands
|
||||
|
||||
## Three files, three concerns
|
||||
|
||||
| File | Holds |
|
||||
|---|---|
|
||||
| `src/command/cmd_defs.c` (`cmd_defs.h`) | The big static array of `Command` records — every `/foo` is one entry. |
|
||||
| `src/command/cmd_funcs.c` (`cmd_funcs.h`) | Handler implementations: `gboolean cmd_<name>(ProfWin*, const char* const, gchar**)`. |
|
||||
| `src/command/cmd_ac.c` (`cmd_ac.h`) | Per-command autocomplete logic and registration. |
|
||||
|
||||
## Anatomy of a command record
|
||||
|
||||
Defined inline in `cmd_defs.c` using uppercase macros (declared in `cmd_defs.c`):
|
||||
|
||||
```c
|
||||
{ CMD_PREAMBLE("/caps",
|
||||
parse_args, 0, 1, NULL)
|
||||
CMD_MAINFUNC(cmd_caps)
|
||||
CMD_TAGS(
|
||||
CMD_TAG_DISCOVERY,
|
||||
CMD_TAG_CHAT,
|
||||
CMD_TAG_GROUPCHAT)
|
||||
CMD_SYN(
|
||||
"/caps",
|
||||
"/caps <fulljid>|<nick>")
|
||||
CMD_DESC("...")
|
||||
CMD_ARGS(
|
||||
{ "<fulljid>", "..." },
|
||||
{ "<nick>", "..." })
|
||||
CMD_EXAMPLES(
|
||||
"/caps user@host/res")
|
||||
}
|
||||
```
|
||||
|
||||
| Macro | Role |
|
||||
|---|---|
|
||||
| `CMD_PREAMBLE(name, parse_fn, min, max, pre_hook)` | Command name, parser, arg count bounds, optional pre-hook. |
|
||||
| `CMD_MAINFUNC(fn)` | Single handler for the whole command. |
|
||||
| `CMD_SUBFUNCS({"sub", fn}, ...)` | Dispatch by first argument. Use **instead of** `CMD_MAINFUNC` when the command has subcommands (e.g. `/status get`, `/status set`). |
|
||||
| `CMD_TAGS(...)` | One or more `CMD_TAG_*` (UI grouping, search). |
|
||||
| `CMD_SYN(...)` | Synopsis lines for `/help <cmd>`. |
|
||||
| `CMD_DESC(...)` | Long description. |
|
||||
| `CMD_ARGS({"name", "desc"}, ...)` | Argument table for help. |
|
||||
| `CMD_EXAMPLES(...)` | Examples shown in help. |
|
||||
|
||||
The full set of `CMD_*` macro definitions is at the top of `cmd_defs.c` —
|
||||
read them when authoring a new command.
|
||||
|
||||
## Argument parsing
|
||||
|
||||
`parse_args()` lives in `src/tools/parser.c`:
|
||||
|
||||
```c
|
||||
gchar** parse_args(const char* const inp, int min, int max, gboolean* result);
|
||||
```
|
||||
|
||||
- Splits on whitespace, honours quoted segments.
|
||||
- Returns `NULL` on parse failure (sets `*result = FALSE`).
|
||||
- The handler receives the result as `gchar** args` — free is owned by the
|
||||
caller in `cmd_defs.c`, the handler does **not** free.
|
||||
|
||||
Parser variants used in `CMD_PREAMBLE`:
|
||||
- `parse_args` — standard quoted-aware split.
|
||||
- `parse_args_with_freetext` — last argument is the rest of the line, no
|
||||
splitting (used by `/msg`, `/me`, etc.).
|
||||
- `parse_args_as_one` — concatenates remaining tokens into a single string.
|
||||
|
||||
## Handler signature
|
||||
|
||||
```c
|
||||
gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args);
|
||||
```
|
||||
|
||||
Return `TRUE` on success, `FALSE` to print bad-usage. `command` is the literal
|
||||
`/foo` typed (useful when the same handler is registered under multiple names).
|
||||
|
||||
## Connection-state gates
|
||||
|
||||
Most commands that touch XMPP must verify connection state first:
|
||||
|
||||
```c
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
`cons_bad_cmd_usage(cmd)` is the canonical "wrong arguments" path; pass the
|
||||
literal command name.
|
||||
|
||||
## Adding a new command
|
||||
|
||||
See `playbooks/add-command.md` for the full walkthrough. Short version:
|
||||
|
||||
1. New entry in `cmd_defs.c` with `CMD_PREAMBLE`/`CMD_MAINFUNC`/...
|
||||
2. Implement `cmd_<name>()` in `cmd_funcs.c` (declare in `cmd_funcs.h`).
|
||||
3. (If autocompletion needed) implement `_<name>_autocomplete()` in
|
||||
`cmd_ac.c` and register it via `g_hash_table_insert(ac_funcs, "/name", ...)`.
|
||||
4. Add a unit test pair `tests/unittests/test_cmd_<name>.{c,h}` and register
|
||||
tests in `tests/unittests/unittests.c`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- `cmd_<name>` for handlers; `_<name>_autocomplete` (file-static) for the
|
||||
autocomplete callback.
|
||||
- Subcommand dispatch via `CMD_SUBFUNCS` is preferred over `if/else` chains
|
||||
inside `cmd_<name>` when possible.
|
||||
- Keep handlers in `cmd_funcs.c` thin: parse & validate args, then delegate
|
||||
to a domain module (`xmpp/`, `config/`, `ui/`).
|
||||
71
patterns/encryption.md
Normal file
71
patterns/encryption.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Encryption
|
||||
|
||||
Three independent stacks, all optional at build time (`./configure` flags):
|
||||
|
||||
| Module | Backend | XMPP bridge |
|
||||
|---|---|---|
|
||||
| `src/omemo/` | libsignal-protocol-c | `src/xmpp/omemo.c` |
|
||||
| `src/otr/` | libotr v4 | (in-stream, no separate xmpp bridge) |
|
||||
| `src/pgp/` | gpgme | `src/xmpp/ox.c` (for OX/XEP-0373) |
|
||||
|
||||
## Module layouts
|
||||
|
||||
### OMEMO (`src/omemo/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `omemo.c/h` | Public surface: session setup, encrypt/decrypt, key publish/fetch. |
|
||||
| `crypto.c/h` | AES-GCM payload encryption; libsignal-protocol crypto provider. |
|
||||
| `store.c/h` | Persistent identity / pre-key / signed pre-key / session storage. |
|
||||
|
||||
`src/xmpp/omemo.c` is the bridge — it talks to `src/omemo/omemo.c` and to
|
||||
libstrophe stanza building.
|
||||
|
||||
### OTR (`src/otr/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `otr.c/h` | Public surface: start/end/secure messaging, fingerprint mgmt. |
|
||||
| `otrlib.h` | Compatibility shim across libotr versions. |
|
||||
| `otrlibv4.c` | libotr v4-specific implementation. |
|
||||
|
||||
OTR runs in-band on the message stream, no separate XMPP bridge file.
|
||||
|
||||
### PGP / OX (`src/pgp/`)
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `gpg.c/h` | Classic PGP signing/encrypting via gpgme. |
|
||||
| `ox.c/h` | XEP-0373 / XEP-0374 (OX): OpenPGP for XMPP. |
|
||||
|
||||
`src/xmpp/ox.c` is the OX bridge; `gpg.c` is invoked directly from message
|
||||
handlers.
|
||||
|
||||
## Conditional compilation
|
||||
|
||||
Each module is gated by `HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME` (set by
|
||||
`configure.ac`). Code that calls into a module guards with `#ifdef
|
||||
HAVE_<X>` and supplies a graceful fallback. When editing:
|
||||
|
||||
- Search for the existing `#ifdef` use of the symbol you are touching to
|
||||
confirm the gating macro.
|
||||
- Stubs in `tests/unittests/{omemo,otr,pgp}/stub_*.c` exist regardless — they
|
||||
satisfy the test linker even when the feature is "off" at runtime.
|
||||
|
||||
## Encryption-aware tests
|
||||
|
||||
`tests/unittests/test_forced_encryption.{c,h}` exercises the policy where
|
||||
sending plain text is refused if a session is OMEMO/OTR/PGP-locked. Touching
|
||||
encryption-policy code → check this test does not regress.
|
||||
|
||||
## Adding or modifying calls into encryption
|
||||
|
||||
1. Locate the bridge: outgoing call → typically `src/xmpp/omemo.c` /
|
||||
`src/xmpp/ox.c` / `src/otr/otr.c`.
|
||||
2. Confirm the `#ifdef HAVE_*` guard at the call site.
|
||||
3. If the module's public surface (header) changes, update the corresponding
|
||||
stub in `tests/unittests/{omemo,otr,pgp}/stub_*.c`.
|
||||
4. Run the relevant unit tests (`test_forced_encryption`, plus anything in
|
||||
`test_cmd_otr.c`, `test_cmd_pgp.c`).
|
||||
|
||||
See `playbooks/add-encryption.md`.
|
||||
59
patterns/events.md
Normal file
59
patterns/events.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Events
|
||||
|
||||
`src/event/` mediates between the XMPP protocol layer and consumers (UI,
|
||||
config, chatlog). Two main directions plus one common module.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `server_events.c/h` | `sv_ev_*` — events triggered by inbound stanzas. |
|
||||
| `client_events.c/h` | `cl_ev_*` — events triggered locally (UI / commands). |
|
||||
| `common.c/h` | Shared helpers used by both. |
|
||||
|
||||
## Server events (`sv_ev_*`)
|
||||
|
||||
Called from `src/xmpp/` parsers (`message.c`, `presence.c`, `iq.c`, `muc.c`,
|
||||
`roster.c`) once a stanza has been validated and decomposed. Each `sv_ev_*`:
|
||||
|
||||
1. Updates persistent state (chatlog, roster, accounts).
|
||||
2. Pushes UI updates (`chatwin_*`, `mucwin_*`, `cons_*`, `console_*`).
|
||||
3. May invoke plugin callbacks (`src/plugins/callbacks.c`).
|
||||
|
||||
Examples (current symbols, grep `sv_ev_` in `server_events.c`):
|
||||
- `sv_ev_incoming_message` — chat message arrived.
|
||||
- `sv_ev_room_message` — MUC message.
|
||||
- `sv_ev_presence_update` — presence change.
|
||||
- `sv_ev_roster_received` — roster pushed by server.
|
||||
|
||||
## Client events (`cl_ev_*`)
|
||||
|
||||
Called from `cmd_funcs.c` and UI code when the user does something locally
|
||||
that needs the same downstream fan-out as a server event.
|
||||
|
||||
Examples:
|
||||
- `cl_ev_send_msg` — outgoing chat message.
|
||||
- `cl_ev_send_presence` — presence update from `/status` or autoaway.
|
||||
- `cl_ev_disconnect` — `/disconnect` initiated locally.
|
||||
|
||||
## Adding an event handler
|
||||
|
||||
1. Decide direction: inbound (server) or outbound (client).
|
||||
2. Add `sv_ev_<name>` or `cl_ev_<name>` in the matching `.c` and declare in
|
||||
the matching `.h`.
|
||||
3. Call it from the trigger site (a parser in `src/xmpp/` for server events;
|
||||
a command handler or UI input handler for client events).
|
||||
4. Inside, perform the fan-out: state update → UI → plugin callback.
|
||||
5. If unit-tested, add a stub for it in `tests/unittests/event/stub_*.c`
|
||||
(or extend the existing stub).
|
||||
|
||||
See `playbooks/add-event-handler.md`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Keep XMPP-protocol concerns in `src/xmpp/`. The event module is a
|
||||
*dispatcher*, not a parser.
|
||||
- Keep UI rendering decisions in `src/ui/`. The event module *invokes* UI
|
||||
functions but does not draw.
|
||||
- Plugin callbacks are the **last** step — state must already be consistent
|
||||
before they run, since plugins may re-enter the codebase.
|
||||
65
patterns/memory.md
Normal file
65
patterns/memory.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Memory management
|
||||
|
||||
## `auto_*` macros
|
||||
|
||||
GCC/Clang `__cleanup__` attribute, declared in `src/common.h` (one exception:
|
||||
`auto_jid` lives in `src/xmpp/jid.h` because it needs the `Jid` type).
|
||||
|
||||
| Macro | Type | Cleanup | Use for |
|
||||
|---|---|---|---|
|
||||
| `auto_gchar` | `gchar*` | `auto_free_gchar()` | GLib strings (`g_strdup`, `g_strdup_printf`, etc.) |
|
||||
| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | GLib string arrays (`g_strsplit`, `g_strjoinv` arg) |
|
||||
| `auto_char` | `char*` | `auto_free_char()` | C strings (`strdup`, `malloc`'d) |
|
||||
| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Unsigned char buffers (e.g. `g_base64_decode`) |
|
||||
| `auto_gfd` | `gint` | `auto_close_gfd()` | File descriptors held in a `gint` |
|
||||
| `auto_FILE` | `FILE*` | `auto_close_FILE()` | `FILE*` from `fopen` |
|
||||
| `auto_jid` | `Jid*` | `jid_auto_destroy()` | `Jid` structs (XMPP) |
|
||||
|
||||
Place the macro **before** the type, like `gboolean`-style attribute:
|
||||
|
||||
```c
|
||||
auto_gchar gchar* msg = g_strdup_printf("hello %s", name);
|
||||
auto_gcharv gchar** parts = g_strsplit(line, " ", -1);
|
||||
```
|
||||
|
||||
The cleanup runs at scope exit, including early returns. Do **not** call the
|
||||
matching `g_free`/`g_strfreev`/`fclose` manually — that double-frees.
|
||||
|
||||
## Manual cleanup helpers
|
||||
|
||||
In `common.h`:
|
||||
|
||||
| Macro | Behaviour |
|
||||
|---|---|
|
||||
| `FREE_SET_NULL(ptr)` | `free(ptr); ptr = NULL;` (use for `malloc`'d) |
|
||||
| `GFREE_SET_NULL(ptr)` | `g_free(ptr); ptr = NULL;` (use for GLib-allocated) |
|
||||
|
||||
Use these when the pointer is a struct field that must remain accessible
|
||||
after the free (so a later `if (x->p)` is safe).
|
||||
|
||||
## GLib free-function reference
|
||||
|
||||
| Allocator | Free with |
|
||||
|---|---|
|
||||
| `g_strdup`, `g_strdup_printf`, `g_strconcat`, ... | `g_free` (or `auto_gchar`) |
|
||||
| `g_strsplit`, `g_strdupv` | `g_strfreev` (or `auto_gcharv`) |
|
||||
| `g_list_*` of allocated items | `g_list_free_full(list, free_fn)` |
|
||||
| `g_hash_table_new[_full]` | `g_hash_table_destroy` (uses key/value destroy fns if supplied) |
|
||||
| `g_base64_decode` | `g_free` (or `auto_guchar`) |
|
||||
| `g_key_file_*` | `g_key_file_free` |
|
||||
|
||||
**Common pitfall:** mixing `g_free` and `free`. GLib uses its own allocator
|
||||
shim; never cross the boundary. If you got the buffer from a GLib function,
|
||||
free it with the matching GLib function.
|
||||
|
||||
**Common pitfall:** `g_strsplit` returns `gchar**` — free with `g_strfreev`,
|
||||
not `g_free`. (See `gotchas.md`.)
|
||||
|
||||
## Adding a new `auto_*`
|
||||
|
||||
1. Declare cleanup function: `void auto_close_foo(Foo** p);`
|
||||
2. Define the macro: `#define auto_foo __attribute__((__cleanup__(auto_close_foo)))`
|
||||
3. Place both in the header that owns the type — `common.h` for project-wide,
|
||||
the type's own header otherwise.
|
||||
4. The cleanup must tolerate `NULL` and idempotent re-entry; assign `*p = NULL`
|
||||
inside if you keep the variable accessible after free.
|
||||
67
patterns/plugins.md
Normal file
67
patterns/plugins.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Plugins
|
||||
|
||||
`src/plugins/` exposes both a C plugin ABI and a Python plugin runtime. Both
|
||||
funnel through a shared core that fires callbacks on cproof events.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `plugins.c/h` | Public surface: load/unload, list, dispatch hooks. |
|
||||
| `c_plugins.c/h` | C plugin loader (dlopen). |
|
||||
| `python_plugins.c/h` | Python plugin loader (embedded interpreter). |
|
||||
| `c_api.c/h` | C-side API exposed **to** plugins. |
|
||||
| `python_api.c/h` | Python-side API exposed **to** plugins. |
|
||||
| `api.c/h` | Shared API helpers. |
|
||||
| `profapi.c/h` | The `prof` Python module surface. |
|
||||
| `callbacks.c/h` | Hooks: pre/post message, pre/post presence, etc. |
|
||||
| `autocompleters.c/h` | Plugin-registered autocompleters. |
|
||||
| `disco.c/h` | Service-discovery feature contributions from plugins. |
|
||||
| `settings.c/h` | Per-plugin settings storage. |
|
||||
| `themes.c/h` | Plugin-supplied themes. |
|
||||
|
||||
## Hook points
|
||||
|
||||
Plugins fire on events around (cf. `src/plugins/callbacks.c`):
|
||||
- Pre/post message send, pre/post message receive.
|
||||
- Pre/post chat-message-display.
|
||||
- Pre/post connect, disconnect.
|
||||
- Pre/post quit.
|
||||
- Window-switch, room-join, room-leave.
|
||||
|
||||
The callback names follow `prof_pre_*` / `prof_post_*` in the plugin API
|
||||
surface; in cproof internals they are dispatched via
|
||||
`plugins_pre_chat_message_send()` and similar.
|
||||
|
||||
## API surface for plugins
|
||||
|
||||
C API (`c_api.h`) and Python API (`python_api.h`) are kept in sync — any
|
||||
new function in one should land in the other unless explicitly C-only or
|
||||
Python-only.
|
||||
|
||||
The Python wrapper is in `profapi.c/h` and is exposed as the `prof` module
|
||||
inside plugins.
|
||||
|
||||
## Adding a plugin hook point
|
||||
|
||||
1. Define dispatcher in `plugins.c/h`:
|
||||
`void plugins_<verb>_<event>(<args>);`
|
||||
2. Wire callback storage in `callbacks.c/h`.
|
||||
3. Expose to plugins via `c_api.c/h` and `python_api.c/h` (matching names).
|
||||
4. Call the dispatcher at the appropriate site (usually in `src/event/` or
|
||||
`src/xmpp/`).
|
||||
5. Document the hook in cproof's plugin docs (out of scope for this repo).
|
||||
|
||||
## Testing
|
||||
|
||||
`tests/unittests/test_plugins_disco.{c,h}` covers plugin-disco feature
|
||||
contribution; broader plugin-system testing is light. Stubs:
|
||||
`tests/unittests/plugins/stub_*.c`.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Plugin callbacks may re-enter cproof via the C/Python API. Ensure state is
|
||||
consistent **before** firing a callback — see `patterns/events.md`.
|
||||
- The Python interpreter is global; plugins share state. Don't assume
|
||||
isolation.
|
||||
- Plugin loading is not sandboxed; treat plugins as trusted code.
|
||||
63
patterns/ui.md
Normal file
63
patterns/ui.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# UI
|
||||
|
||||
`src/ui/` is built on ncurses (wide-char). It owns windows, the input line,
|
||||
status/title bars, and tray notifications.
|
||||
|
||||
## Window types
|
||||
|
||||
| File | Window | Notes |
|
||||
|---|---|---|
|
||||
| `chatwin.c` | One-on-one chat | One per peer JID. |
|
||||
| `mucwin.c` | MUC room | Tracks occupants list, subject, role. |
|
||||
| `privwin.c` | MUC private chat | Per-occupant private window inside a room. |
|
||||
| `console.c` | Console | Always present, index 1; system messages. |
|
||||
| `confwin.c` | Form / config | Used for ad-hoc commands and data forms. |
|
||||
| `xmlwin.c` | XML console | Raw stanza tracer. |
|
||||
| `vcardwin.c` | vCard view | Read-only viewer. |
|
||||
|
||||
`win_types.h` declares the window-type enum used to discriminate at runtime.
|
||||
`window.c/h` is the base type; `window_list.c/h` manages the open-windows
|
||||
collection.
|
||||
|
||||
## Buffers
|
||||
|
||||
`buffer.c/h` — append-only ring of rendered lines per window. UI rendering
|
||||
reads from the buffer; chatlog persists separately.
|
||||
|
||||
## Bars and chrome
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `statusbar.c/h` | Bottom status line (window list, unread counts). |
|
||||
| `titlebar.c/h` | Top bar (current window title, encryption state). |
|
||||
| `tray.c/h` | System-tray notifications via libnotify. |
|
||||
| `screen.c/h` | Screen-level helpers (resize, refresh). |
|
||||
| `notifier.c` | Sound and visual notifications dispatch. |
|
||||
| `inputwin.c/h` | Input line: line editor, history, completion trigger. |
|
||||
| `core.c` | UI lifecycle (init / shutdown / refresh loop). |
|
||||
|
||||
## Roster panel
|
||||
|
||||
`rosterwin.c` and `occupantswin.c` render the side panels (roster and MUC
|
||||
occupants). They subscribe to roster / muc state changes via the event layer.
|
||||
|
||||
## Input → completion
|
||||
|
||||
`inputwin.c` calls into `cmd_ac.c` on Tab. See `patterns/autocomplete.md` for
|
||||
how that flow continues.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Public UI calls used from outside `src/ui/`:
|
||||
- `cons_show("...")` — formatted line into the console.
|
||||
- `cons_bad_cmd_usage(cmd)` — print canonical "wrong usage" line.
|
||||
- `chatwin_*`, `mucwin_*`, `privwin_*` — write into a specific window type.
|
||||
- UI code does not block on XMPP. State queries go through getters in
|
||||
`src/xmpp/` and `src/config/`.
|
||||
- Color / theme lookups go through `src/config/color.c` and
|
||||
`src/config/theme.c` — do not hardcode ncurses attribute pairs.
|
||||
|
||||
## Testing
|
||||
|
||||
UI is heavily stubbed in unit tests — see `tests/unittests/ui/stub_ui.c` and
|
||||
sibling stubs. Real ncurses calls do not run under `make check`.
|
||||
95
patterns/xmpp.md
Normal file
95
patterns/xmpp.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# XMPP layer
|
||||
|
||||
`src/xmpp/` wraps libstrophe and exposes higher-level operations to the
|
||||
command, event, and UI layers.
|
||||
|
||||
## Connection lifecycle
|
||||
|
||||
`src/xmpp/connection.c` owns the connection state machine. Public surface in
|
||||
`connection.h`:
|
||||
|
||||
- `jabber_conn_status_t connection_get_status(void);`
|
||||
- `jabber_conn_status_t connection_connect(const char* fulljid, const char* passwd, ...);`
|
||||
- `jabber_conn_status_t connection_register(...);`
|
||||
- `void connection_disconnect(void);`
|
||||
|
||||
`jabber_conn_status_t` values:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `JABBER_DISCONNECTED` | Not connected. |
|
||||
| `JABBER_CONNECTING` | Connect in progress. |
|
||||
| `JABBER_CONNECTED` | Live session. |
|
||||
| `JABBER_DISCONNECTING` | Tearing down. |
|
||||
|
||||
**Most code paths gate on `JABBER_CONNECTED`.** A canonical command-handler
|
||||
guard:
|
||||
|
||||
```c
|
||||
if (connection_get_status() != JABBER_CONNECTED) {
|
||||
cons_show("You are not currently connected.");
|
||||
return TRUE;
|
||||
}
|
||||
```
|
||||
|
||||
In tests, `will_return(connection_get_status, JABBER_CONNECTED)` (or
|
||||
`JABBER_DISCONNECTED`) before the call. See `gotchas.md`.
|
||||
|
||||
## Stanza building
|
||||
|
||||
`src/xmpp/stanza.c` is the assembler. Helpers like
|
||||
`stanza_create_message(...)`, `stanza_create_presence(...)`, plus low-level
|
||||
`xmpp_stanza_*` from libstrophe.
|
||||
|
||||
Outgoing flow:
|
||||
- `cmd_funcs.c` or `client_events.c` → `xmpp/message.c` (or `presence.c`,
|
||||
`iq.c`) → `stanza.c` to build → libstrophe to send.
|
||||
|
||||
Incoming flow:
|
||||
- libstrophe handler → `xmpp/message.c` (or sibling) parses → `event/server_events.c`
|
||||
fans out → `ui/`, `chatlog.c`, plugin callbacks.
|
||||
|
||||
## JIDs
|
||||
|
||||
`src/xmpp/jid.h` defines `Jid` and the `auto_jid` cleanup macro. Use
|
||||
`auto_jid` rather than manual `jid_destroy()` whenever practical:
|
||||
|
||||
```c
|
||||
auto_jid Jid* parsed = jid_create(input);
|
||||
if (!parsed || !parsed->barejid) {
|
||||
return FALSE;
|
||||
}
|
||||
```
|
||||
|
||||
`Jid` exposes `barejid`, `fulljid`, `localpart`, `domainpart`, `resourcepart`.
|
||||
|
||||
## Sessions, MUC, presence, roster
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `session.c` | Logical XMPP session: account, fulljid, presence, settings. |
|
||||
| `muc.c` | Multi-user chat rooms (joins, occupants, configuration). |
|
||||
| `presence.c` | Presence stanzas, subscription requests. |
|
||||
| `roster.c` / `roster_list.c` | Roster fetch, push, contact lookup. |
|
||||
| `chat_session.c` | Per-conversation state (carbons, OTR channel, etc.). |
|
||||
| `chat_state.c` | Composing / paused / inactive notifications (XEP-0085). |
|
||||
| `bookmark.c` | Bookmarked rooms (XEP-0048). |
|
||||
| `blocking.c` | Block list (XEP-0191). |
|
||||
| `vcard.c` / `vcard_funcs.h` | vCard fetch / publish. |
|
||||
| `avatar.c` | Avatar publish / fetch (XEP-0084). |
|
||||
| `iq.c` | Generic IQ handling and pending-IQ map. |
|
||||
| `capabilities.c` | Entity capabilities cache (XEP-0115). |
|
||||
|
||||
## Encryption bridges
|
||||
|
||||
`src/xmpp/omemo.c` and `src/xmpp/ox.c` are *bridges* into the encryption
|
||||
modules — they live alongside same-named files in `src/omemo/` and
|
||||
`src/pgp/`. Be precise about which path you mean.
|
||||
|
||||
## Testing
|
||||
|
||||
When unit-testing code that calls into `xmpp/`, expect to:
|
||||
|
||||
- Mock `connection_get_status` with `will_return(...)`.
|
||||
- Stub session getters (`session_get_account_name`) and roster lookups.
|
||||
- Use stubs in `tests/unittests/xmpp/stub_*.c`. See `testing/stubs.md`.
|
||||
Reference in New Issue
Block a user