From d3f892e9a7d0189e277956344ef5cef885f5e6ab Mon Sep 17 00:00:00 2001 From: deepend-tildeclub Date: Fri, 2 Oct 2026 14:14:24 -0600 Subject: [PATCH] add opt-in post-display print hooks --- docs/plugin-api-additions/post.md | 21 ++++++++++++++++ plugins/python/_zoitechat_post.py | 21 ++++++++++++++++ src/common/plugin.c | 40 +++++++++++++++++++++++++++++++ src/common/plugin.h | 1 + src/common/text.c | 1 + src/common/zoitechat-plugin.h | 7 ++++++ 6 files changed, 91 insertions(+) create mode 100644 docs/plugin-api-additions/post.md create mode 100644 plugins/python/_zoitechat_post.py diff --git a/docs/plugin-api-additions/post.md b/docs/plugin-api-additions/post.md new file mode 100644 index 00000000..5fbb8fd3 --- /dev/null +++ b/docs/plugin-api-additions/post.md @@ -0,0 +1,21 @@ +# Post-display print observers + +Python: `hook_print_after(name, callback, userdata=None, priority=PRI_NORM)`. +The callback is `(word, word_eol, userdata, attrs)`, matching hook_print_attrs(). +It runs after display_event() has handled the text, so prnt() follows the original +text. Return values are ignored. Events eaten by a pre-hook or hidden by client +preferences do not invoke these observers. This is post-display notification, +not a notification that the entire incoming IRC command has finished updating state. + +C: `zoitechat_hook_print_after(ph, name, priority, flags, callback, userdata)`; +flags must be zero. Check `get_info("api_hook_print_after")` for "1" before +accessing the appended Windows function-table slot. Existing pre-hook timing is +unchanged. Unhook/unload use existing lifecycle functions. Recursive emission +skips an already-running observer to prevent it immediately invoking itself. + +```python +def after(word, word_eol, userdata, attrs): + zoitechat.prnt('This follows the original channel message') + +zoitechat.hook_print_after('Channel Message', after) +``` diff --git a/plugins/python/_zoitechat_post.py b/plugins/python/_zoitechat_post.py new file mode 100644 index 00000000..4140adde --- /dev/null +++ b/plugins/python/_zoitechat_post.py @@ -0,0 +1,21 @@ +"""Opt-in post-display print observers.""" +import _zoitechat as api +from _zoitechat_embedded import ffi, lib + +__all__ = ['hook_print_after'] + + +def hook_print_after(name, callback, userdata=None, priority=api.PRI_NORM): + """callback(word, word_eol, userdata, attrs); its return value is ignored.""" + if not callable(callback): + raise TypeError('callback must be callable') + plugin = api.__get_current_plugin() + hook = plugin.add_hook(callback, userdata) + handle = lib.zoitechat_hook_print_after(lib.ph, name.encode(), priority, 0, + lib._on_print_attrs_hook, hook.handle) + if handle == ffi.NULL: + hook.is_unload = True + plugin.remove_hook(id(hook)) + raise RuntimeError('unable to install post-display hook') + hook.zoitechat_hook = handle + return id(hook) diff --git a/src/common/plugin.c b/src/common/plugin.c index 6a48a3d7..a681fb8e 100644 --- a/src/common/plugin.c +++ b/src/common/plugin.c @@ -143,6 +143,7 @@ enum HOOK_PRINT_ATTRS = 1 << 4, /* same as above, with attributes */ HOOK_TIMER = 1 << 5, /* timeouts */ HOOK_FD = 1 << 6, /* sockets & fds */ + HOOK_PRINT_AFTER = 1 << 8, /* opt-in post-display observers */ HOOK_DELETED = 1 << 7 /* marked for deletion */ }; @@ -351,6 +352,7 @@ plugin_add (session *sess, char *filename, void *handle, void *init_func, pl->zoitechat_emit_print_attrs = zoitechat_emit_print_attrs; pl->zoitechat_event_attrs_create = zoitechat_event_attrs_create; pl->zoitechat_event_attrs_free = zoitechat_event_attrs_free; + pl->zoitechat_hook_print_after = zoitechat_hook_print_after; /* run zoitechat_plugin_init, if it returns 0, close the plugin */ if (((zoitechat_init_func *)init_func) (pl, &pl->name, &pl->desc, &pl->version, arg) == 0) @@ -621,6 +623,13 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[], hook = list->data; next = list->next; + if (type == HOOK_PRINT_AFTER && hook->tag) + { + list = next; + continue; /* no recursive invocation of this same observer */ + } + if (type == HOOK_PRINT_AFTER) + hook->tag = 1; hook->pl->context = sess; /* run the plugin's callback function */ @@ -629,6 +638,7 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[], case HOOK_COMMAND: ret = ((zoitechat_cmd_cb *)hook->callback) (word, word_eol, hook->userdata); break; + case HOOK_PRINT_AFTER: case HOOK_PRINT_ATTRS: ret = ((zoitechat_print_attrs_cb *)hook->callback) (word, attrs, hook->userdata); break; @@ -643,6 +653,15 @@ plugin_hook_run (session *sess, char *name, char *word[], char *word_eol[], break; } + if (type == HOOK_PRINT_AFTER) + { + hook->tag = 0; + if (!is_session (sess)) + goto xit; + list = next; + continue; /* display already happened; observers cannot eat it */ + } + if ((ret & ZOITECHAT_EAT_ZOITECHAT) && (ret & ZOITECHAT_EAT_PLUGIN)) { eat = 1; @@ -725,6 +744,15 @@ plugin_emit_print (session *sess, char *word[], time_t server_time) HOOK_PRINT | HOOK_PRINT_ATTRS); } +void +plugin_emit_print_after (session *sess, char *word[], time_t server_time) +{ + zoitechat_event_attrs attrs; + attrs.server_time_utc = server_time; + if (is_session (sess)) + plugin_hook_run (sess, word[0], word, NULL, &attrs, HOOK_PRINT_AFTER); +} + int plugin_emit_dummy_print (session *sess, char *name) { @@ -1028,6 +1056,15 @@ zoitechat_unhook (zoitechat_plugin *ph, zoitechat_hook *hook) return hook->userdata; } +zoitechat_hook * +zoitechat_hook_print_after (zoitechat_plugin *ph, const char *name, int pri, + int flags, zoitechat_print_attrs_cb *callback, void *userdata) +{ + if (flags != 0 || !name || !callback) + return NULL; + return plugin_add_hook (ph, HOOK_PRINT_AFTER, pri, name, NULL, callback, 0, userdata); +} + zoitechat_hook * zoitechat_hook_command (zoitechat_plugin *ph, const char *name, int pri, zoitechat_cmd_cb *callb, const char *help_text, void *userdata) @@ -1197,6 +1234,9 @@ zoitechat_get_info (zoitechat_plugin *ph, const char *id) ? "ascii" : "rfc1459"; } + if (strcmp (id, "api_hook_print_after") == 0) + return "1"; + hash = str_hash (id); /* do the session independant ones first */ switch (hash) diff --git a/src/common/plugin.h b/src/common/plugin.h index d4ef3334..ae071349 100644 --- a/src/common/plugin.h +++ b/src/common/plugin.h @@ -183,6 +183,7 @@ int plugin_emit_command (session *sess, char *name, char *word[], char *word_eol int plugin_emit_server (session *sess, char *name, char *word[], char *word_eol[], time_t server_time); int plugin_emit_print (session *sess, char *word[], time_t server_time); +void plugin_emit_print_after (session *sess, char *word[], time_t server_time); int plugin_emit_dummy_print (session *sess, char *name); int plugin_emit_keypress (session *sess, unsigned int state, unsigned int keyval, gunichar key); GList* plugin_command_list(GList *tmp_list); diff --git a/src/common/text.c b/src/common/text.c index e22ceedf..8f6894e1 100644 --- a/src/common/text.c +++ b/src/common/text.c @@ -2306,6 +2306,7 @@ text_emit (int index, session *sess, char *a, char *b, char *c, char *d, if (!prefs.hex_away_omit_alerts || !sess->server->is_away) sound_play_event (index); display_event (sess, index, word, stripcolor_args, timestamp); + plugin_emit_print_after (sess, word, timestamp); } char * diff --git a/src/common/zoitechat-plugin.h b/src/common/zoitechat-plugin.h index e7863238..ba18ab80 100644 --- a/src/common/zoitechat-plugin.h +++ b/src/common/zoitechat-plugin.h @@ -206,6 +206,12 @@ struct _zoitechat_plugin #endif +/* api_hook_print_after=1: observer returns are ignored; flags must be zero. */ +zoitechat_hook *zoitechat_hook_print_after (zoitechat_plugin *ph, + const char *name, int pri, int flags, + int (*callback) (char *word[], zoitechat_event_attrs *attrs, void *userdata), + void *userdata); + zoitechat_hook * zoitechat_hook_command (zoitechat_plugin *ph, const char *name, @@ -425,6 +431,7 @@ zoitechat_pluginpref_list (zoitechat_plugin *ph, #define zoitechat_hook_server ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server) #define zoitechat_hook_server_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_server_attrs) #define zoitechat_hook_print ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print) +#define zoitechat_hook_print_after ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_after) #define zoitechat_hook_print_attrs ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_print_attrs) #define zoitechat_hook_timer ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_timer) #define zoitechat_hook_fd ((ZOITECHAT_PLUGIN_HANDLE)->zoitechat_hook_fd)