Coverage Report

Created: 2026-10-17 06:19

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/control-notify.c
Line
Count
Source
1
/* $OpenBSD: control-notify.c,v 1.39 2026/09/08 10:20:08 nicm Exp $ */
2
3
/*
4
 * Copyright (c) 2012 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
24
#include "tmux.h"
25
26
/* Should this client be sent events? */
27
#define CONTROL_SHOULD_NOTIFY_CLIENT(c) \
28
0
  ((c) != NULL && \
29
0
   ((c)->flags & CLIENT_CONTROL) && \
30
0
   (~(c)->flags & CLIENT_EXIT) && \
31
0
   (c)->control_state != NULL)
32
33
/* Notify control clients that pane mode changed. */
34
static void
35
control_pane_mode_changed_cb(__unused const char *name,
36
    struct event_payload *ep, __unused void *sink_data)
37
0
{
38
0
  struct window_pane  *wp;
39
0
  struct client   *c;
40
0
  char      *value;
41
42
0
  wp = event_payload_get_pane(ep, "pane");
43
0
  if (wp != NULL) {
44
0
    TAILQ_FOREACH(c, &clients, entry) {
45
0
      if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
46
0
        continue;
47
0
      control_notify_write(c, "%%pane-mode-changed %%%u",
48
0
          wp->id);
49
0
    }
50
0
    return;
51
0
  }
52
53
0
  value = event_payload_print(ep, "pane");
54
0
  if (value == NULL)
55
0
    return;
56
0
  TAILQ_FOREACH(c, &clients, entry) {
57
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
58
0
      continue;
59
0
    control_notify_write(c, "%%pane-mode-changed %s", value);
60
0
  }
61
0
  free(value);
62
0
}
63
64
/* Notify control clients that window layout changed. */
65
static void
66
control_window_layout_changed_cb(__unused const char *name,
67
    struct event_payload *ep, __unused void *sink_data)
68
0
{
69
0
  struct client   *c;
70
0
  struct session    *s;
71
0
  struct winlink    *wl;
72
0
  struct window   *w = event_payload_get_window(ep, "window");
73
0
  struct format_tree  *ft;
74
0
  const char    *template;
75
0
  char      *cp;
76
77
0
  if (w == NULL)
78
0
    return;
79
80
0
  template = "%layout-change #{window_id} #{window_layout} "
81
0
      "#{window_visible_layout} #{window_raw_flags}";
82
83
  /*
84
   * When the last pane in a window is closed it won't have a layout root
85
   * and we don't need to inform the client about the layout change
86
   * because the whole window will go away soon.
87
   */
88
0
  if (TAILQ_FIRST(&w->winlinks) == NULL || w->layout_root == NULL)
89
0
    return;
90
91
0
  TAILQ_FOREACH(c, &clients, entry) {
92
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
93
0
      continue;
94
0
    s = c->session;
95
0
    wl = winlink_find_by_window_id(&s->windows, w->id);
96
0
    if (wl == NULL)
97
0
      continue;
98
99
0
    ft = format_create(c, NULL, FORMAT_NONE, 0);
100
0
    format_defaults(ft, c, s, wl, NULL);
101
0
    cp = format_expand(ft, template);
102
0
    format_free(ft);
103
104
0
    control_notify_write(c, "%s", cp);
105
0
    free(cp);
106
0
  }
107
0
}
108
109
/* Notify control clients that window pane changed. */
110
static void
111
control_window_pane_changed_cb(__unused const char *name,
112
    struct event_payload *ep, __unused void *sink_data)
113
0
{
114
0
  struct client *c;
115
0
  struct window *w = event_payload_get_window(ep, "window");
116
117
0
  if (w == NULL || w->active == NULL)
118
0
    return;
119
0
  TAILQ_FOREACH(c, &clients, entry) {
120
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
121
0
      continue;
122
123
0
    control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id,
124
0
        w->active->id);
125
0
  }
126
0
}
127
128
/* Notify control clients that a window was unlinked. */
129
static void
130
control_window_unlinked_cb(__unused const char *name, struct event_payload *ep,
131
    __unused void *sink_data)
132
0
{
133
0
  struct client   *c;
134
0
  struct session    *cs;
135
0
  struct window   *w = event_payload_get_window(ep, "window");
136
137
0
  if (w == NULL)
138
0
    return;
139
0
  TAILQ_FOREACH(c, &clients, entry) {
140
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
141
0
      continue;
142
0
    cs = c->session;
143
144
0
    if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
145
0
      control_notify_write(c, "%%window-close @%u", w->id);
146
0
    else {
147
0
      control_notify_write(c, "%%unlinked-window-close @%u",
148
0
          w->id);
149
0
    }
150
0
  }
151
0
}
152
153
/* Notify control clients that a window was linked. */
154
static void
155
control_window_linked_cb(__unused const char *name, struct event_payload *ep,
156
    __unused void *sink_data)
157
0
{
158
0
  struct client   *c;
159
0
  struct session    *cs;
160
0
  struct window   *w = event_payload_get_window(ep, "window");
161
162
0
  if (w == NULL)
163
0
    return;
164
0
  TAILQ_FOREACH(c, &clients, entry) {
165
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
166
0
      continue;
167
0
    cs = c->session;
168
169
0
    if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
170
0
      control_notify_write(c, "%%window-add @%u", w->id);
171
0
    else {
172
0
      control_notify_write(c, "%%unlinked-window-add @%u",
173
0
          w->id);
174
0
    }
175
0
  }
176
0
}
177
178
/* Notify control clients that a window was renamed. */
179
static void
180
control_window_renamed_cb(__unused const char *name, struct event_payload *ep,
181
    __unused void *sink_data)
182
0
{
183
0
  struct client   *c;
184
0
  struct session    *cs;
185
0
  struct window   *w = event_payload_get_window(ep, "window");
186
187
0
  if (w == NULL)
188
0
    return;
189
0
  TAILQ_FOREACH(c, &clients, entry) {
190
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
191
0
      continue;
192
0
    cs = c->session;
193
194
0
    if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) {
195
0
      control_notify_write(c, "%%window-renamed @%u %s",
196
0
          w->id, w->name);
197
0
    } else {
198
0
      control_notify_write(c,
199
0
          "%%unlinked-window-renamed @%u %s", w->id, w->name);
200
0
    }
201
0
  }
202
0
}
203
204
/* Notify control clients that a client changed session. */
205
static void
206
control_client_session_changed_cb(__unused const char *name,
207
    struct event_payload *ep, __unused void *sink_data)
208
0
{
209
0
  struct client   *cc = event_payload_get_client(ep, "client");
210
0
  struct client   *c;
211
0
  struct session    *s;
212
213
0
  if (cc == NULL || cc->session == NULL)
214
0
    return;
215
0
  s = cc->session;
216
217
0
  TAILQ_FOREACH(c, &clients, entry) {
218
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
219
0
      continue;
220
221
0
    if (cc == c) {
222
0
      control_notify_write(c, "%%session-changed $%u %s",
223
0
          s->id, s->name);
224
0
    } else {
225
0
      control_notify_write(c,
226
0
          "%%client-session-changed %s $%u %s", cc->name,
227
0
          s->id, s->name);
228
0
    }
229
0
  }
230
0
}
231
232
/* Notify control clients that a client detached. */
233
static void
234
control_client_detached_cb(__unused const char *name, struct event_payload *ep,
235
    __unused void *sink_data)
236
0
{
237
0
  struct client   *cc = event_payload_get_client(ep, "client");
238
0
  struct client   *c;
239
240
0
  if (cc == NULL)
241
0
    return;
242
0
  TAILQ_FOREACH(c, &clients, entry) {
243
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
244
0
      continue;
245
0
    control_notify_write(c, "%%client-detached %s", cc->name);
246
0
  }
247
0
}
248
249
/* Notify control clients that a session was renamed. */
250
static void
251
control_session_renamed_cb(__unused const char *name, struct event_payload *ep,
252
    __unused void *sink_data)
253
0
{
254
0
  struct session    *s = event_payload_get_session(ep, "session");
255
0
  struct client   *c;
256
257
0
  if (s == NULL)
258
0
    return;
259
0
  TAILQ_FOREACH(c, &clients, entry) {
260
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
261
0
      continue;
262
0
    control_notify_write(c, "%%session-renamed $%u %s", s->id,
263
0
        s->name);
264
0
  }
265
0
}
266
267
/* Notify control clients that sessions changed. */
268
static void
269
control_session_created_cb(__unused const char *name,
270
    __unused struct event_payload *ep, __unused void *sink_data)
271
0
{
272
0
  struct client *c;
273
274
0
  TAILQ_FOREACH(c, &clients, entry) {
275
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
276
0
      continue;
277
0
    control_notify_write(c, "%%sessions-changed");
278
0
  }
279
0
}
280
281
/* Notify control clients that sessions changed. */
282
static void
283
control_session_closed_cb(__unused const char *name,
284
    __unused struct event_payload *ep, __unused void *sink_data)
285
0
{
286
0
  struct client *c;
287
288
0
  TAILQ_FOREACH(c, &clients, entry) {
289
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
290
0
      continue;
291
0
    control_notify_write(c, "%%sessions-changed");
292
0
  }
293
0
}
294
295
/* Notify control clients that the current window changed. */
296
static void
297
control_session_window_changed_cb(__unused const char *name,
298
    struct event_payload *ep, __unused void *sink_data)
299
0
{
300
0
  struct session  *s = event_payload_get_session(ep, "session");
301
0
  struct client *c;
302
303
  /*
304
   * A deferred session-window-changed notification can fire after the
305
   * session has been destroyed (which sets curw to NULL) but is kept
306
   * alive by the notification's reference. Skip the notification.
307
   */
308
0
  if (s == NULL || s->curw == NULL)
309
0
    return;
310
311
0
  TAILQ_FOREACH(c, &clients, entry) {
312
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
313
0
      continue;
314
0
    control_notify_write(c, "%%session-window-changed $%u @%u",
315
0
        s->id, s->curw->window->id);
316
0
  }
317
0
}
318
319
/* Notify control clients that a paste buffer changed. */
320
static void
321
control_paste_buffer_changed_cb(__unused const char *name,
322
    struct event_payload *ep, __unused void *sink_data)
323
0
{
324
0
  const char  *pbname = event_payload_get_string(ep, "paste_buffer");
325
0
  struct client *c;
326
327
0
  if (pbname == NULL)
328
0
    return;
329
0
  TAILQ_FOREACH(c, &clients, entry) {
330
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
331
0
      continue;
332
0
    control_notify_write(c, "%%paste-buffer-changed %s", pbname);
333
0
  }
334
0
}
335
336
/* Notify control clients that a paste buffer was deleted. */
337
static void
338
control_paste_buffer_deleted_cb(__unused const char *name,
339
    struct event_payload *ep, __unused void *sink_data)
340
0
{
341
0
  const char  *pbname = event_payload_get_string(ep, "paste_buffer");
342
0
  struct client *c;
343
344
0
  if (pbname == NULL)
345
0
    return;
346
0
  TAILQ_FOREACH(c, &clients, entry) {
347
0
    if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
348
0
      continue;
349
0
    control_notify_write(c, "%%paste-buffer-deleted %s", pbname);
350
0
  }
351
0
}
352
353
/* Build control event sinks. */
354
void
355
control_build_events(void)
356
0
{
357
  /* Control event sink callbacks. */
358
0
  static struct {
359
0
    const char  *name;
360
0
    events_cb  cb;
361
0
  } events[] = {
362
0
    { "pane-mode-changed", control_pane_mode_changed_cb },
363
0
    { "window-layout-changed", control_window_layout_changed_cb },
364
0
    { "window-pane-changed", control_window_pane_changed_cb },
365
0
    { "window-unlinked", control_window_unlinked_cb },
366
0
    { "window-linked", control_window_linked_cb },
367
0
    { "window-renamed", control_window_renamed_cb },
368
0
    { "client-session-changed", control_client_session_changed_cb },
369
0
    { "client-detached", control_client_detached_cb },
370
0
    { "session-renamed", control_session_renamed_cb },
371
0
    { "session-created", control_session_created_cb },
372
0
    { "session-closed", control_session_closed_cb },
373
0
    { "session-window-changed", control_session_window_changed_cb },
374
0
    { "paste-buffer-changed", control_paste_buffer_changed_cb },
375
0
    { "paste-buffer-deleted", control_paste_buffer_deleted_cb }
376
0
  };
377
0
  u_int i;
378
379
0
  for (i = 0; i < nitems(events); i++)
380
0
    events_add_sink(events[i].name, events[i].cb, NULL);
381
0
}