Coverage Report

Created: 2026-10-17 06:19

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.17 2026/10/15 08:21:11 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
      cmd_list_free(pr->cmdlist);
168
0
      break;
169
0
    }
170
0
  } else {
171
0
    a = options_array_first(o);
172
0
    while (a != NULL) {
173
0
      if (hd->expand) {
174
0
        value = options_array_item_value(a)->string;
175
0
        pr = hooks_parse(hd, &fs, value);
176
0
        switch (pr->status) {
177
0
        case CMD_PARSE_ERROR:
178
0
          if (pr->error != NULL) {
179
0
            cmdq_error(item, "%s",
180
0
                pr->error);
181
0
          }
182
0
          free(pr->error);
183
0
          break;
184
0
        case CMD_PARSE_SUCCESS:
185
0
          item = hooks_insert_one(item, hd,
186
0
              pr->cmdlist, state);
187
0
          cmd_list_free(pr->cmdlist);
188
0
          break;
189
0
        }
190
0
      } else {
191
0
        cmdlist = options_array_item_value(a)->cmdlist;
192
0
        item = hooks_insert_one(item, hd, cmdlist,
193
0
            state);
194
0
      }
195
0
      a = options_array_next(a);
196
0
    }
197
0
  }
198
199
0
  cmdq_free_state(state);
200
0
}
201
202
/* Insert commands for a hook event. */
203
static void
204
hooks_insert_event(struct cmdq_item *item, const char *name,
205
    struct event_payload *ep, struct options *oo, int expand)
206
0
{
207
0
  struct hooks_data  hd;
208
0
  struct format_tree  *ft;
209
0
  struct client   *c;
210
211
0
  if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS))
212
0
    return;
213
214
0
  c = event_payload_get_client(ep, "client");
215
0
  ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS);
216
0
  event_payload_add_formats(ep, ft, "hook_");
217
0
  format_add(ft, "hook", "%s", name);
218
0
  format_log_debug(ft, __func__);
219
220
0
  memset(&hd, 0, sizeof hd);
221
0
  hd.name = name;
222
0
  cmd_find_clear_state(&hd.fs, 0);
223
0
  event_payload_get_target(ep, &hd.fs);
224
0
  hd.formats = ft;
225
0
  hd.oo = oo;
226
0
  hd.client = c;
227
0
  hd.expand = expand;
228
229
0
  hooks_insert(item, &hd);
230
0
  format_free(ft);
231
0
}
232
233
/* Handle an event for hooks. */
234
static void
235
hooks_event_cb(const char *name, struct event_payload *ep,
236
    __unused void *sink_data)
237
0
{
238
0
  struct cmdq_item  *item;
239
240
0
  if (event_payload_get_pointer(ep, "_hooks_monitor") != NULL)
241
0
    return;
242
243
0
  item = event_payload_get_pointer(ep, "_cmdq_item");
244
0
  if (item != NULL) {
245
0
    hooks_insert_event(item, name, ep, NULL, 0);
246
0
    return;
247
0
  }
248
249
0
  item = cmdq_running(NULL);
250
0
  if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS))
251
0
    hooks_insert_event(NULL, name, ep, NULL, 0);
252
0
}
253
254
/* Add a hook event sink. */
255
void
256
hooks_add_event(const char *name)
257
0
{
258
0
  struct hooks_event  *he;
259
260
0
  TAILQ_FOREACH(he, &hooks_events, entry) {
261
0
    if (strcmp(he->name, name) == 0)
262
0
      return;
263
0
  }
264
265
0
  he = xcalloc(1, sizeof *he);
266
0
  he->name = xstrdup(name);
267
0
  he->sink = events_add_sink(name, hooks_event_cb, NULL);
268
0
  TAILQ_INSERT_TAIL(&hooks_events, he, entry);
269
0
}
270
271
/* Return if a hook event sink exists. */
272
int
273
hooks_is_event(const char *name)
274
0
{
275
0
  struct hooks_event  *he;
276
277
0
  TAILQ_FOREACH(he, &hooks_events, entry) {
278
0
    if (strcmp(he->name, name) == 0)
279
0
      return (1);
280
0
  }
281
0
  return (0);
282
0
}
283
284
/* Return if an event name can be fired through the hooks path. */
285
int
286
hooks_valid_event_name(const char *name)
287
0
{
288
0
  const struct options_table_entry  *oe;
289
290
0
  if (*name == '@')
291
0
    return (1);
292
0
  oe = options_search(name);
293
0
  return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK));
294
0
}
295
296
/* Add hook event sinks for all built-in hooks. */
297
void
298
hooks_build_events(void)
299
0
{
300
0
  const struct options_table_entry  *oe;
301
302
0
  for (oe = options_table; oe->name != NULL; oe++) {
303
0
    if (oe->flags & OPTIONS_TABLE_IS_HOOK)
304
0
      hooks_add_event(oe->name);
305
0
  }
306
0
}
307
308
/* Run a hook immediately. */
309
void
310
hooks_run(struct cmdq_item *item, const char *name)
311
0
{
312
0
  struct cmd_find_state *target = cmdq_get_target(item);
313
0
  struct hooks_data  hd = { 0 };
314
315
0
  hd.name = name;
316
0
  cmd_find_copy_state(&hd.fs, target);
317
0
  hd.client = cmdq_get_client(item);
318
319
0
  hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS);
320
0
  format_add(hd.formats, "hook", "%s", name);
321
0
  format_log_debug(hd.formats, __func__);
322
323
0
  hooks_insert(item, &hd);
324
0
  format_free(hd.formats);
325
0
}
326
327
/* Free a hook monitor. */
328
void
329
hooks_monitor_free(void *data)
330
0
{
331
0
  struct hooks_monitor  *hm = data;
332
333
0
  events_remove_sink(hm->sink);
334
0
  monitor_destroy(hm->set);
335
0
  free(hm->format);
336
0
  free(hm);
337
0
}
338
339
/* Remove a hook monitor. */
340
void
341
hooks_monitor_remove(struct options *oo, const char *name)
342
0
{
343
0
  struct options_entry  *o;
344
0
  struct hooks_monitor  *hm;
345
346
0
  o = options_get_only(oo, name);
347
0
  if (o == NULL)
348
0
    return;
349
350
0
  hm = options_get_monitor_data(o);
351
0
  if (hm != NULL) {
352
0
    options_set_monitor_data(o, NULL);
353
0
    hooks_monitor_free(hm);
354
0
  }
355
0
}
356
357
/* Handle a hook monitor event. */
358
static void
359
hooks_monitor_hook_cb(const char *name, struct event_payload *ep,
360
    void *sink_data)
361
0
{
362
0
  struct hooks_monitor  *hm = sink_data;
363
364
0
  if (event_payload_get_pointer(ep, "_hooks_monitor") == hm)
365
0
    hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1);
366
0
}
367
368
/* Fire a hook monitor event. */
369
static void
370
hooks_monitor_cb(struct monitor_change *change, void *data)
371
0
{
372
0
  struct hooks_monitor  *hm = data;
373
0
  struct event_payload  *ep;
374
0
  struct winlink    *wl = change->wl;
375
0
  struct window_pane  *wp = change->wp;
376
0
  struct cmd_find_state  fs;
377
378
0
  ep = event_payload_create();
379
0
  event_payload_set_pointer(ep, "_hooks_monitor", data, NULL, NULL);
380
381
0
  cmd_find_clear_state(&fs, 0);
382
0
  if (wl != NULL && wp != NULL && wp->window == wl->window)
383
0
    cmd_find_from_winlink_pane(&fs, wl, wp, 0);
384
0
  else if (wl != NULL)
385
0
    cmd_find_from_winlink(&fs, wl, 0);
386
0
  else if (wp != NULL)
387
0
    cmd_find_from_pane(&fs, wp, 0);
388
0
  else if (change->s != NULL)
389
0
    cmd_find_from_session(&fs, change->s, 0);
390
0
  else
391
0
    cmd_find_copy_state(&fs, &hm->fs);
392
0
  event_payload_set_target(ep, &fs);
393
394
0
  if (change->value != NULL)
395
0
    event_payload_set_string(ep, "value", "%s", change->value);
396
0
  else
397
0
    event_payload_set_string(ep, "value", "%s", "");
398
0
  if (change->last != NULL)
399
0
    event_payload_set_string(ep, "last", "%s", change->last);
400
0
  else
401
0
    event_payload_set_string(ep, "last", "%s", "");
402
403
0
  if (change->c != NULL)
404
0
    event_payload_set_client(ep, "client", change->c);
405
0
  if (change->s != NULL)
406
0
    event_payload_set_session(ep, "session", change->s);
407
0
  if (wl != NULL) {
408
0
    if (change->s == NULL)
409
0
      event_payload_set_session(ep, "session", wl->session);
410
0
    event_payload_set_window(ep, "window", wl->window);
411
0
    event_payload_set_int(ep, "window_index", wl->idx);
412
0
  }
413
0
  if (wp != NULL) {
414
0
    event_payload_set_pane(ep, "pane", wp);
415
0
    if (wl == NULL)
416
0
      event_payload_set_window(ep, "window", wp->window);
417
0
  }
418
419
0
  events_fire(change->name, ep);
420
0
}
421
422
/* Add a hook monitor. */
423
void
424
hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo,
425
    const char *name, enum monitor_type type, int id, const char *format,
426
    int flags, struct cmd_find_state *fs, struct session *s)
427
0
{
428
0
  struct options_entry  *o;
429
0
  struct hooks_monitor  *hm;
430
431
0
  hooks_monitor_remove(oo, name);
432
0
  o = options_get_only(oo, name);
433
0
  if (o == NULL)
434
0
    o = options_set_string(oo, name, 0, "%s", "");
435
436
0
  hm = xcalloc(1, sizeof *hm);
437
0
  hm->oo = oo;
438
0
  cmd_find_copy_state(&hm->fs, fs);
439
0
  hm->type = type;
440
0
  hm->id = id;
441
0
  hm->format = xstrdup(format);
442
0
  hm->set = monitor_create_session(s, hooks_monitor_cb, hm);
443
0
  hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm);
444
0
  options_set_monitor_data(o, hm);
445
0
  monitor_add(hm->set, name, type, id, format, flags);
446
0
}
447
448
/* Convert a hook monitor to its value. */
449
char *
450
hooks_monitor_to_string(struct options_entry *o)
451
0
{
452
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
453
0
  const char    *name = options_name(o);
454
0
  char      *s;
455
456
0
  if (hm == NULL)
457
0
    return (NULL);
458
459
0
  switch (hm->type) {
460
0
  case MONITOR_SESSION:
461
0
    xasprintf(&s, "%s::%s", name, hm->format);
462
0
    break;
463
0
  case MONITOR_PANE:
464
0
    xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format);
465
0
    break;
466
0
  case MONITOR_ALL_PANES:
467
0
    xasprintf(&s, "%s:%%*:%s", name, hm->format);
468
0
    break;
469
0
  case MONITOR_WINDOW:
470
0
    xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format);
471
0
    break;
472
0
  case MONITOR_ALL_WINDOWS:
473
0
    xasprintf(&s, "%s:@*:%s", name, hm->format);
474
0
    break;
475
0
  }
476
0
  return (s);
477
0
}
478
479
/* Get the parts of a hook monitor. */
480
int
481
hooks_monitor_get(struct options_entry *o, enum monitor_type *type, int *id,
482
    const char **format)
483
0
{
484
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
485
486
0
  if (hm == NULL)
487
0
    return (0);
488
0
  *type = hm->type;
489
0
  *id = hm->id;
490
0
  *format = hm->format;
491
0
  return (1);
492
0
}
493
494
/* Get hook monitor firing count. */
495
u_int
496
hooks_monitor_get_fire_count(struct options_entry *o)
497
0
{
498
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
499
500
0
  if (hm == NULL)
501
0
    return (0);
502
0
  return (monitor_get_fire_count(hm->set, options_name(o)));
503
0
}
504
505
/* Get hook monitor firing time. */
506
time_t
507
hooks_monitor_get_fire_time(struct options_entry *o)
508
0
{
509
0
  struct hooks_monitor  *hm = options_get_monitor_data(o);
510
511
0
  if (hm == NULL)
512
0
    return (0);
513
0
  return (monitor_get_fire_time(hm->set, options_name(o)));
514
0
}