Coverage Report

Created: 2026-08-13 06:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/hooks.c
Line
Count
Source
1
/* $OpenBSD: hooks.c,v 1.16 2026/07/27 19:15:58 nicm Exp $ */
2
3
/*
4
 * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
5
 * Copyright (c) 2012 George Nachman <tmux@georgester.com>
6
 *
7
 * Permission to use, copy, modify, and distribute this software for any
8
 * purpose with or without fee is hereby granted, provided that the above
9
 * copyright notice and this permission notice appear in all copies.
10
 *
11
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
16
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
17
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
 */
19
20
#include <sys/types.h>
21
22
#include <stdlib.h>
23
#include <string.h>
24
25
#include "tmux.h"
26
27
/* Hook monitor state owned by an option entry. */
28
struct hooks_monitor {
29
  struct options    *oo;
30
31
  struct monitor_set  *set;
32
  struct events_sink  *sink;
33
  struct cmd_find_state  fs;
34
35
  enum monitor_type  type;
36
  int      id;
37
  char      *format;
38
};
39
40
/* Hook command data built from an event payload. */
41
struct hooks_data {
42
  const char    *name;
43
  struct cmd_find_state  fs;
44
  struct format_tree  *formats;
45
  struct options    *oo;
46
  struct client   *client;
47
  int      expand;
48
};
49
50
/* Hook event sink registered for a notify event name. */
51
struct hooks_event {
52
  char      *name;
53
  struct events_sink  *sink;
54
  TAILQ_ENTRY(hooks_event) entry;
55
};
56
TAILQ_HEAD(hooks_events, hooks_event);
57
static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events);
58
59
/* Insert one hook command list. */
60
static struct cmdq_item *
61
hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd,
62
    struct cmd_list *cmdlist, struct cmdq_state *state)
63
0
{
64
0
  struct cmdq_item  *new_item;
65
0
  char      *s;
66
67
0
  if (cmdlist == NULL)
68
0
    return (item);
69
0
  if (log_get_level() != 0) {
70
0
    s = cmd_list_print(cmdlist, 0);
71
0
    log_debug("%s: hook %s is: %s", __func__, hd->name, s);
72
0
    free(s);
73
0
  }
74
0
  new_item = cmdq_get_command(cmdlist, state);
75
0
  if (item != NULL)
76
0
    return (cmdq_insert_after(item, new_item));
77
0
  return (cmdq_append(NULL, new_item));
78
0
}
79
80
/* Parse a hook command. */
81
static struct cmd_parse_result *
82
hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs,
83
    const char *value)
84
0
{
85
0
  struct cmd_parse_result *pr;
86
0
  struct format_tree  *ft;
87
0
  char      *expanded;
88
89
0
  if (!hd->expand)
90
0
    return (cmd_parse_from_string(value, NULL));
91
92
0
  ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp);
93
0
  if (hd->formats != NULL)
94
0
    format_merge(ft, hd->formats);
95
0
  expanded = format_expand(ft, value);
96
0
  format_free(ft);
97
98
0
  pr = cmd_parse_from_string(expanded, NULL);
99
0
  free(expanded);
100
0
  return (pr);
101
0
}
102
103
/* Insert commands for a hook. */
104
static void
105
hooks_insert(struct cmdq_item *item, struct hooks_data *hd)
106
0
{
107
0
  struct cmd_find_state    fs;
108
0
  struct options      *oo;
109
0
  struct cmdq_state   *state;
110
0
  struct options_entry    *o;
111
0
  struct options_array_item *a;
112
0
  struct cmd_list     *cmdlist;
113
0
  const char      *value;
114
0
  struct cmd_parse_result   *pr;
115
116
0
  log_debug("%s: inserting hook %s", __func__, hd->name);
117
118
0
  cmd_find_clear_state(&fs, 0);
119
0
  if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs))
120
0
    cmd_find_from_nothing(&fs, 0);
121
0
  else
122
0
    cmd_find_copy_state(&fs, &hd->fs);
123
124
0
  if (hd->oo != NULL) {
125
0
    oo = hd->oo;
126
0
    o = options_get_only(oo, hd->name);
127
0
  } else {
128
0
    if (fs.s == NULL)
129
0
      oo = global_s_options;
130
0
    else
131
0
      oo = fs.s->options;
132
0
    o = options_get(oo, hd->name);
133
0
    if (o == NULL && fs.wp != NULL) {
134
0
      oo = fs.wp->options;
135
0
      o = options_get(oo, hd->name);
136
0
    }
137
0
    if (o == NULL && fs.wl != NULL) {
138
0
      oo = fs.wl->window->options;
139
0
      o = options_get(oo, hd->name);
140
0
    }
141
0
  }
142
0
  if (o == NULL) {
143
0
    log_debug("%s: hook %s not found", __func__, hd->name);
144
0
    return;
145
0
  }
146
0
  options_hook_fired(o);
147
148
0
  if (item == NULL)
149
0
    state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS);
150
0
  else {
151
0
    state = cmdq_new_state(&fs, cmdq_get_event(item),
152
0
        CMDQ_STATE_NOHOOKS);
153
0
  }
154
0
  cmdq_add_formats(state, hd->formats);
155
156
0
  if (*hd->name == '@') {
157
0
    value = options_get_string(oo, hd->name);
158
0
    pr = hooks_parse(hd, &fs, value);
159
0
    switch (pr->status) {
160
0
    case CMD_PARSE_ERROR:
161
0
      log_debug("%s: can't parse hook %s: %s", __func__,
162
0
          hd->name, pr->error);
163
0
      free(pr->error);
164
0
      break;
165
0
    case CMD_PARSE_SUCCESS:
166
0
      hooks_insert_one(item, hd, pr->cmdlist, state);
167
0
      break;
168
0
    }
169
0
  } else {
170
0
    a = options_array_first(o);
171
0
    while (a != NULL) {
172
0
      if (hd->expand) {
173
0
        value = options_array_item_value(a)->string;
174
0
        pr = hooks_parse(hd, &fs, value);
175
0
        switch (pr->status) {
176
0
        case CMD_PARSE_ERROR:
177
0
          if (pr->error != NULL) {
178
0
            cmdq_error(item, "%s",
179
0
                pr->error);
180
0
          }
181
0
          break;
182
0
        case CMD_PARSE_SUCCESS:
183
0
          item = hooks_insert_one(item, hd,
184
0
              pr->cmdlist, state);
185
0
          break;
186
0
        }
187
0
      } else {
188
0
        cmdlist = options_array_item_value(a)->cmdlist;
189
0
        item = hooks_insert_one(item, hd, cmdlist,
190
0
            state);
191
0
      }
192
0
      a = options_array_next(a);
193
0
    }
194
0
  }
195
196
0
  cmdq_free_state(state);
197
0
}
198
199
/* Insert commands for a hook event. */
200
static void
201
hooks_insert_event(struct cmdq_item *item, const char *name,
202
    struct event_payload *ep, struct options *oo, int expand)
203
0
{
204
0
  struct hooks_data  hd;
205
0
  struct format_tree  *ft;
206
0
  struct client   *c;
207
208
0
  if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS))
209
0
    return;
210
211
0
  c = event_payload_get_client(ep, "client");
212
0
  ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS);
213
0
  event_payload_add_formats(ep, ft, "hook_");
214
0
  format_add(ft, "hook", "%s", name);
215
0
  format_log_debug(ft, __func__);
216
217
0
  memset(&hd, 0, sizeof hd);
218
0
  hd.name = name;
219
0
  cmd_find_clear_state(&hd.fs, 0);
220
0
  event_payload_get_target(ep, &hd.fs);
221
0
  hd.formats = ft;
222
0
  hd.oo = oo;
223
0
  hd.client = c;
224
0
  hd.expand = expand;
225
226
0
  hooks_insert(item, &hd);
227
0
  format_free(ft);
228
0
}
229
230
/* Handle an event for hooks. */
231
static void
232
hooks_event_cb(const char *name, struct event_payload *ep,
233
    __unused void *sink_data)
234
0
{
235
0
  struct cmdq_item  *item;
236
237
0
  if (event_payload_get_pointer(ep, "_hooks_monitor") != NULL)
238
0
    return;
239
240
0
  item = event_payload_get_pointer(ep, "_cmdq_item");
241
0
  if (item != NULL) {
242
0
    hooks_insert_event(item, name, ep, NULL, 0);
243
0
    return;
244
0
  }
245
246
0
  item = cmdq_running(NULL);
247
0
  if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS))
248
0
    hooks_insert_event(NULL, name, ep, NULL, 0);
249
0
}
250
251
/* Add a hook event sink. */
252
void
253
hooks_add_event(const char *name)
254
0
{
255
0
  struct hooks_event  *he;
256
257
0
  TAILQ_FOREACH(he, &hooks_events, entry) {
258
0
    if (strcmp(he->name, name) == 0)
259
0
      return;
260
0
  }
261
262
0
  he = xcalloc(1, sizeof *he);
263
0
  he->name = xstrdup(name);
264
0
  he->sink = events_add_sink(name, hooks_event_cb, NULL);
265
0
  TAILQ_INSERT_TAIL(&hooks_events, he, entry);
266
0
}
267
268
/* Return if a hook event sink exists. */
269
int
270
hooks_is_event(const char *name)
271
0
{
272
0
  struct hooks_event  *he;
273
274
0
  TAILQ_FOREACH(he, &hooks_events, entry) {
275
0
    if (strcmp(he->name, name) == 0)
276
0
      return (1);
277
0
  }
278
0
  return (0);
279
0
}
280
281
/* Return if an event name can be fired through the hooks path. */
282
int
283
hooks_valid_event_name(const char *name)
284
0
{
285
0
  const struct options_table_entry  *oe;
286
287
0
  if (*name == '@')
288
0
    return (1);
289
0
  oe = options_search(name);
290
0
  return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK));
291
0
}
292
293
/* Add hook event sinks for all built-in hooks. */
294
void
295
hooks_build_events(void)
296
0
{
297
0
  const struct options_table_entry  *oe;
298
299
0
  for (oe = options_table; oe->name != NULL; oe++) {
300
0
    if (oe->flags & OPTIONS_TABLE_IS_HOOK)
301
0
      hooks_add_event(oe->name);
302
0
  }
303
0
}
304
305
/* Run a hook immediately. */
306
void
307
hooks_run(struct cmdq_item *item, const char *name)
308
0
{
309
0
  struct cmd_find_state *target = cmdq_get_target(item);
310
0
  struct hooks_data  hd = { 0 };
311
312
0
  hd.name = name;
313
0
  cmd_find_copy_state(&hd.fs, target);
314
0
  hd.client = cmdq_get_client(item);
315
316
0
  hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS);
317
0
  format_add(hd.formats, "hook", "%s", name);
318
0
  format_log_debug(hd.formats, __func__);
319
320
0
  hooks_insert(item, &hd);
321
0
  format_free(hd.formats);
322
0
}
323
324
/* Free a hook monitor. */
325
void
326
hooks_monitor_free(void *data)
327
0
{
328
0
  struct hooks_monitor  *hm = data;
329
330
0
  events_remove_sink(hm->sink);
331
0
  monitor_destroy(hm->set);
332
0
  free(hm->format);
333
0
  free(hm);
334
0
}
335
336
/* Remove a hook monitor. */
337
void
338
hooks_monitor_remove(struct options *oo, const char *name)
339
0
{
340
0
  struct options_entry  *o;
341
0
  struct hooks_monitor  *hm;
342
343
0
  o = options_get_only(oo, name);
344
0
  if (o == NULL)
345
0
    return;
346
347
0
  hm = options_get_monitor_data(o);
348
0
  if (hm != NULL) {
349
0
    options_set_monitor_data(o, NULL);
350
0
    hooks_monitor_free(hm);
351
0
  }
352
0
}
353
354
/* Handle a hook monitor event. */
355
static void
356
hooks_monitor_hook_cb(const char *name, struct event_payload *ep,
357
    void *sink_data)
358
0
{
359
0
  struct hooks_monitor  *hm = sink_data;
360
361
0
  if (event_payload_get_pointer(ep, "_hooks_monitor") == hm)
362
0
    hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1);
363
0
}
364
365
/* Fire a hook monitor event. */
366
static void
367
hooks_monitor_cb(struct monitor_change *change, void *data)
368
0
{
369
0
  struct hooks_monitor  *hm = data;
370
0
  struct event_payload  *ep;
371
0
  struct winlink    *wl = change->wl;
372
0
  struct window_pane  *wp = change->wp;
373
0
  struct cmd_find_state  fs;
374
375
0
  ep = event_payload_create();
376
0
  event_payload_set_pointer(ep, "_hooks_monitor", data, NULL, NULL);
377
378
0
  cmd_find_clear_state(&fs, 0);
379
0
  if (wl != NULL && wp != NULL && wp->window == wl->window)
380
0
    cmd_find_from_winlink_pane(&fs, wl, wp, 0);
381
0
  else if (wl != NULL)
382
0
    cmd_find_from_winlink(&fs, wl, 0);
383
0
  else if (wp != NULL)
384
0
    cmd_find_from_pane(&fs, wp, 0);
385
0
  else if (change->s != NULL)
386
0
    cmd_find_from_session(&fs, change->s, 0);
387
0
  else
388
0
    cmd_find_copy_state(&fs, &hm->fs);
389
0
  event_payload_set_target(ep, &fs);
390
391
0
  if (change->value != NULL)
392
0
    event_payload_set_string(ep, "value", "%s", change->value);
393
0
  else
394
0
    event_payload_set_string(ep, "value", "%s", "");
395
0
  if (change->last != NULL)
396
0
    event_payload_set_string(ep, "last", "%s", change->last);
397
0
  else
398
0
    event_payload_set_string(ep, "last", "%s", "");
399
400
0
  if (change->c != NULL)
401
0
    event_payload_set_client(ep, "client", change->c);
402
0
  if (change->s != NULL)
403
0
    event_payload_set_session(ep, "session", change->s);
404
0
  if (wl != NULL) {
405
0
    if (change->s == NULL)
406
0
      event_payload_set_session(ep, "session", wl->session);
407
0
    event_payload_set_window(ep, "window", wl->window);
408
0
    event_payload_set_int(ep, "window_index", wl->idx);
409
0
  }
410
0
  if (wp != NULL) {
411
0
    event_payload_set_pane(ep, "pane", wp);
412
0
    if (wl == NULL)
413
0
      event_payload_set_window(ep, "window", wp->window);
414
0
  }
415
416
0
  events_fire(change->name, ep);
417
0
}
418
419
/* Add a hook monitor. */
420
void
421
hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo,
422
    const char *name, enum monitor_type type, int id, const char *format,
423
    int flags, struct cmd_find_state *fs, struct session *s)
424
0
{
425
0
  struct options_entry  *o;
426
0
  struct hooks_monitor  *hm;
427
428
0
  hooks_monitor_remove(oo, name);
429
0
  o = options_get_only(oo, name);
430
0
  if (o == NULL)
431
0
    o = options_set_string(oo, name, 0, "%s", "");
432
433
0
  hm = xcalloc(1, sizeof *hm);
434
0
  hm->oo = oo;
435
0
  cmd_find_copy_state(&hm->fs, fs);
436
0
  hm->type = type;
437
0
  hm->id = id;
438
0
  hm->format = xstrdup(format);
439
0
  hm->set = monitor_create_session(s, hooks_monitor_cb, hm);
440
0
  hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm);
441
0
  options_set_monitor_data(o, hm);
442
0
  monitor_add(hm->set, name, type, id, format, flags);
443
0
}
444
445
/* Convert a hook monitor to its value. */
446
char *
447
hooks_monitor_to_string(struct options_entry *o)
448
0
{
449
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
450
0
  const char    *name = options_name(o);
451
0
  char      *s;
452
453
0
  if (hm == NULL)
454
0
    return (NULL);
455
456
0
  switch (hm->type) {
457
0
  case MONITOR_SESSION:
458
0
    xasprintf(&s, "%s::%s", name, hm->format);
459
0
    break;
460
0
  case MONITOR_PANE:
461
0
    xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format);
462
0
    break;
463
0
  case MONITOR_ALL_PANES:
464
0
    xasprintf(&s, "%s:%%*:%s", name, hm->format);
465
0
    break;
466
0
  case MONITOR_WINDOW:
467
0
    xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format);
468
0
    break;
469
0
  case MONITOR_ALL_WINDOWS:
470
0
    xasprintf(&s, "%s:@*:%s", name, hm->format);
471
0
    break;
472
0
  }
473
0
  return (s);
474
0
}
475
476
/* Get the parts of a hook monitor. */
477
int
478
hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id,
479
    const char **format)
480
0
{
481
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
482
483
0
  if (hm == NULL)
484
0
    return (0);
485
0
  *type = hm->type;
486
0
  *id = hm->id;
487
0
  *format = hm->format;
488
0
  return (1);
489
0
}
490
491
/* Get hook monitor firing count. */
492
u_int
493
hooks_monitor_get_fire_count(struct options_entry *o)
494
0
{
495
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
496
497
0
  if (hm == NULL)
498
0
    return (0);
499
0
  return (monitor_get_fire_count(hm->set, options_name(o)));
500
0
}
501
502
/* Get hook monitor firing time. */
503
time_t
504
hooks_monitor_get_fire_time(struct options_entry *o)
505
0
{
506
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
507
508
0
  if (hm == NULL)
509
0
    return (0);
510
0
  return (monitor_get_fire_time(hm->set, options_name(o)));
511
0
}