Coverage Report

Created: 2026-09-01 06:24

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