Files
tmux/server-client.c
Michael Grant 104a0cee99 server-client: fix damage being composed twice per redraw pass
`(~c->flags & CLIENT_ALLREDRAWFLAGS)` - for a multi-bit mask, ~x & MASK
means "at least one of these bits is unset" (almost always true), not
"none of these bits are set" as the comment and surrounding logic
clearly intend. Every floating-pane drag command unconditionally sets
CLIENT_REDRAWBORDERS (server_redraw_window_borders(), called from
cmd-resize-pane.c/cmd-join-pane.c/cmd-split-window.c) alongside
reporting window damage, so this fallback fired on every single drag
step: redraw_client_damage(c) ran here, then ran again a few lines
later at the CLIENT_ALLREDRAWFLAGS block for the exact same
rectangles. Confirmed via the server's own -vv log: a 6-step drag
produced 12 "composing damage" lines in matched pairs (identical
position and size, milliseconds apart) with no fix, 6 with it.

No memory-safety issue - redraw_client_damage() only reads w->damage,
never frees it - just wasted work rendering the same rectangles twice
per pass.

Fix: `(c->flags & CLIENT_ALLREDRAWFLAGS) == 0`, matching the comment's
actual intent.

regress/floating-pane-drag-no-double-composite.sh drags a floating
pane and asserts no two consecutive "composing damage" log lines share
the same position and size (comparing both together, since distinct
drag steps commonly share the same rectangle size and only the
position differs). Verified failing 3/3 against the pre-fix code (all
6 rectangles doubled each run) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:09:28 +01:00

3237 lines
85 KiB
C

/* $OpenBSD: server-client.c,v 1.513 2026/09/21 10:22:31 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/uio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "tmux.h"
static void server_client_free(int, short, void *);
static void server_client_check_pane_resize(struct window_pane *);
static void server_client_check_pane_buffer(struct window_pane *);
static void server_client_check_window_resize(struct window *);
static key_code server_client_check_mouse(struct client *, struct key_event *);
static void server_client_repeat_timer(int, short, void *);
static void server_client_click_timer(int, short, void *);
static void server_client_check_exit(struct client *, int);
static void server_client_exit_timer(int, short, void *);
static void server_client_check_redraw(struct client *);
static void server_client_check_modes(struct client *);
static void server_client_set_title(struct client *);
static void server_client_set_path(struct client *);
static void server_client_set_progress_bar(struct client *);
static void server_client_reset_state(struct client *);
static void server_client_update_latest(struct client *);
static int server_client_handle_dead_key(struct window_pane *, key_code);
static void server_client_dispatch(struct imsg *, void *);
static int server_client_dispatch_command(struct client *, struct imsg *);
static int server_client_dispatch_identify(struct client *, struct imsg *);
static int server_client_dispatch_shell(struct client *);
static void server_client_report_theme(struct client *, enum client_theme);
/* Number of attached clients. */
u_int
server_client_how_many(void)
{
struct client *c;
u_int n;
n = 0;
TAILQ_FOREACH(c, &clients, entry) {
if (c->session != NULL && (~c->flags & CLIENT_UNATTACHEDFLAGS))
n++;
}
return (n);
}
/* Are these ranges empty? That is, nothing is visible. */
int
server_client_ranges_is_empty(struct visible_ranges *r)
{
u_int i;
for (i = 0; i < r->used; i++) {
if (r->ranges[i].nx != 0)
return (0);
}
return (1);
}
/* Ensure we have space for at least n ranges. */
void
server_client_ensure_ranges(struct visible_ranges *r, u_int n)
{
if (r->size >= n)
return;
r->ranges = xrecallocarray(r->ranges, r->size, n, sizeof *r->ranges);
r->size = n;
}
/* Check if this client is inside this server. */
int
server_client_check_nested(struct client *c)
{
struct environ_entry *envent;
struct window_pane *wp;
envent = environ_find(c->environ, "TMUX");
if (envent == NULL || *envent->value == '\0')
return (0);
RB_FOREACH(wp, window_pane_tree, &all_window_panes) {
if (strcmp(wp->tty, c->ttyname) == 0)
return (1);
}
return (0);
}
/* Set client key table. */
void
server_client_set_key_table(struct client *c, const char *name)
{
if (name == NULL)
name = server_client_get_key_table(c);
key_bindings_unref_table(c->keytable);
c->keytable = key_bindings_get_table(name, 1);
c->keytable->references++;
if (gettimeofday(&c->keytable->activity_time, NULL) != 0)
fatal("gettimeofday failed");
}
static uint64_t
server_client_key_table_activity_diff(struct client *c)
{
struct timeval diff;
timersub(&c->activity_time, &c->keytable->activity_time, &diff);
return ((diff.tv_sec * 1000ULL) + (diff.tv_usec / 1000ULL));
}
/* Get default key table. */
const char *
server_client_get_key_table(struct client *c)
{
struct session *s = c->session;
const char *name;
if (s == NULL)
return ("root");
name = options_get_string(s->options, "key-table");
if (*name == '\0')
return ("root");
return (name);
}
/* Is this table the default key table? */
static int
server_client_is_default_key_table(struct client *c, struct key_table *table)
{
return (strcmp(table->name, server_client_get_key_table(c)) == 0);
}
/* Create a new client. */
struct client *
server_client_create(int fd)
{
struct client *c;
u_int i;
setblocking(fd, 0);
c = xcalloc(1, sizeof *c);
c->references = 1;
c->peer = proc_add_peer(server_proc, fd, server_client_dispatch, c);
if (gettimeofday(&c->creation_time, NULL) != 0)
fatal("gettimeofday failed");
memcpy(&c->activity_time, &c->creation_time, sizeof c->activity_time);
c->environ = environ_create();
c->fd = -1;
c->out_fd = -1;
c->queue = cmdq_new();
RB_INIT(&c->files);
c->tty.sx = 80;
c->tty.sy = 24;
for (i = 0; i < COLOUR_THEME_COUNT; i++)
c->theme_colours[i] = 8;
c->theme = THEME_UNKNOWN;
status_init(c);
c->flags |= CLIENT_FOCUSED;
c->keytable = key_bindings_get_table("root", 1);
c->keytable->references++;
evtimer_set(&c->repeat_timer, server_client_repeat_timer, c);
evtimer_set(&c->click_timer, server_client_click_timer, c);
evtimer_set(&c->exit_timer, server_client_exit_timer, c);
c->click_wp = -1;
TAILQ_INIT(&c->input_requests);
TAILQ_INSERT_TAIL(&clients, c, entry);
log_debug("new client %p", c);
return (c);
}
/* Open client terminal if needed. */
int
server_client_open(struct client *c, char **cause)
{
const char *ttynam = _PATH_TTY;
if (c->flags & CLIENT_CONTROL)
return (0);
if (strcmp(c->ttyname, ttynam) == 0||
((isatty(STDIN_FILENO) &&
(ttynam = ttyname(STDIN_FILENO)) != NULL &&
strcmp(c->ttyname, ttynam) == 0) ||
(isatty(STDOUT_FILENO) &&
(ttynam = ttyname(STDOUT_FILENO)) != NULL &&
strcmp(c->ttyname, ttynam) == 0) ||
(isatty(STDERR_FILENO) &&
(ttynam = ttyname(STDERR_FILENO)) != NULL &&
strcmp(c->ttyname, ttynam) == 0))) {
xasprintf(cause, "can't use %s", c->ttyname);
return (-1);
}
if (!(c->flags & CLIENT_TERMINAL)) {
*cause = xstrdup("not a terminal");
return (-1);
}
if (tty_open(&c->tty, cause) != 0)
return (-1);
server_client_update_theme_colours(c);
return (0);
}
/* Lost an attached client. */
static void
server_client_attached_lost(struct client *c)
{
struct session *s;
struct window *w;
struct client *loop;
struct client *found;
log_debug("lost attached client %p", c);
/*
* By this point the session in the client has been cleared so walk all
* windows to find any with this client as the latest.
*/
RB_FOREACH(w, windows, &windows) {
if (w->latest != c)
continue;
found = NULL;
TAILQ_FOREACH(loop, &clients, entry) {
s = loop->session;
if (loop == c || s == NULL || s->curw->window != w)
continue;
if (found == NULL || timercmp(&loop->activity_time,
&found->activity_time, >))
found = loop;
}
if (found != NULL)
server_client_update_latest(found);
}
}
/* Fire client session changed. */
static void
server_client_fire_session_changed(struct client *c, struct session *old)
{
struct event_payload *ep;
struct cmd_find_state fs;
ep = event_payload_create();
cmd_find_from_client(&fs, c, 0);
event_payload_set_target(ep, &fs);
event_payload_set_client(ep, "client", c);
if (fs.s != NULL) {
event_payload_set_session(ep, "session", fs.s);
event_payload_set_session(ep, "new_session", fs.s);
}
if (old != NULL)
event_payload_set_session(ep, "old_session", old);
if (fs.w != NULL)
event_payload_set_window(ep, "window", fs.w);
if (fs.wl != NULL)
event_payload_set_int(ep, "window_index", fs.wl->idx);
else if (fs.idx != -1)
event_payload_set_int(ep, "window_index", fs.idx);
if (fs.wp != NULL)
event_payload_set_pane(ep, "pane", fs.wp);
events_fire("client-session-changed", ep);
}
/* Fire client resized. */
static void
server_client_fire_resized(struct client *c, u_int old_sx, u_int old_sy)
{
struct event_payload *ep;
struct cmd_find_state fs;
ep = event_payload_create();
cmd_find_from_client(&fs, c, 0);
event_payload_set_target(ep, &fs);
event_payload_set_client(ep, "client", c);
if (fs.s != NULL)
event_payload_set_session(ep, "session", fs.s);
if (fs.w != NULL)
event_payload_set_window(ep, "window", fs.w);
if (fs.wl != NULL)
event_payload_set_int(ep, "window_index", fs.wl->idx);
else if (fs.idx != -1)
event_payload_set_int(ep, "window_index", fs.idx);
if (fs.wp != NULL)
event_payload_set_pane(ep, "pane", fs.wp);
event_payload_set_uint(ep, "width", c->tty.sx);
event_payload_set_uint(ep, "height", c->tty.sy);
event_payload_set_uint(ep, "old_width", old_sx);
event_payload_set_uint(ep, "old_height", old_sy);
events_fire("client-resized", ep);
}
/* Set client session. */
void
server_client_set_session(struct client *c, struct session *s)
{
struct session *old = c->session;
if (s != NULL && c->session != NULL && c->session != s)
c->last_session = c->session;
else if (s == NULL)
c->last_session = NULL;
c->session = s;
c->flags |= CLIENT_FOCUSED;
if (old != NULL && old->curw != NULL)
window_update_focus(old->curw->window);
if (s != NULL) {
s->curw->window->latest = c;
recalculate_sizes();
window_update_focus(s->curw->window);
session_update_activity(s, NULL);
session_theme_changed(s);
gettimeofday(&s->last_attached_time, NULL);
s->curw->flags &= ~WINLINK_ALERTFLAGS;
alerts_check_session(s);
tty_update_client_offset(c);
status_timer_start(c);
server_client_fire_session_changed(c, old);
/*
* A full redraw is only needed if the client's session or
* current window actually changed - not if this merely
* confirmed the client is still looking at the same window
* (as happens when switch-client -t targets a pane in the
* already-current window, e.g. clicking a pane name in a
* second #{P:} status line: the default MouseDown1Status
* binding resolves that click to switch-client -t=, which
* reaches here regardless of whether anything besides the
* active pane changed). Redrawing unconditionally here
* forced a full window redraw for what should have been
* just an active-pane change, already handled narrowly by
* window_set_active_pane() and window_redraw_active_switch()
* before this is reached.
*
* old and s may be the same session object, whose curw was
* already updated to the new window before this function was
* called - old->curw and s->curw would then read the same,
* already-current value, so comparing them can never detect
* a same-session window change. Compare against the client's
* own cached scene instead, which only reflects what it has
* actually drawn.
*/
if (old == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
}
server_check_unattached();
server_update_socket();
}
/* Lost a client. */
void
server_client_lost(struct client *c)
{
struct client_file *cf, *cf1;
if (cfg_client == c)
cfg_client = NULL;
c->flags |= CLIENT_DEAD;
status_prompt_clear(c);
status_message_clear(c);
RB_FOREACH_SAFE(cf, client_files, &c->files, cf1) {
cf->error = EINTR;
file_fire_done(cf);
}
TAILQ_REMOVE(&clients, c, entry);
log_debug("lost client %p", c);
if (c->flags & CLIENT_ATTACHED) {
server_client_attached_lost(c);
events_fire_client("client-detached", c);
}
if (c->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL)))
events_fire_client("client-closed", c);
if (c->flags & CLIENT_CONTROL)
control_stop(c);
if (c->flags & CLIENT_TERMINAL)
tty_free(&c->tty);
free(c->ttyname);
free(c->clipboard_panes);
free(c->term_name);
free(c->term_type);
tty_term_free_list(c->term_caps, c->term_ncaps);
status_free(c);
input_cancel_requests(c);
free(c->title);
free(c->path);
free((void *)c->cwd);
free(c->exit_session);
free(c->exit_message);
evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer);
evtimer_del(&c->exit_timer);
if (event_initialized(&c->cycle_timer))
evtimer_del(&c->cycle_timer);
key_bindings_unref_table(c->keytable);
free(c->message_string);
if (event_initialized(&c->message_timer))
evtimer_del(&c->message_timer);
prompt_free(c->prompt);
format_lost_client(c);
environ_free(c->environ);
proc_remove_peer(c->peer);
c->peer = NULL;
if (c->out_fd != -1)
close(c->out_fd);
if (c->fd != -1) {
close(c->fd);
c->fd = -1;
}
server_client_unref(c);
server_add_accept(0); /* may be more file descriptors now */
recalculate_sizes();
server_check_unattached();
server_update_socket();
}
/* Remove reference from a client. */
void
server_client_unref(struct client *c)
{
log_debug("unref client %p (%d references)", c, c->references);
c->references--;
if (c->references == 0)
event_once(-1, EV_TIMEOUT, server_client_free, c, NULL);
}
/* Free dead client. */
static void
server_client_free(__unused int fd, __unused short events, void *arg)
{
struct client *c = arg;
log_debug("free client %p (%d references)", c, c->references);
redraw_free_scene(c->redraw_scene);
cmdq_free(c->queue);
if (c->references == 0) {
free((void *)c->name);
free((void *)c->user);
free(c);
}
}
/* Suspend a client. */
void
server_client_suspend(struct client *c)
{
struct session *s = c->session;
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
return;
tty_stop_tty(&c->tty);
c->flags |= CLIENT_SUSPENDED;
proc_send(c->peer, MSG_SUSPEND, -1, NULL, 0);
}
/* Detach a client. */
void
server_client_detach(struct client *c, enum msgtype msgtype)
{
struct session *s = c->session;
if (s == NULL || (c->flags & CLIENT_NODETACHFLAGS))
return;
c->flags |= CLIENT_EXIT;
c->exit_type = CLIENT_EXIT_DETACH;
c->exit_msgtype = msgtype;
c->exit_session = xstrdup(s->name);
}
/* Execute command to replace a client. */
void
server_client_exec(struct client *c, const char *cmd)
{
struct session *s = c->session;
char *msg;
const char *shell;
size_t cmdsize, shellsize;
if (*cmd == '\0')
return;
cmdsize = strlen(cmd) + 1;
if (s != NULL)
shell = options_get_string(s->options, "default-shell");
else
shell = options_get_string(global_s_options, "default-shell");
if (!checkshell(shell))
shell = _PATH_BSHELL;
shellsize = strlen(shell) + 1;
msg = xmalloc(cmdsize + shellsize);
memcpy(msg, cmd, cmdsize);
memcpy(msg + cmdsize, shell, shellsize);
proc_send(c->peer, MSG_EXEC, -1, msg, cmdsize + shellsize);
free(msg);
}
/* Is this point inside the auto-hide scrollbar interaction area? */
static int
server_client_in_scrollbar_area(struct window_pane *wp, int px, int py)
{
struct window *w = wp->window;
u_int width, pad, total;
int start, end;
if (!window_pane_scrollbar_overlay(wp))
return (0);
if (py < wp->yoff || py >= wp->yoff + (int)wp->sy)
return (0);
width = wp->scrollbar_style.width;
pad = wp->scrollbar_style.pad;
total = width + pad;
if (total == 0 || total > wp->sx)
total = wp->sx;
if (w->sb_pos == PANE_SCROLLBARS_LEFT) {
start = wp->xoff;
end = wp->xoff + (int)total - 1;
} else {
end = wp->xoff + (int)wp->sx - 1;
start = end - (int)total + 1;
}
return (px >= start && px <= end);
}
/* Update auto-hide scrollbars for a mouse movement. */
static void
server_client_update_scrollbar_hover(struct client *c, int type, int px, int py)
{
struct window *w = c->session->curw->window;
struct window_pane *wp;
if (type != KEYC_TYPE_MOUSEMOVE)
return;
TAILQ_FOREACH(wp, &w->panes, entry) {
if (!window_pane_is_visible(wp))
continue;
if (server_client_in_scrollbar_area(wp, px, py)) {
wp->sb_auto_hover = 1;
window_pane_scrollbar_show(wp, 1);
} else {
wp->sb_auto_hover = 0;
window_pane_scrollbar_start_timer(wp);
}
}
}
/* Is the mouse inside a pane? */
static enum key_code_mouse_location
server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py,
u_int *sl_mpos)
{
struct window *w = wp->window;
struct window_pane *fwp;
int pane_status, sb_w, sb_pad;
int pane_status_line, sl_top, sl_bottom;
int bdr_bottom, bdr_top, bdr_left, bdr_right;
int sb_start, sb_end, sb_overlay;
pane_status = window_pane_get_pane_status(wp);
sb_overlay = window_pane_scrollbar_overlay(wp);
if (window_pane_scrollbar_visible(wp)) {
sb_w = wp->scrollbar_style.width;
sb_pad = wp->scrollbar_style.pad;
if (sb_overlay && sb_w > (int)wp->sx)
sb_w = wp->sx;
} else {
sb_w = 0;
sb_pad = 0;
}
if (pane_status == PANE_STATUS_TOP)
pane_status_line = wp->yoff - 1;
else if (pane_status == PANE_STATUS_BOTTOM)
pane_status_line = wp->yoff + wp->sy;
else
pane_status_line = -1; /* not used */
bdr_left = wp->xoff - 1;
if (!sb_overlay && w->sb_pos == PANE_SCROLLBARS_LEFT)
bdr_left -= sb_pad + sb_w;
if (sb_overlay && sb_w != 0 &&
py >= wp->yoff && py < wp->yoff + (int)wp->sy &&
px >= wp->xoff && px < wp->xoff + (int)wp->sx) {
if (w->sb_pos == PANE_SCROLLBARS_LEFT) {
sb_start = wp->xoff;
sb_end = sb_start + sb_w - 1;
} else {
sb_end = wp->xoff + (int)wp->sx - 1;
sb_start = sb_end - sb_w + 1;
}
if (px >= sb_start && px <= sb_end) {
sl_top = wp->yoff + wp->sb_slider_y;
sl_bottom = (wp->yoff + wp->sb_slider_y +
wp->sb_slider_h - 1);
if (py < sl_top)
return (KEYC_MOUSE_LOCATION_SCROLLBAR_UP);
else if (py >= sl_top && py <= sl_bottom) {
*sl_mpos = (py - wp->sb_slider_y - wp->yoff);
return (KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER);
} else
return (KEYC_MOUSE_LOCATION_SCROLLBAR_DOWN);
}
return (KEYC_MOUSE_LOCATION_PANE);
}
/* Check if point is within the pane or scrollbar. */
if (((pane_status != PANE_STATUS_OFF &&
py != pane_status_line && py != wp->yoff + (int)wp->sy) ||
(wp->yoff == 0 && py < (int)wp->sy) ||
(py >= wp->yoff && py < wp->yoff + (int)wp->sy)) &&
((w->sb_pos == PANE_SCROLLBARS_RIGHT &&
px < wp->xoff + (int)wp->sx + sb_pad + sb_w) ||
(w->sb_pos == PANE_SCROLLBARS_LEFT &&
px < wp->xoff + (int)wp->sx - sb_pad - sb_w))) {
/* Check if in the scrollbar. */
if ((w->sb_pos == PANE_SCROLLBARS_RIGHT &&
(px >= wp->xoff + (int)wp->sx + sb_pad &&
px < wp->xoff + (int)wp->sx + sb_pad + sb_w)) ||
(w->sb_pos == PANE_SCROLLBARS_LEFT &&
(px >= wp->xoff - sb_pad - sb_w &&
px < wp->xoff - sb_pad))) {
/* Check where inside the scrollbar. */
sl_top = wp->yoff + wp->sb_slider_y;
sl_bottom = (wp->yoff + wp->sb_slider_y +
wp->sb_slider_h - 1);
if (py < sl_top)
return (KEYC_MOUSE_LOCATION_SCROLLBAR_UP);
else if (py >= sl_top && py <= sl_bottom) {
*sl_mpos = (py - wp->sb_slider_y - wp->yoff);
return (KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER);
} else /* py > sl_bottom */
return (KEYC_MOUSE_LOCATION_SCROLLBAR_DOWN);
} else if (window_pane_is_floating(wp) &&
window_pane_get_pane_lines(wp) != PANE_LINES_NONE &&
(px == bdr_left ||
py == wp->yoff - 1 ||
py == wp->yoff + (int)wp->sy)) {
/* Floating pane left, bottom or top border. */
return (KEYC_MOUSE_LOCATION_BORDER);
} else {
/* Must be inside the pane. */
return (KEYC_MOUSE_LOCATION_PANE);
}
} else {
/* Try the pane borders. */
TAILQ_FOREACH(fwp, &w->panes, entry) {
if (!window_pane_is_visible(fwp))
continue;
if (window_pane_is_floating(fwp) &&
window_pane_get_pane_lines(fwp) == PANE_LINES_NONE)
continue;
if (window_pane_scrollbar_reserve(fwp)) {
sb_w = fwp->scrollbar_style.width;
sb_pad = fwp->scrollbar_style.pad;
} else {
sb_w = 0;
sb_pad = 0;
}
bdr_top = fwp->yoff - 1;
bdr_bottom = fwp->yoff + fwp->sy;
bdr_left = fwp->xoff - 1;
if (w->sb_pos == PANE_SCROLLBARS_LEFT) {
bdr_left -= sb_pad + sb_w;
bdr_right = fwp->xoff + fwp->sx;
} else {
/* PANE_SCROLLBARS_RIGHT or none. */
bdr_right = fwp->xoff + fwp->sx + sb_pad + sb_w;
}
if (py >= fwp->yoff - 1 &&
py <= fwp->yoff + (int)fwp->sy) {
if (px == bdr_right)
break;
if (window_pane_is_floating(wp)) {
/* Floating pane, check left border. */
if (px == bdr_left)
break;
}
}
if (px >= bdr_left && px <= fwp->xoff + (int)fwp->sx) {
bdr_bottom = fwp->yoff + fwp->sy;
if (py == bdr_bottom)
break;
if (py == bdr_top)
break;
}
}
if (fwp != NULL)
return (KEYC_MOUSE_LOCATION_BORDER);
}
return (KEYC_MOUSE_LOCATION_NOWHERE);
}
/* Check for mouse keys. */
static key_code
server_client_check_mouse(struct client *c, struct key_event *event)
{
struct mouse_event *m = &event->m;
struct session *s = c->session, *fs;
struct window *w = s->curw->window;
struct winlink *fwl;
struct window_pane *wp, *fwp, *lwp = NULL;
u_int x, y, sx, sy, px, py, n, sl_mpos = 0;
u_int b, bn;
int ignore = 0;
int modal_drag = 0;
key_code key;
struct timeval tv;
struct style_range *sr;
enum key_code_type type = KEYC_TYPE_NOTYPE;
enum key_code_mouse_location loc = KEYC_MOUSE_LOCATION_NOWHERE;
log_debug("%s mouse %02x at %u,%u (last %u,%u) (%d)", c->name, m->b,
m->x, m->y, m->lx, m->ly, c->tty.mouse_drag_flag);
/* Find last pane, if any. */
if (c->tty.mouse_last_pane != -1) {
lwp = window_pane_find_by_id(c->tty.mouse_last_pane);
if (lwp != NULL)
log_debug("%s mouse last pane %%%u", c->name, lwp->id);
}
/* What type of event is this? */
if (event->key == KEYC_DOUBLECLICK) {
type = KEYC_TYPE_DOUBLECLICK;
x = m->x, y = m->y, b = m->b;
ignore = 1;
log_debug("double-click at %u,%u", x, y);
} else if ((m->sgr_type != ' ' &&
MOUSE_DRAG(m->sgr_b) &&
MOUSE_RELEASE(m->sgr_b)) ||
(m->sgr_type == ' ' &&
MOUSE_DRAG(m->b) &&
MOUSE_RELEASE(m->b) &&
MOUSE_RELEASE(m->lb))) {
type = KEYC_TYPE_MOUSEMOVE;
x = m->x, y = m->y, b = 0;
log_debug("move at %u,%u", x, y);
} else if (MOUSE_DRAG(m->b)) {
type = KEYC_TYPE_MOUSEDRAG;
if (c->tty.mouse_drag_flag) {
x = m->x, y = m->y, b = m->b;
if (x == m->lx && y == m->ly)
return (KEYC_UNKNOWN);
log_debug("drag update at %u,%u", x, y);
} else {
x = m->lx, y = m->ly, b = m->lb;
log_debug("drag start at %u,%u", x, y);
}
} else if (MOUSE_WHEEL(m->b)) {
if ((m->b & MOUSE_MASK_BUTTONS) == MOUSE_WHEEL_UP)
type = KEYC_TYPE_WHEELUP;
else
type = KEYC_TYPE_WHEELDOWN;
x = m->x, y = m->y, b = m->b;
log_debug("wheel at %u,%u", x, y);
} else if (MOUSE_RELEASE(m->b)) {
type = KEYC_TYPE_MOUSEUP;
x = m->x, y = m->y, b = m->lb;
if (m->sgr_type == 'm')
b = m->sgr_b;
log_debug("up at %u,%u", x, y);
} else {
if (c->flags & CLIENT_DOUBLECLICK) {
evtimer_del(&c->click_timer);
c->flags &= ~CLIENT_DOUBLECLICK;
type = KEYC_TYPE_SECONDCLICK;
x = m->x, y = m->y, b = m->b;
log_debug("second-click at %u,%u", x, y);
c->flags |= CLIENT_TRIPLECLICK;
} else if (c->flags & CLIENT_TRIPLECLICK) {
evtimer_del(&c->click_timer);
c->flags &= ~CLIENT_TRIPLECLICK;
type = KEYC_TYPE_TRIPLECLICK;
x = m->x, y = m->y, b = m->b;
log_debug("triple-click at %u,%u", x, y);
goto have_event;
}
/* DOWN is the only remaining event type. */
if (type == KEYC_TYPE_NOTYPE) {
type = KEYC_TYPE_MOUSEDOWN;
x = m->x, y = m->y, b = m->b;
log_debug("down at %u,%u", x, y);
c->flags |= CLIENT_DOUBLECLICK;
}
}
have_event:
if (type == KEYC_TYPE_NOTYPE)
return (KEYC_UNKNOWN);
/* Save the session. */
m->s = s->id;
m->w = -1;
m->wp = -1;
m->ignore = ignore;
/* Is this on the status line? */
m->statusat = status_at_line(c);
m->statuslines = status_line_size(c);
if (m->statusat != -1 &&
y >= (u_int)m->statusat &&
y < m->statusat + m->statuslines) {
sr = status_get_range(c, x, y - m->statusat);
if (sr == NULL) {
loc = KEYC_MOUSE_LOCATION_STATUS_DEFAULT;
} else {
switch (sr->type) {
case STYLE_RANGE_NONE:
return (KEYC_UNKNOWN);
case STYLE_RANGE_LEFT:
log_debug("mouse range: left");
loc = KEYC_MOUSE_LOCATION_STATUS_LEFT;
break;
case STYLE_RANGE_RIGHT:
log_debug("mouse range: right");
loc = KEYC_MOUSE_LOCATION_STATUS_RIGHT;
break;
case STYLE_RANGE_PANE:
fwp = window_pane_find_by_id(sr->argument);
if (fwp == NULL)
return (KEYC_UNKNOWN);
m->wp = sr->argument;
log_debug("mouse range: pane %%%u", m->wp);
loc = KEYC_MOUSE_LOCATION_STATUS;
break;
case STYLE_RANGE_WINDOW:
fwl = winlink_find_by_index(&s->windows,
sr->argument);
if (fwl == NULL)
return (KEYC_UNKNOWN);
m->w = fwl->window->id;
log_debug("mouse range: window @%u", m->w);
loc = KEYC_MOUSE_LOCATION_STATUS;
break;
case STYLE_RANGE_SESSION:
fs = session_find_by_id(sr->argument);
if (fs == NULL)
return (KEYC_UNKNOWN);
m->s = sr->argument;
log_debug("mouse range: session $%u", m->s);
loc = KEYC_MOUSE_LOCATION_STATUS;
break;
case STYLE_RANGE_USER:
log_debug("mouse range: user");
loc = KEYC_MOUSE_LOCATION_STATUS;
break;
case STYLE_RANGE_CONTROL:
n = sr->argument; /* parsing keeps this < 10 */
log_debug("mouse range: control %u", n);
loc = KEYC_MOUSE_LOCATION_CONTROL0 + n;
break;
}
}
}
/*
* Not on status line. Adjust position and check for border, pane, or
* scrollbar.
*/
if (loc == KEYC_MOUSE_LOCATION_NOWHERE && c->tty.mouse_scrolling_flag) {
if (lwp != NULL) {
loc = KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER;
m->wp = lwp->id;
m->w = lwp->window->id;
}
} else if (loc == KEYC_MOUSE_LOCATION_NOWHERE) {
px = x;
if (m->statusat == 0 && y >= m->statuslines)
py = y - m->statuslines;
else if (m->statusat > 0 && y >= (u_int)m->statusat)
py = m->statusat - 1;
else
py = y;
tty_window_offset(&c->tty, &m->ox, &m->oy, &sx, &sy);
log_debug("mouse window @%u at %u,%u (%ux%u)", w->id, m->ox,
m->oy, sx, sy);
if (px > sx || py > sy) {
server_client_update_scrollbar_hover(c, type, -1, -1);
return (KEYC_UNKNOWN);
}
px = px + m->ox;
py = py + m->oy;
if (w->modal != NULL &&
!window_pane_contains(w->modal, px, py)) {
if (lwp == w->modal &&
c->tty.mouse_drag_flag != 0 &&
(type == KEYC_TYPE_MOUSEDRAG ||
type == KEYC_TYPE_MOUSEUP)) {
modal_drag = 1;
wp = lwp;
loc = KEYC_MOUSE_LOCATION_PANE;
m->wp = wp->id;
m->w = wp->window->id;
} else {
server_client_update_scrollbar_hover(c, type,
-1, -1);
c->tty.mouse_drag_update = NULL;
c->tty.mouse_drag_release = NULL;
c->tty.mouse_drag_flag = 0;
c->tty.mouse_scrolling_flag = 0;
c->tty.mouse_slider_mpos = -1;
c->tty.mouse_last_pane = -1;
if ((w->modal->flags & PANE_CLOSEONCLICK) &&
(type == KEYC_TYPE_MOUSEDOWN ||
type == KEYC_TYPE_SECONDCLICK ||
type == KEYC_TYPE_TRIPLECLICK))
server_kill_pane(w->modal);
return (KEYC_UNKNOWN);
}
}
server_client_update_scrollbar_hover(c, type, px, py);
if (modal_drag) {
/* Keep the drag with the modal pane. */
} else if (type == KEYC_TYPE_MOUSEDRAG && lwp != NULL) {
/* Use pane from last mouse event. */
wp = lwp;
} else {
/* Try inside the pane. */
wp = window_get_active_at(w, px, py);
}
if (wp == NULL) {
loc = KEYC_MOUSE_LOCATION_EMPTY;
m->w = w->id;
log_debug("mouse %u,%u on empty area", x, y);
} else {
if (!modal_drag) {
loc = server_client_check_mouse_in_pane(wp, px,
py, &sl_mpos);
}
if (loc == KEYC_MOUSE_LOCATION_PANE) {
log_debug("mouse %u,%u on pane %%%u", x, y,
wp->id);
} else if (loc == KEYC_MOUSE_LOCATION_BORDER) {
sr = window_pane_status_get_range(wp, px, py);
if (sr != NULL) {
n = sr->argument;
loc = KEYC_MOUSE_LOCATION_CONTROL0 + n;
}
log_debug("mouse on pane %%%u border", wp->id);
} else if (loc == KEYC_MOUSE_LOCATION_SCROLLBAR_UP ||
loc == KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER ||
loc == KEYC_MOUSE_LOCATION_SCROLLBAR_DOWN) {
log_debug("mouse on pane %%%u scrollbar",
wp->id);
}
m->wp = wp->id;
m->w = wp->window->id;
}
} else
server_client_update_scrollbar_hover(c, type, -1, -1);
/* Reset click type or add a click timer if needed. */
if (type == KEYC_TYPE_MOUSEDOWN ||
type == KEYC_TYPE_SECONDCLICK ||
type == KEYC_TYPE_TRIPLECLICK) {
if (type != KEYC_TYPE_MOUSEDOWN &&
(m->b != c->click_button ||
loc != (enum key_code_mouse_location)c->click_loc ||
m->wp != c->click_wp)) {
type = KEYC_TYPE_MOUSEDOWN;
log_debug("click sequence reset at %u,%u", x, y);
c->flags &= ~CLIENT_TRIPLECLICK;
c->flags |= CLIENT_DOUBLECLICK;
}
if (type != KEYC_TYPE_TRIPLECLICK && KEYC_CLICK_TIMEOUT != 0) {
memcpy(&c->click_event, m, sizeof c->click_event);
c->click_button = m->b;
c->click_loc = loc;
c->click_wp = m->wp;
log_debug("click timer started");
tv.tv_sec = KEYC_CLICK_TIMEOUT / 1000;
tv.tv_usec = (KEYC_CLICK_TIMEOUT % 1000) * 1000L;
evtimer_del(&c->click_timer);
evtimer_add(&c->click_timer, &tv);
}
}
key = KEYC_UNKNOWN;
/* Stop dragging if needed. */
if (type != KEYC_TYPE_MOUSEDRAG &&
type != KEYC_TYPE_WHEELUP &&
type != KEYC_TYPE_WHEELDOWN &&
type != KEYC_TYPE_DOUBLECLICK &&
type != KEYC_TYPE_TRIPLECLICK &&
c->tty.mouse_drag_flag != 0) {
if (c->tty.mouse_drag_release != NULL)
c->tty.mouse_drag_release(c, m);
c->tty.mouse_drag_update = NULL;
c->tty.mouse_drag_release = NULL;
c->tty.mouse_scrolling_flag = 0;
/*
* End a mouse drag by passing a MouseDragEnd key corresponding
* to the button that started the drag.
*/
type = KEYC_TYPE_MOUSEDRAGEND;
c->tty.mouse_drag_flag = 0;
c->tty.mouse_slider_mpos = -1;
c->tty.mouse_last_pane = -1;
}
/* Convert to a key binding. */
if (type == KEYC_TYPE_MOUSEMOVE && loc == KEYC_MOUSE_LOCATION_PANE) {
key = KEYC_MOUSEMOVE_PANE;
if (wp != NULL &&
wp != w->active &&
options_get_number(s->options, "focus-follows-mouse")) {
window_redraw_active_switch(w, wp);
window_set_active_pane(w, wp, 1);
server_redraw_window_borders(w);
server_status_window(w);
}
}
if (type == KEYC_TYPE_MOUSEDRAG) {
if (c->tty.mouse_drag_update != NULL)
key = KEYC_DRAGGING;
/*
* Begin a drag by setting the flag to a non-zero value that
* corresponds to the mouse button in use. If starting to drag
* the scrollbar, store the relative position in the slider
* where the user grabbed.
*/
if (c->tty.mouse_drag_flag == 0) {
c->tty.mouse_drag_x = px;
c->tty.mouse_drag_y = py;
}
c->tty.mouse_drag_flag = MOUSE_BUTTONS(b) + 1;
/* Only change pane if not already dragging a pane border. */
if (lwp == NULL) {
lwp = wp = window_get_active_at(w, px, py);
if (wp != NULL)
c->tty.mouse_last_pane = wp->id;
}
if (c->tty.mouse_scrolling_flag == 0 &&
loc == KEYC_MOUSE_LOCATION_SCROLLBAR_SLIDER) {
c->tty.mouse_scrolling_flag = 1;
if (m->statusat == 0) {
c->tty.mouse_slider_mpos = sl_mpos +
m->statuslines;
} else
c->tty.mouse_slider_mpos = sl_mpos;
}
}
if (key == KEYC_UNKNOWN) {
/* Adjust the button number. */
if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_1)
bn = 1;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_2)
bn = 2;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_3)
bn = 3;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_6)
bn = 6;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_7)
bn = 7;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_8)
bn = 8;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_9)
bn = 9;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_10)
bn = 10;
else if (MOUSE_BUTTONS(b) == MOUSE_BUTTON_11)
bn = 11;
else
bn = 0;
key = KEYC_MAKE_MOUSE_KEY(type, bn, loc);
}
/* Apply modifiers if any. */
if (b & MOUSE_MASK_META)
key |= KEYC_META;
if (b & MOUSE_MASK_CTRL)
key |= KEYC_CTRL;
if (b & MOUSE_MASK_SHIFT)
key |= KEYC_SHIFT;
if (log_get_level() != 0)
log_debug("mouse key is %s", key_string_lookup_key (key, 1));
return (key);
}
/* Update client theme colours from server options. */
void
server_client_update_theme_colours(struct client *c)
{
struct format_tree *ft;
const char *name, *value;
enum client_theme theme;
char *expanded;
u_int i;
int colour, option;
if (c == NULL)
return;
option = options_get_number(global_options, "theme");
if (option == 1) {
for (i = 0; i < COLOUR_THEME_COUNT; i++)
c->theme_colours[i] = colour_theme_terminal_colour(i);
return;
}
ft = format_create(c, NULL, FORMAT_NONE, FORMAT_NOJOBS);
format_defaults(ft, c, NULL, NULL, NULL);
theme = c->theme;
if (theme == THEME_UNKNOWN)
theme = colour_totheme(c->tty.bg);
if (option == 2)
theme = THEME_LIGHT;
else if (option == 3)
theme = THEME_DARK;
for (i = 0; i < COLOUR_THEME_COUNT; i++) {
c->theme_colours[i] = 8;
name = colour_theme_option(i, theme);
if (name == NULL)
continue;
value = options_get_string(global_options, name);
expanded = format_expand(ft, value);
colour = colour_fromstring(expanded);
free(expanded);
if (colour == -1 || (colour & COLOUR_FLAG_THEME))
continue;
c->theme_colours[i] = colour;
}
format_free(ft);
}
/* Is this a bracket paste key? */
static int
server_client_is_bracket_paste(struct client *c, key_code key)
{
if ((key & KEYC_MASK_KEY) == KEYC_PASTE_START) {
c->flags |= CLIENT_BRACKETPASTING;
c->paste_time = current_time;
log_debug("%s: bracket paste on", c->name);
return (0);
}
if ((key & KEYC_MASK_KEY) == KEYC_PASTE_END) {
c->flags &= ~CLIENT_BRACKETPASTING;
log_debug("%s: bracket paste off", c->name);
return (0);
}
return !!(c->flags & CLIENT_BRACKETPASTING);
}
/* Is this fast enough to probably be a paste? */
static int
server_client_is_assume_paste(struct client *c)
{
struct session *s = c->session;
struct timeval tv;
int t;
if (c->flags & CLIENT_BRACKETPASTING)
return (0);
if ((t = options_get_number(s->options, "assume-paste-time")) == 0)
return (0);
if (tty_term_has(c->tty.term, TTYC_ENBP))
return (0);
timersub(&c->activity_time, &c->last_activity_time, &tv);
if (tv.tv_sec == 0 && tv.tv_usec < t * 1000) {
if (c->flags & CLIENT_ASSUMEPASTING)
return (1);
c->flags |= CLIENT_ASSUMEPASTING;
c->paste_time = current_time;
log_debug("%s: assume paste on", c->name);
return (0);
}
if (c->flags & CLIENT_ASSUMEPASTING) {
c->flags &= ~CLIENT_ASSUMEPASTING;
log_debug("%s: assume paste off", c->name);
}
return (0);
}
/* Has the latest client changed? */
static void
server_client_update_latest(struct client *c)
{
struct window *w;
if (c->session == NULL)
return;
w = c->session->curw->window;
if (w->latest == c)
return;
w->latest = c;
if (options_get_number(w->options, "window-size") == WINDOW_SIZE_LATEST)
recalculate_size(w, 0);
events_fire_client("client-active", c);
}
/* Get repeat time. */
static u_int
server_client_repeat_time(struct client *c, struct key_binding *bd)
{
struct session *s = c->session;
u_int repeat, initial;
if (~bd->flags & KEY_BINDING_REPEAT)
return (0);
repeat = options_get_number(s->options, "repeat-time");
if (repeat == 0)
return (0);
if ((~c->flags & CLIENT_REPEAT) || bd->key != c->last_key) {
initial = options_get_number(s->options, "initial-repeat-time");
if (initial != 0)
repeat = initial;
}
return (repeat);
}
/* Handle a key press which closes a dead pane. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
int remain_on_exit;
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key))
return (0);
remain_on_exit = options_get_number(wp->options, "remain-on-exit");
if (remain_on_exit != 3 && remain_on_exit != 4)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
return (1);
}
/*
* Handle data key input from client. This owns and can modify the key event it
* is given and is responsible for freeing it.
*/
static enum cmd_retval
server_client_key_callback(struct cmdq_item *item, void *data)
{
struct key_event *event = data;
struct client *c, *ec = event->client;
key_code key = event->key;
struct mouse_event *m = &event->m;
struct session *s;
struct winlink *wl;
struct window_pane *wp;
struct window_mode_entry *wme;
struct timeval tv;
struct key_table *table, *first;
struct key_binding *bd;
u_int repeat;
uint64_t flags, prefix_delay;
struct cmd_find_state fs;
key_code key0, prefix, prefix2;
if (ec != NULL)
c = ec;
else
c = cmdq_get_client(item);
s = c->session;
/* Check the client is good to accept input. */
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
goto out;
wl = s->curw;
/* Update the activity timer. */
memcpy(&c->last_activity_time, &c->activity_time,
sizeof c->last_activity_time);
if (gettimeofday(&c->activity_time, NULL) != 0)
fatal("gettimeofday failed");
session_update_activity(s, &c->activity_time);
/* Check for mouse keys. */
m->valid = 0;
if (key == KEYC_MOUSE || key == KEYC_DOUBLECLICK) {
if (c->flags & CLIENT_READONLY)
goto out;
key = server_client_check_mouse(c, event);
if (key == KEYC_UNKNOWN)
goto out;
m->valid = 1;
m->key = key;
/*
* Mouse drag is in progress, so fire the callback (now that
* the mouse event is valid).
*
* Start a synchronized-output region here rather than
* leaving it to whatever redraw eventually follows: a drag
* callback may write directly via the pane's fast path
* immediately, with any correction only arriving later via
* redraw_client_damage(), which opens its own sync region.
* Since tty_sync_end() is only called once, at the very end
* of this client's pass in server_client_reset_state(),
* starting it here merges both into one atomic terminal
* update instead of two visible frames.
*/
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
tty_sync_start(&c->tty);
c->tty.mouse_drag_update(c, m);
goto out;
}
event->key = key;
}
/* Find affected pane. */
if (!KEYC_IS_MOUSE(key) || cmd_find_from_mouse(&fs, m, 0) != 0)
cmd_find_from_client(&fs, c, 0);
wp = fs.wp;
/* Forward mouse keys if disabled. */
if (KEYC_IS_MOUSE(key) && !options_get_number(s->options, "mouse"))
goto forward_key;
/* Forward if bracket pasting. */
if (server_client_is_bracket_paste (c, key))
goto paste_key;
/* Treat everything as a regular key when pasting is detected. */
if (!KEYC_IS_MOUSE(key) &&
key != KEYC_FOCUS_IN &&
key != KEYC_FOCUS_OUT &&
(~key & KEYC_SENT) &&
server_client_is_assume_paste(c))
goto paste_key;
/* Forward keys directly if this pane is capturing all keys. */
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
(~wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
TAILQ_EMPTY(&wp->modes))
goto forward_key;
/* Focus events are not keys and cannot be bound. */
if (key == KEYC_FOCUS_IN || key == KEYC_FOCUS_OUT)
goto forward_key;
/*
* Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table.
*/
if (server_client_is_default_key_table(c, c->keytable) &&
wp != NULL &&
(wme = TAILQ_FIRST(&wp->modes)) != NULL &&
wme->mode->key_table != NULL)
table = key_bindings_get_table(wme->mode->key_table(wme), 1);
else
table = c->keytable;
first = table;
table_changed:
/*
* The prefix always takes precedence and forces a switch to the prefix
* table, unless we are already there.
*/
prefix = (key_code)options_get_number(s->options, "prefix");
prefix2 = (key_code)options_get_number(s->options, "prefix2");
key0 = (key & (KEYC_MASK_KEY|KEYC_MASK_MODIFIERS));
if ((key0 == (prefix & (KEYC_MASK_KEY|KEYC_MASK_MODIFIERS)) ||
key0 == (prefix2 & (KEYC_MASK_KEY|KEYC_MASK_MODIFIERS))) &&
strcmp(table->name, "prefix") != 0) {
server_client_set_key_table(c, "prefix");
server_status_client(c);
goto out;
}
flags = c->flags;
try_again:
/* Log key table. */
if (wp == NULL)
log_debug("key table %s (no pane)", table->name);
else
log_debug("key table %s (pane %%%u)", table->name, wp->id);
if (c->flags & CLIENT_REPEAT)
log_debug("currently repeating");
bd = key_bindings_get(table, key0);
/*
* If prefix-timeout is enabled and we're in the prefix table, see if
* the timeout has been exceeded. Revert to the root table if so.
*/
prefix_delay = options_get_number(global_options, "prefix-timeout");
if (prefix_delay > 0 &&
strcmp(table->name, "prefix") == 0 &&
server_client_key_table_activity_diff(c) > prefix_delay) {
/*
* If repeating is active and this is a repeating binding,
* ignore the timeout.
*/
if (bd != NULL &&
(c->flags & CLIENT_REPEAT) &&
(bd->flags & KEY_BINDING_REPEAT)) {
log_debug("prefix timeout ignored, repeat is active");
} else {
log_debug("prefix timeout exceeded");
server_client_set_key_table(c, NULL);
first = table = c->keytable;
server_status_client(c);
goto table_changed;
}
}
/* Try to see if there is a key binding in the current table. */
if (bd != NULL) {
/*
* Key was matched in this table. If currently repeating but a
* non-repeating binding was found, stop repeating and try
* again in the root table.
*/
if ((c->flags & CLIENT_REPEAT) &&
(~bd->flags & KEY_BINDING_REPEAT)) {
log_debug("found in key table %s (not repeating)",
table->name);
server_client_set_key_table(c, NULL);
first = table = c->keytable;
c->flags &= ~CLIENT_REPEAT;
server_status_client(c);
goto table_changed;
}
log_debug("found in key table %s", table->name);
/*
* Take a reference to this table to make sure the key binding
* doesn't disappear.
*/
table->references++;
/*
* If this is a repeating key, start the timer. Otherwise reset
* the client back to the root table.
*/
repeat = server_client_repeat_time(c, bd);
if (repeat != 0) {
c->flags |= CLIENT_REPEAT;
c->last_key = bd->key;
tv.tv_sec = repeat / 1000;
tv.tv_usec = (repeat % 1000) * 1000L;
evtimer_del(&c->repeat_timer);
evtimer_add(&c->repeat_timer, &tv);
} else {
c->flags &= ~CLIENT_REPEAT;
server_client_set_key_table(c, NULL);
}
server_status_client(c);
/* Execute the key binding. */
key_bindings_dispatch(bd, item, c, event, &fs);
key_bindings_unref_table(table);
goto out;
}
/*
* No match, try the ANY key.
*/
if (key0 != KEYC_ANY) {
key0 = KEYC_ANY;
goto try_again;
}
/*
* Binding movement keys is useless since we only turn them on when the
* application requests, so don't let them exit the prefix table.
*/
if (key == KEYC_MOUSEMOVE_PANE ||
key == KEYC_MOUSEMOVE_STATUS ||
key == KEYC_MOUSEMOVE_STATUS_LEFT ||
key == KEYC_MOUSEMOVE_STATUS_RIGHT ||
key == KEYC_MOUSEMOVE_STATUS_DEFAULT ||
key == KEYC_MOUSEMOVE_BORDER)
goto forward_key;
/*
* No match in this table. If not in the root table or if repeating
* switch the client back to the root table and try again.
*/
log_debug("not found in key table %s", table->name);
if (!server_client_is_default_key_table(c, table) ||
(c->flags & CLIENT_REPEAT)) {
log_debug("trying in root table");
server_client_set_key_table(c, NULL);
table = c->keytable;
if (c->flags & CLIENT_REPEAT)
first = table;
c->flags &= ~CLIENT_REPEAT;
server_status_client(c);
goto table_changed;
}
/*
* No match in the root table either. If this wasn't the first table
* tried, don't pass the key to the pane.
*/
if (first != table && (~flags & CLIENT_REPEAT)) {
server_client_set_key_table(c, NULL);
server_status_client(c);
goto out;
}
forward_key:
if (server_client_handle_dead_key(wp, key))
goto out;
if (c->flags & CLIENT_READONLY)
goto out;
if (wp != NULL)
window_pane_key(wp, c, s, wl, key, m);
goto out;
paste_key:
if (c->flags & CLIENT_READONLY)
goto out;
if (event->buf != NULL)
window_pane_paste(wp, key, event->buf, event->len);
key = KEYC_NONE;
goto out;
out:
if (s != NULL && key != KEYC_FOCUS_OUT)
server_client_update_latest(c);
if (ec != NULL)
server_client_unref(ec);
free(event->buf);
free(event);
return (CMD_RETURN_NORMAL);
}
/* Handle a key event for the active window menu, if any. */
static int
server_client_handle_menu_key(struct client *c, struct key_event *event)
{
struct window *w = c->session->curw->window;
struct key_event new_event;
struct mouse_event *m;
u_int ox, oy, sx, sy;
if (w->menu == NULL)
return (0);
memcpy(&new_event, event, sizeof new_event);
if (KEYC_IS_MOUSE(event->key)) {
m = &new_event.m;
m->statusat = status_at_line(c);
m->statuslines = status_line_size(c);
tty_window_offset(&c->tty, &ox, &oy, &sx, &sy);
m->x += ox;
if (m->statusat == 0) {
if (m->y < m->statuslines)
m->x = m->y = UINT_MAX;
else
m->y = m->y - m->statuslines + oy;
} else if (m->statusat > 0 && m->y >= (u_int)m->statusat)
m->x = m->y = UINT_MAX;
else
m->y += oy;
}
if (menu_key(c, w->menu, &new_event) == 1)
menu_close(w);
return (1);
}
/* Handle a key event. */
static int
server_client_handle_key0(struct client *c, struct key_event *event,
struct cmdq_item *after, struct cmdq_item **next)
{
struct session *s = c->session;
struct cmdq_item *item;
struct window_pane *wp;
/* Check the client is good to accept input. */
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
return (0);
if (event->key == KEYC_REPORT_LIGHT_THEME) {
server_client_report_theme(c, THEME_LIGHT);
return (0);
}
if (event->key == KEYC_REPORT_DARK_THEME) {
server_client_report_theme(c, THEME_DARK);
return (0);
}
/*
* Dead panes waiting for a key, modal cancel keys, panes capturing all keys
* and the command prompt are special cases. The queue might be blocked so
* they need to be processed immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
if (c->message_ignore_keys)
return (0);
status_message_clear(c);
}
wp = s->curw->window->active;
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (wp != NULL &&
wp == wp->window->modal &&
(wp->flags & PANE_CLOSEONCANCEL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL))) {
server_kill_pane(wp);
return (0);
}
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
return (0);
}
}
if (server_client_handle_menu_key(c, event))
return (0);
if (c->prompt != NULL) {
switch (status_prompt_key(c, event->key, &event->m)) {
case PROMPT_KEY_HANDLED:
case PROMPT_KEY_CLOSE:
return (0);
case PROMPT_KEY_NOT_HANDLED:
case PROMPT_KEY_MOVE:
break;
}
}
wp = s->curw->window->active;
if (wp == NULL || !window_pane_has_prompt(wp)) {
TAILQ_FOREACH(wp, &s->curw->window->panes, entry) {
if (window_pane_has_prompt(wp) &&
window_pane_is_visible(wp))
break;
}
}
if (wp != NULL &&
window_pane_has_prompt(wp) &&
window_pane_is_visible(wp)) {
switch (window_pane_prompt_key(wp, c, event->key,
&event->m)) {
case PROMPT_KEY_HANDLED:
case PROMPT_KEY_CLOSE:
case PROMPT_KEY_MOVE:
return (0);
case PROMPT_KEY_NOT_HANDLED:
if (KEYC_IS_MOUSE(event->key))
return (0);
break;
}
}
}
/*
* Add the key to the queue so it happens after any commands queued by
* previous keys.
*/
item = cmdq_get_callback(server_client_key_callback, event);
if (after != NULL) {
event->client = c;
c->references++;
item = cmdq_insert_after(after, item);
if (next != NULL)
*next = item;
return (1);
}
cmdq_append(c, item);
return (1);
}
/* Handle key and insert at end of queue. */
int
server_client_handle_key(struct client *c, struct key_event *event)
{
return (server_client_handle_key0(c, event, NULL, NULL));
}
/* Handle key and insert after another item. */
int
server_client_handle_key_after(struct client *c, struct key_event *event,
struct cmdq_item *after, struct cmdq_item **next)
{
return (server_client_handle_key0(c, event, after, next));
}
/* Client functions that need to happen every loop. */
void
server_client_loop(void)
{
struct client *c;
struct window *w;
struct window_pane *wp;
struct window_mode_entry *wme;
/* Check for window resize. This is done before redrawing. */
RB_FOREACH(w, windows, &windows)
server_client_check_window_resize(w);
/* Notify modes that pane styles may have changed. */
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_STYLECHANGED) {
wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL &&
wme->mode->style_changed != NULL)
wme->mode->style_changed(wme);
}
}
}
/* Check clients. */
TAILQ_FOREACH(c, &clients, entry) {
server_client_check_exit(c, 0);
if (c->session != NULL && c->session->curw != NULL) {
server_client_check_modes(c);
server_client_check_redraw(c);
server_client_reset_state(c);
}
}
/*
* Any windows will have been redrawn as part of clients, so clear
* their flags now. A client whose redraw was deferred this pass
* (waiting for outstanding tty output to drain) has already
* escalated to CLIENT_REDRAWWINDOW or CLIENT_REDRAWSCROLLBARS in
* server_client_check_redraw() to cover whatever it is about to
* lose here, so PANE_REDRAW/PANE_REDRAWSCROLLBAR and window damage
* can simply be cleared unconditionally.
*/
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->fd != -1) {
server_client_check_pane_resize(wp);
server_client_check_pane_buffer(wp);
}
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY);
}
redraw_free_damage(w);
check_window_name(w);
}
/* Send theme updates. */
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry)
window_pane_send_theme_update(wp);
}
}
/* Check if window needs to be resized. */
static void
server_client_check_window_resize(struct window *w)
{
struct winlink *wl;
if (~w->flags & WINDOW_RESIZE)
return;
TAILQ_FOREACH(wl, &w->winlinks, wentry) {
if (wl->session->attached != 0 && wl->session->curw == wl)
break;
}
if (wl == NULL)
return;
log_debug("%s: resizing window @%u", __func__, w->id);
resize_window(w, w->new_sx, w->new_sy, w->new_xpixel, w->new_ypixel);
}
/* Resize timer event. */
static void
server_client_resize_timer(__unused int fd, __unused short events, void *data)
{
struct window_pane *wp = data;
log_debug("%s: %%%u resize timer expired", __func__, wp->id);
evtimer_del(&wp->resize_timer);
}
/* Check if pane should be resized. */
static void
server_client_check_pane_resize(struct window_pane *wp)
{
struct window_pane_resize *r, *first, *last;
struct timeval tv = { .tv_usec = 250000 };
if (TAILQ_EMPTY(&wp->resize_queue))
return;
if (!event_initialized(&wp->resize_timer))
evtimer_set(&wp->resize_timer, server_client_resize_timer, wp);
if (evtimer_pending(&wp->resize_timer, NULL))
return;
log_debug("%s: %%%u needs to be resized", __func__, wp->id);
TAILQ_FOREACH(r, &wp->resize_queue, entry) {
log_debug("queued resize: %ux%u -> %ux%u", r->osx, r->osy,
r->sx, r->sy);
}
/*
* There are three cases that matter:
*
* - Only one resize. It can just be applied.
*
* - Multiple resizes and the ending size is different from the
* starting size. We can discard all resizes except the most recent.
*
* - Multiple resizes and the ending size is the same as the starting
* size. We must resize at least twice to force the application to
* redraw. So apply the first and leave the last on the queue for
* next time.
*/
first = TAILQ_FIRST(&wp->resize_queue);
last = TAILQ_LAST(&wp->resize_queue, window_pane_resizes);
if (first == last) {
/* Only one resize. */
window_pane_send_resize(wp, first->sx, first->sy);
TAILQ_REMOVE(&wp->resize_queue, first, entry);
free(first);
} else if (last->sx != first->osx || last->sy != first->osy) {
/* Multiple resizes ending up with a different size. */
window_pane_send_resize(wp, last->sx, last->sy);
window_pane_clear_resizes(wp, NULL);
} else {
/*
* Multiple resizes ending up with the same size. There will
* not be more than one to the same size in succession so we
* can just use the last-but-one on the list and leave the last
* for later. We reduce the time until the next check to avoid
* a long delay between the resizes.
*/
r = TAILQ_PREV(last, window_pane_resizes, entry);
window_pane_send_resize(wp, r->sx, r->sy);
window_pane_clear_resizes(wp, last);
tv.tv_usec = 10000;
}
evtimer_add(&wp->resize_timer, &tv);
}
/* Check pane buffer size. */
static void
server_client_check_pane_buffer(struct window_pane *wp)
{
struct evbuffer *evb = wp->event->input;
size_t minimum;
struct client *c;
struct window_pane_offset *wpo;
int off = 1, flag;
u_int attached_clients = 0;
size_t new_size;
/*
* Work out the minimum used size. This is the most that can be removed
* from the buffer.
*/
minimum = wp->offset.used;
if (wp->pipe_fd != -1 && wp->pipe_offset.used < minimum)
minimum = wp->pipe_offset.used;
TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL)
continue;
attached_clients++;
if (~c->flags & CLIENT_CONTROL) {
off = 0;
continue;
}
wpo = control_pane_offset(c, wp, &flag);
if (wpo == NULL) {
if (!flag)
off = 0;
continue;
}
if (!flag)
off = 0;
window_pane_get_new_data(wp, wpo, &new_size);
log_debug("%s: %s has %zu bytes used and %zu left for %%%u",
__func__, c->name, wpo->used - wp->base_offset, new_size,
wp->id);
if (wpo->used < minimum)
minimum = wpo->used;
}
if (attached_clients == 0)
off = 0;
minimum -= wp->base_offset;
if (minimum == 0)
goto out;
/* Drain the buffer. */
log_debug("%s: %%%u has %zu minimum (of %zu) bytes used", __func__,
wp->id, minimum, EVBUFFER_LENGTH(evb));
evbuffer_drain(evb, minimum);
/*
* Adjust the base offset. If it would roll over, all the offsets into
* the buffer need to be adjusted.
*/
if (wp->base_offset > SIZE_MAX - minimum) {
log_debug("%s: %%%u base offset has wrapped", __func__, wp->id);
wp->offset.used -= wp->base_offset;
if (wp->pipe_fd != -1)
wp->pipe_offset.used -= wp->base_offset;
TAILQ_FOREACH(c, &clients, entry) {
if (c->session == NULL || (~c->flags & CLIENT_CONTROL))
continue;
wpo = control_pane_offset(c, wp, &flag);
if (wpo != NULL && !flag)
wpo->used -= wp->base_offset;
}
wp->base_offset = minimum;
} else
wp->base_offset += minimum;
out:
/*
* If there is data remaining, and there are no clients able to consume
* it, do not read any more. This is true when there are attached
* clients, all of which are control clients which are not able to
* accept any more data.
*/
log_debug("%s: pane %%%u is %s", __func__, wp->id, off ? "off" : "on");
if (off)
bufferevent_disable(wp->event, EV_READ);
else
bufferevent_enable(wp->event, EV_READ);
}
/* Move cursor for pane prompt. */
static int
server_client_prompt_cursor(struct client *c, struct window_pane *wp, int *mode,
u_int *cx, u_int *cy)
{
struct tty *tty = &c->tty;
struct visible_ranges *r;
u_int ox, oy, sx, sy;
int px, py;
if (!window_pane_has_prompt(wp))
return (0);
*mode &= ~MODE_CURSOR;
tty_window_offset(tty, &ox, &oy, &sx, &sy);
if (status_at_line(c) == 0)
py = wp->yoff;
else
py = wp->yoff + wp->sy - 1;
px = wp->xoff + wp->prompt_cx;
if (px < (int)ox || px > (int)(ox + sx) ||
py < (int)oy || py > (int)(oy + sy))
return (1);
*cx = px - ox;
*cy = py - oy;
r = window_visible_ranges(wp, *cx, *cy, 1, NULL);
if (window_position_is_visible(r, *cx)) {
if (status_at_line(c) == 0)
*cy += status_line_size(c);
*mode |= MODE_CURSOR;
}
return (1);
}
/*
* Update cursor position and mode settings. The scroll region and attributes
* are cleared when idle (waiting for an event) as this is the most likely time
* a user may interrupt tmux, for example with ~^Z in ssh(1). This is a
* compromise between excessive resets and likelihood of an interrupt.
*
* tty_region/tty_reset/tty_update_mode already take care of not resetting
* things that are already in their default state.
*/
static void
server_client_reset_state(struct client *c)
{
struct tty *tty = &c->tty;
struct window *w = c->session->curw->window;
struct window_pane *wp = w->active, *loop;
struct screen *s = NULL;
struct options *oo = c->session->options;
int mode = 0, cursor, flags, pane_mode = 0;
u_int cx = 0, cy = 0, ox, oy, sx, sy, prompt = 0;
u_int sb_w;
struct visible_ranges *r;
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
/* Disable the block flag. */
flags = (tty->flags & TTY_BLOCK);
tty->flags &= ~TTY_BLOCK;
/* Get mode from the menu if any, else from the screen. */
if (w->menu != NULL) {
menu_get_cursor(w->menu, &cx, &cy);
s = menu_screen(w->menu);
} else if (wp != NULL && c->prompt == NULL)
s = wp->screen;
else
s = c->status.active;
if (s != NULL)
mode = s->mode;
if (log_get_level() != 0) {
log_debug("%s: client %s mode %s", __func__, c->name,
screen_mode_to_string(mode));
}
/* Reset region and margin. */
tty_region_off(tty);
tty_margin_off(tty);
/* Move cursor to pane cursor and offset. */
if (c->prompt != NULL) {
prompt = 1;
status_prompt_cursor(c, &cx, &cy);
} else if (wp != NULL) {
if (w->menu != NULL) {
tty_window_offset(tty, &ox, &oy, &sx, &sy);
if (cx < ox || cx >= ox + sx ||
cy < oy || cy >= oy + sy)
mode &= ~MODE_CURSOR;
else {
cx -= ox;
cy -= oy;
if (status_at_line(c) == 0)
cy += status_line_size(c);
}
prompt = 1;
} else {
prompt = server_client_prompt_cursor(c, wp, &mode, &cx,
&cy);
}
if (!prompt) {
cursor = 0;
pane_mode = wp->base.mode;
tty_window_offset(tty, &ox, &oy, &sx, &sy);
if (wp->xoff + (int)s->cx >= (int)ox &&
wp->xoff + (int)s->cx <= (int)ox + (int)sx &&
wp->yoff + (int)s->cy >= (int)oy &&
wp->yoff + (int)s->cy <= (int)oy + (int)sy) {
cursor = 1;
cx = wp->xoff + (int)s->cx - (int)ox;
cy = wp->yoff + (int)s->cy - (int)oy;
r = window_visible_ranges(wp, cx, cy, 1, NULL);
if (!window_position_is_visible(r, cx))
cursor = 0;
if (window_pane_scrollbar_overlay_visible(wp)) {
sb_w = wp->scrollbar_style.width;
if (sb_w > wp->sx)
sb_w = wp->sx;
if (sb_w != 0 &&
w->sb_pos == PANE_SCROLLBARS_LEFT) {
if (s->cx < sb_w)
cursor = 0;
} else if (sb_w != 0 &&
s->cx >= wp->sx - sb_w)
cursor = 0;
}
if (status_at_line(c) == 0)
cy += status_line_size(c);
}
if (!cursor)
mode &= ~MODE_CURSOR;
}
} else if (s == NULL)
mode &= ~MODE_CURSOR;
if (~pane_mode & MODE_SYNC) {
log_debug("%s: cursor to %u,%u", __func__, cx, cy);
tty_cursor(tty, cx, cy);
} else {
mode &= ~CURSOR_MODES;
mode |= tty->mode & CURSOR_MODES;
s = NULL;
}
/*
* Set mouse mode if requested. To support dragging, always use button
* mode. For focus-follows-mouse, we need all-motion mode to receive
* movement events.
*/
if (options_get_number(oo, "mouse")) {
if (w->menu == NULL) {
mode &= ~ALL_MOUSE_MODES;
TAILQ_FOREACH(loop, &w->panes, entry) {
if (loop->screen->mode & MODE_MOUSE_ALL)
mode |= MODE_MOUSE_ALL;
}
}
if (options_get_number(oo, "focus-follows-mouse") ||
w->sb == PANE_SCROLLBARS_MODAL ||
w->sb == PANE_SCROLLBARS_AUTOHIDE)
mode |= MODE_MOUSE_ALL;
else if (~mode & MODE_MOUSE_ALL)
mode |= MODE_MOUSE_BUTTON;
}
/* Clear bracketed paste mode if at the prompt. */
if (prompt)
mode &= ~MODE_BRACKETPASTE;
/* Set the terminal mode and reset attributes. */
tty_update_mode(tty, mode, s);
tty_reset(tty);
/* All writing must be done, send a sync end (if it was started). */
tty_sync_end(tty);
tty->flags |= flags;
}
/* Repeat time callback. */
static void
server_client_repeat_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
if (c->flags & CLIENT_REPEAT) {
server_client_set_key_table(c, NULL);
c->flags &= ~CLIENT_REPEAT;
server_status_client(c);
}
}
/* Double-click callback. */
static void
server_client_click_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
struct key_event *event;
log_debug("click timer expired");
if (c->flags & CLIENT_TRIPLECLICK) {
/*
* Waiting for a third click that hasn't happened, so this must
* have been a double click.
*/
event = xcalloc(1, sizeof *event);
event->key = KEYC_DOUBLECLICK;
memcpy(&event->m, &c->click_event, sizeof event->m);
if (!server_client_handle_key(c, event)) {
free(event->buf);
free(event);
}
}
c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK);
}
/* Start client exit timer. */
static void
server_client_start_exit_timer(struct client *c)
{
struct timeval tv = { .tv_sec = 10 };
if (!evtimer_pending(&c->exit_timer, NULL))
evtimer_add(&c->exit_timer, &tv);
}
/* Exit timer has expired: stop waiting for the client. */
static void
server_client_exit_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_EXITED) {
log_debug("%s: %s took too long to exit", __func__, c->name);
server_client_lost(c);
} else if (c->flags & CLIENT_EXIT) {
log_debug("%s: %s took too long to flush", __func__, c->name);
server_client_check_exit(c, 1);
}
}
/* Check if client should be exited, abandoning buffered output if forced. */
static void
server_client_check_exit(struct client *c, int force)
{
struct client_file *cf;
const char *name = c->exit_session;
char *data;
size_t size, msize;
if (c->flags & (CLIENT_DEAD|CLIENT_EXITED))
return;
if (~c->flags & CLIENT_EXIT)
return;
if (c->flags & CLIENT_CONTROL) {
if (force)
control_discard_all(c);
else {
control_discard(c);
if (!control_all_done(c)) {
server_client_start_exit_timer(c);
return;
}
}
}
if (!force) {
RB_FOREACH(cf, client_files, &c->files) {
if (EVBUFFER_LENGTH(cf->buffer) != 0) {
server_client_start_exit_timer(c);
return;
}
}
}
c->flags |= CLIENT_EXITED;
evtimer_del(&c->exit_timer);
server_client_start_exit_timer(c);
switch (c->exit_type) {
case CLIENT_EXIT_RETURN:
if (c->exit_message != NULL)
msize = strlen(c->exit_message) + 1;
else
msize = 0;
size = (sizeof c->retval) + msize;
data = xmalloc(size);
memcpy(data, &c->retval, sizeof c->retval);
if (c->exit_message != NULL)
memcpy(data + sizeof c->retval, c->exit_message, msize);
proc_send(c->peer, MSG_EXIT, -1, data, size);
free(data);
break;
case CLIENT_EXIT_SHUTDOWN:
proc_send(c->peer, MSG_SHUTDOWN, -1, NULL, 0);
break;
case CLIENT_EXIT_DETACH:
proc_send(c->peer, c->exit_msgtype, -1, name, strlen(name) + 1);
break;
}
}
/* Redraw timer callback. */
static void
server_client_redraw_timer(__unused int fd, __unused short events,
__unused void *data)
{
log_debug("redraw timer fired");
}
/*
* Check if modes need to be updated. Only modes in the current window are
* updated and it is done when the status line is redrawn.
*/
static void
server_client_check_modes(struct client *c)
{
struct window *w = c->session->curw->window;
struct window_pane *wp;
struct window_mode_entry *wme;
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
if (~c->flags & CLIENT_REDRAWSTATUS)
return;
TAILQ_FOREACH(wp, &w->panes, entry) {
wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL && wme->mode->update != NULL)
wme->mode->update(wme);
}
}
/* Check if any panes need to be redrawn. */
static int
server_client_any_pane_redraw(struct client *c)
{
struct session *s = c->session;
struct window *w = s->curw->window;
struct window_pane *wp;
if (c->flags & CLIENT_REDRAWWINDOW)
return (1);
if (!TAILQ_EMPTY(&w->damage))
return (1);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
return (1);
}
return (0);
}
/* Check for client redraws. */
static void
server_client_check_redraw(struct client *c)
{
struct session *s = c->session;
struct tty *tty = &c->tty;
struct window *w = s->curw->window;
struct window_pane *wp;
int needed, tflags, mode = tty->mode;
struct timeval tv = { .tv_usec = 1000 };
static struct event ev;
size_t n;
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
log_debug("%s: redraw%s%s%s%s", c->name,
(c->flags & CLIENT_REDRAWWINDOW) ? " window" : "",
(c->flags & CLIENT_REDRAWSTATUS) ? " status" : "",
(c->flags & CLIENT_REDRAWBORDERS) ? " borders" : "",
(c->flags & CLIENT_REDRAWMENU) ? " menu" : "");
}
/* Work out if a redraw is actually needed. */
needed = 0;
if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS))
needed = 1;
else if (server_client_any_pane_redraw(c))
needed = 1;
if (!needed) {
c->flags &= ~CLIENT_STATUSFORCE;
return;
}
/*
* If there is outstanding data, defer the redraw until it has been
* consumed. We can just add a timer to get out of the event loop and
* end up back here. server_client_loop() clears PANE_REDRAW,
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
* so escalate to a coarser, persistent client flag that survives
* that clear and forces a full catch-up redraw once this client is
* unblocked, rather than trying to keep the fine-grained state
* around for a retry.
*
* If a synchronized-output frame is open, discount anything queued
* since it started (down to sync_offset, the length when it opened):
* those bytes are already part of the frame this pass is committed
* to flushing (see tty_sync_start()), not a reason to defer this
* pass's redraw - without this, a mouse-drag callback that itself
* opens the frame before writing anything would see its own
* just-queued bytes as "outstanding output" and defer against
* itself every single motion event.
*/
n = EVBUFFER_LENGTH(tty->out);
if ((tty->flags & TTY_SYNCING) && n > tty->sync_offset)
n = tty->sync_offset;
if (n != 0 || (tty->flags & TTY_BLOCK)) {
if (n != 0)
log_debug("%s: redraw deferred (%zu left)", c->name, n);
else
log_debug("%s: redraw deferred (blocked)", c->name);
if (!evtimer_initialized(&ev))
evtimer_set(&ev, server_client_redraw_timer, NULL);
if (!evtimer_pending(&ev, NULL)) {
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (!TAILQ_EMPTY(&w->damage))
c->flags |= CLIENT_REDRAWWINDOW;
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
break;
}
if (wp->flags & PANE_REDRAWSCROLLBAR)
c->flags |= CLIENT_REDRAWSCROLLBARS;
}
return;
}
/* Unfreeze the tty and turn off the cursor. */
log_debug("%s: redraw needed", c->name);
tflags = tty->flags & (TTY_BLOCK|TTY_FREEZE|TTY_NOCURSOR);
tty->flags = (tty->flags & ~(TTY_BLOCK|TTY_FREEZE))|TTY_NOCURSOR;
/*
* If not redrawing the entire window, check whether each pane needs to
* be redrawn.
*/
if (~c->flags & CLIENT_REDRAWWINDOW) {
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
log_debug("%s: redraw pane %%%u", __func__,
wp->id);
redraw_pane(c, wp);
} else if ((wp->flags & PANE_REDRAWSCROLLBAR) ||
(c->flags & CLIENT_REDRAWSCROLLBARS)) {
log_debug("%s: redraw scrollbar %%%u", __func__,
wp->id);
redraw_pane_scrollbar(c, wp);
}
}
/*
* Window damage is also what makes server_client_any_pane_
* redraw() decide a redraw is needed at all, independently of
* any CLIENT_ALLREDRAWFLAGS bit. Every current damage source
* happens to set one of those flags too, so the block below
* always consumes it - but consume it here too in case that
* ever stops holding, since server_client_loop() clears
* window damage unconditionally every pass regardless of
* whether it was actually drawn.
*/
if (!TAILQ_EMPTY(&w->damage) &&
(c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
redraw_client_damage(c);
}
/*
* Set titles etc and do the redraw if there are redraw flags (and we
* aren't here just to redraw panes).
*/
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
if (options_get_number(s->options, "set-titles")) {
server_client_set_title(c);
server_client_set_path(c);
}
server_client_set_progress_bar(c);
redraw_screen(c);
redraw_client_damage(c);
}
/* Put the tty back how it was. */
tty->flags = (tty->flags & ~TTY_NOCURSOR)|(tflags & TTY_NOCURSOR);
tty_update_mode(tty, mode, NULL);
tty->flags = (tty->flags & ~(TTY_BLOCK|TTY_FREEZE|TTY_NOCURSOR))|tflags;
/*
* All the redraw flags can now be cleared. Also record how many bytes
* were written.
*/
c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS|
CLIENT_STATUSFORCE);
c->redraw = EVBUFFER_LENGTH(tty->out);
log_debug("%s: redraw added %zu bytes", c->name, c->redraw);
}
/* Set client title. */
static void
server_client_set_title(struct client *c)
{
struct session *s = c->session;
const char *template;
char *title;
struct format_tree *ft;
template = options_get_string(s->options, "set-titles-string");
ft = format_create(c, NULL, FORMAT_NONE, 0);
format_defaults(ft, c, NULL, NULL, NULL);
title = format_expand_time(ft, template);
if (c->title == NULL || strcmp(title, c->title) != 0) {
free(c->title);
c->title = xstrdup(title);
tty_set_title(&c->tty, c->title);
}
free(title);
format_free(ft);
}
/* Set client path. */
static void
server_client_set_path(struct client *c)
{
struct session *s = c->session;
const char *path;
if (s->curw == NULL || s->curw->window->active == NULL)
return;
if (s->curw->window->active->base.path == NULL)
path = "";
else
path = s->curw->window->active->base.path;
if (c->path == NULL || strcmp(path, c->path) != 0) {
free(c->path);
c->path = xstrdup(path);
tty_set_path(&c->tty, c->path);
}
}
/* Set client progress bar. */
static void
server_client_set_progress_bar(struct client *c)
{
struct session *s = c->session;
struct progress_bar *pane_pb;
if (s->curw == NULL || s->curw->window->active == NULL)
return;
pane_pb = &s->curw->window->active->base.progress_bar;
if (pane_pb->state == c->progress_bar.state &&
pane_pb->progress == c->progress_bar.progress)
return;
memcpy(&c->progress_bar, pane_pb, sizeof c->progress_bar);
tty_set_progress_bar(&c->tty, &c->progress_bar);
}
/* Dispatch message from client. */
static void
server_client_dispatch(struct imsg *imsg, void *arg)
{
struct client *c = arg;
ssize_t datalen;
struct session *s;
u_int old_sx, old_sy;
if (c->flags & CLIENT_DEAD)
return;
if (imsg == NULL) {
server_client_lost(c);
return;
}
datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
switch (imsg->hdr.type) {
case MSG_IDENTIFY_CLIENTPID:
case MSG_IDENTIFY_CWD:
case MSG_IDENTIFY_ENVIRON:
case MSG_IDENTIFY_FEATURES:
case MSG_IDENTIFY_FLAGS:
case MSG_IDENTIFY_LONGFLAGS:
case MSG_IDENTIFY_STDIN:
case MSG_IDENTIFY_STDOUT:
case MSG_IDENTIFY_TERM:
case MSG_IDENTIFY_TERMINFO:
case MSG_IDENTIFY_TTYNAME:
case MSG_IDENTIFY_DONE:
if (server_client_dispatch_identify(c, imsg) != 0)
goto bad;
break;
case MSG_COMMAND:
if (server_client_dispatch_command(c, imsg) != 0)
goto bad;
break;
case MSG_RESIZE:
if (datalen != 0)
goto bad;
if (c->flags & CLIENT_CONTROL)
break;
server_client_update_latest(c);
old_sx = c->tty.sx;
old_sy = c->tty.sy;
tty_resize(&c->tty);
tty_repeat_requests(&c->tty, 0);
recalculate_sizes();
server_redraw_client(c);
if (c->session != NULL)
server_client_fire_resized(c, old_sx, old_sy);
break;
case MSG_EXITING:
if (datalen != 0)
goto bad;
server_client_set_session(c, NULL);
recalculate_sizes();
tty_close(&c->tty);
proc_send(c->peer, MSG_EXITED, -1, NULL, 0);
break;
case MSG_WAKEUP:
case MSG_UNLOCK:
if (datalen != 0)
goto bad;
if (!(c->flags & CLIENT_SUSPENDED))
break;
c->flags &= ~CLIENT_SUSPENDED;
if (c->fd == -1 || c->session == NULL) /* exited already */
break;
s = c->session;
if (gettimeofday(&c->activity_time, NULL) != 0)
fatal("gettimeofday failed");
tty_start_tty(&c->tty);
server_redraw_client(c);
recalculate_sizes();
if (s != NULL)
session_update_activity(s, &c->activity_time);
break;
case MSG_SHELL:
if (datalen != 0)
goto bad;
if (server_client_dispatch_shell(c) != 0)
goto bad;
break;
case MSG_WRITE_READY:
if (file_write_ready(&c->files, imsg) != 0)
goto bad;
break;
case MSG_WRITE_DONE:
if (file_write_done(&c->files, imsg) != 0)
goto bad;
break;
case MSG_READ:
if (file_read_data(&c->files, imsg) != 0)
goto bad;
break;
case MSG_READ_DONE:
if (file_read_done(&c->files, imsg) != 0)
goto bad;
break;
}
return;
bad:
log_debug("client %p invalid message type %d", c, imsg->hdr.type);
proc_kill_peer(c->peer);
}
/* Callback when command is not allowed. */
static enum cmd_retval
server_client_read_only(struct cmdq_item *item, __unused void *data)
{
cmdq_error(item, "client is read-only");
return (CMD_RETURN_ERROR);
}
/* Callback for default command. */
static enum cmd_retval
server_client_default_command(struct cmdq_item *item, __unused void *data)
{
struct client *c = cmdq_get_client(item);
struct cmd_list *cmdlist;
struct cmdq_item *new_item;
cmdlist = options_get_command(global_options, "default-client-command");
if ((c->flags & CLIENT_READONLY) &&
!cmd_list_all_have(cmdlist, CMD_READONLY))
new_item = cmdq_get_callback(server_client_read_only, NULL);
else
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_insert_after(item, new_item);
return (CMD_RETURN_NORMAL);
}
/* Callback when command is done. */
static enum cmd_retval
server_client_command_done(struct cmdq_item *item, __unused void *data)
{
struct client *c = cmdq_get_client(item);
if (~c->flags & CLIENT_ATTACHED)
c->flags |= CLIENT_EXIT;
else if (~c->flags & CLIENT_EXIT) {
if (c->flags & CLIENT_CONTROL)
control_ready(c);
tty_send_requests(&c->tty);
}
return (CMD_RETURN_NORMAL);
}
/* Handle command message. */
static int
server_client_dispatch_command(struct client *c, struct imsg *imsg)
{
struct msg_command data;
char *buf;
size_t len;
int argc = 0;
char **argv, *cause;
struct cmd_parse_result *pr;
struct args_value *values;
struct cmdq_item *new_item;
if (c->flags & CLIENT_EXIT)
return (0);
if (imsg->hdr.len - IMSG_HEADER_SIZE < sizeof data)
return (-1);
memcpy(&data, imsg->data, sizeof data);
buf = (char *)imsg->data + sizeof data;
len = imsg->hdr.len - IMSG_HEADER_SIZE - sizeof data;
if (len > 0 && buf[len - 1] != '\0')
return (-1);
if (cmd_unpack_argv(buf, len, data.argc, &argv) != 0) {
cause = xstrdup("command too long");
goto error;
}
argc = data.argc;
if (argc == 0) {
new_item = cmdq_get_callback(server_client_default_command,
NULL);
} else {
values = args_from_vector(argc, argv);
pr = cmd_parse_from_arguments(values, argc, NULL);
switch (pr->status) {
case CMD_PARSE_ERROR:
cause = pr->error;
goto error;
case CMD_PARSE_SUCCESS:
break;
}
args_free_values(values, argc);
free(values);
cmd_free_argv(argc, argv);
if ((c->flags & CLIENT_READONLY) &&
!cmd_list_all_have(pr->cmdlist, CMD_READONLY)) {
new_item = cmdq_get_callback(server_client_read_only,
NULL);
} else
new_item = cmdq_get_command(pr->cmdlist, NULL);
cmd_list_free(pr->cmdlist);
}
cmdq_append(c, new_item);
cmdq_append(c, cmdq_get_callback(server_client_command_done, NULL));
return (0);
error:
cmd_free_argv(argc, argv);
cmdq_append(c, cmdq_get_error(cause));
free(cause);
c->flags |= CLIENT_EXIT;
return (0);
}
/* Handle identify message. */
static int
server_client_dispatch_identify(struct client *c, struct imsg *imsg)
{
const char *data, *home;
size_t datalen;
int flags, feat;
uint64_t longflags;
char *name;
if (c->flags & CLIENT_IDENTIFIED)
return (-1);
data = imsg->data;
datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
switch (imsg->hdr.type) {
case MSG_IDENTIFY_FEATURES:
if (datalen != sizeof feat)
return (-1);
memcpy(&feat, data, sizeof feat);
c->term_features |= feat;
log_debug("client %p IDENTIFY_FEATURES %s", c,
tty_get_features(feat));
break;
case MSG_IDENTIFY_FLAGS:
if (datalen != sizeof flags)
return (-1);
memcpy(&flags, data, sizeof flags);
c->flags |= flags;
log_debug("client %p IDENTIFY_FLAGS %#x", c, flags);
break;
case MSG_IDENTIFY_LONGFLAGS:
if (datalen != sizeof longflags)
return (-1);
memcpy(&longflags, data, sizeof longflags);
c->flags |= longflags;
log_debug("client %p IDENTIFY_LONGFLAGS %#llx", c,
(unsigned long long)longflags);
break;
case MSG_IDENTIFY_TERM:
if (datalen == 0 || data[datalen - 1] != '\0')
return (-1);
c->term_name = xstrdup(data);
log_debug("client %p IDENTIFY_TERM %s", c, data);
break;
case MSG_IDENTIFY_TERMINFO:
if (datalen == 0 || data[datalen - 1] != '\0')
return (-1);
c->term_caps = xreallocarray(c->term_caps, c->term_ncaps + 1,
sizeof *c->term_caps);
c->term_caps[c->term_ncaps++] = xstrdup(data);
log_debug("client %p IDENTIFY_TERMINFO %s", c, data);
break;
case MSG_IDENTIFY_TTYNAME:
if (datalen == 0 || data[datalen - 1] != '\0')
return (-1);
c->ttyname = xstrdup(data);
log_debug("client %p IDENTIFY_TTYNAME %s", c, data);
break;
case MSG_IDENTIFY_CWD:
if (datalen == 0 || data[datalen - 1] != '\0')
return (-1);
if (access(data, X_OK) == 0)
c->cwd = xstrdup(data);
else if ((home = find_home()) != NULL)
c->cwd = xstrdup(home);
else
c->cwd = xstrdup("/");
log_debug("client %p IDENTIFY_CWD %s", c, data);
break;
case MSG_IDENTIFY_STDIN:
if (datalen != 0)
return (-1);
c->fd = imsg_get_fd(imsg);
log_debug("client %p IDENTIFY_STDIN %d", c, c->fd);
break;
case MSG_IDENTIFY_STDOUT:
if (datalen != 0)
return (-1);
c->out_fd = imsg_get_fd(imsg);
log_debug("client %p IDENTIFY_STDOUT %d", c, c->out_fd);
break;
case MSG_IDENTIFY_ENVIRON:
if (datalen == 0 || data[datalen - 1] != '\0')
return (-1);
if (strchr(data, '=') != NULL)
environ_put(c->environ, data, 0);
log_debug("client %p IDENTIFY_ENVIRON %s", c, data);
break;
case MSG_IDENTIFY_CLIENTPID:
if (datalen != sizeof c->pid)
return (-1);
memcpy(&c->pid, data, sizeof c->pid);
log_debug("client %p IDENTIFY_CLIENTPID %ld", c, (long)c->pid);
break;
default:
break;
}
if (imsg->hdr.type != MSG_IDENTIFY_DONE)
return (0);
c->flags |= CLIENT_IDENTIFIED;
if (c->term_name == NULL || *c->term_name == '\0') {
free(c->term_name);
c->term_name = xstrdup("unknown");
}
if (c->ttyname != NULL && *c->ttyname != '\0')
name = xstrdup(c->ttyname);
else
xasprintf(&name, "client-%ld", (long)c->pid);
c->name = name;
log_debug("client %p name is %s", c, c->name);
#ifdef __CYGWIN__
c->fd = open(c->ttyname, O_RDWR|O_NOCTTY);
c->out_fd = dup(c->fd);
#endif
if (c->flags & CLIENT_CONTROL)
control_start(c);
else if (c->fd != -1) {
if (tty_init(&c->tty, c) != 0) {
close(c->fd);
c->fd = -1;
} else {
tty_resize(&c->tty);
c->flags |= CLIENT_TERMINAL;
}
if (c->out_fd != -1)
close(c->out_fd);
c->out_fd = -1;
}
if (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL))
events_fire_client("client-created", c);
/* If pasting has taken too long, turn it off. */
if (c->flags & (CLIENT_BRACKETPASTING|CLIENT_ASSUMEPASTING) &&
current_time - c->paste_time > CLIENT_PASTE_TIME_LIMIT) {
log_debug("%s: paste time limit exceeded", c->name);
c->flags &= ~(CLIENT_BRACKETPASTING|CLIENT_ASSUMEPASTING);
}
/*
* If this is the first client, load configuration files. Any later
* clients are allowed to continue with their command even if the
* config has not been loaded - they might have been run from inside it
*/
if ((~c->flags & CLIENT_EXIT) &&
!cfg_finished &&
c == TAILQ_FIRST(&clients))
start_cfg();
return (0);
}
/* Handle shell message. */
static int
server_client_dispatch_shell(struct client *c)
{
const char *shell;
shell = options_get_string(global_s_options, "default-shell");
if (!checkshell(shell))
shell = _PATH_BSHELL;
proc_send(c->peer, MSG_SHELL, -1, shell, strlen(shell) + 1);
proc_kill_peer(c->peer);
return (0);
}
/* Get client working directory. */
const char *
server_client_get_cwd(struct client *c, struct session *s)
{
const char *home;
if (!cfg_finished && cfg_client != NULL)
return (cfg_client->cwd);
if (c != NULL && c->session == NULL && c->cwd != NULL)
return (c->cwd);
if (s != NULL && s->cwd != NULL)
return (s->cwd);
if (c != NULL && (s = c->session) != NULL && s->cwd != NULL)
return (s->cwd);
if ((home = find_home()) != NULL)
return (home);
return ("/");
}
/* Get control client flags. */
static uint64_t
server_client_control_flags(struct client *c, const char *next)
{
if (strcmp(next, "pause-after") == 0) {
c->pause_age = 0;
return (CLIENT_CONTROL_PAUSEAFTER);
}
if (sscanf(next, "pause-after=%u", &c->pause_age) == 1) {
c->pause_age *= 1000;
return (CLIENT_CONTROL_PAUSEAFTER);
}
if (strcmp(next, "no-output") == 0)
return (CLIENT_CONTROL_NOOUTPUT);
if (strcmp(next, "wait-exit") == 0)
return (CLIENT_CONTROL_WAITEXIT);
if (strcmp(next, "new-layouts") == 0)
return (CLIENT_CONTROL_NEWLAYOUTS);
return (0);
}
/* Set client flags. */
void
server_client_set_flags(struct client *c, const char *flags)
{
char *s, *copy, *next;
uint64_t flag;
int not;
s = copy = xstrdup(flags);
while ((next = strsep(&s, ",")) != NULL) {
not = (*next == '!');
if (not)
next++;
if (c->flags & CLIENT_CONTROL)
flag = server_client_control_flags(c, next);
else
flag = 0;
if (strcmp(next, "read-only") == 0)
flag = CLIENT_READONLY;
else if (strcmp(next, "ignore-size") == 0)
flag = CLIENT_IGNORESIZE;
else if (strcmp(next, "no-detach-on-destroy") == 0)
flag = CLIENT_NO_DETACH_ON_DESTROY;
if (flag == 0)
continue;
log_debug("client %s set flag %s", c->name, next);
if (not) {
if (c->flags & CLIENT_READONLY)
flag &= ~CLIENT_READONLY;
c->flags &= ~flag;
} else
c->flags |= flag;
if (flag == CLIENT_CONTROL_NOOUTPUT)
control_reset_offsets(c);
}
free(copy);
proc_send(c->peer, MSG_FLAGS, -1, &c->flags, sizeof c->flags);
}
/* Get client flags. This is only flags useful to show to users. */
const char *
server_client_get_flags(struct client *c)
{
static char s[256];
char tmp[32];
*s = '\0';
if (c->flags & CLIENT_ATTACHED)
strlcat(s, "attached,", sizeof s);
if (c->flags & CLIENT_FOCUSED)
strlcat(s, "focused,", sizeof s);
if (c->flags & CLIENT_CONTROL)
strlcat(s, "control-mode,", sizeof s);
if (c->flags & CLIENT_IGNORESIZE)
strlcat(s, "ignore-size,", sizeof s);
if (c->flags & CLIENT_NO_DETACH_ON_DESTROY)
strlcat(s, "no-detach-on-destroy,", sizeof s);
if (c->flags & CLIENT_CONTROL_NOOUTPUT)
strlcat(s, "no-output,", sizeof s);
if (c->flags & CLIENT_CONTROL_WAITEXIT)
strlcat(s, "wait-exit,", sizeof s);
if (c->flags & CLIENT_CONTROL_NEWLAYOUTS)
strlcat(s, "new-layouts,", sizeof s);
if (c->flags & CLIENT_CONTROL_PAUSEAFTER) {
xsnprintf(tmp, sizeof tmp, "pause-after=%u,",
c->pause_age / 1000);
strlcat(s, tmp, sizeof s);
}
if (c->flags & CLIENT_READONLY)
strlcat(s, "read-only,", sizeof s);
if (c->flags & CLIENT_SUSPENDED)
strlcat(s, "suspended,", sizeof s);
if (c->flags & CLIENT_UTF8)
strlcat(s, "UTF-8,", sizeof s);
if (*s != '\0')
s[strlen(s) - 1] = '\0';
return (s);
}
/* Remove pane from client state. */
void
server_client_remove_pane(struct window_pane *wp)
{
struct client *c;
TAILQ_FOREACH(c, &clients, entry) {
if (c->tty.mouse_last_pane == (int)wp->id) {
c->tty.mouse_last_pane = -1;
c->tty.mouse_drag_update = NULL;
c->tty.mouse_scrolling_flag = 0;
}
}
}
/* Print to a client. */
void
server_client_print(struct client *c, int parse, struct evbuffer *evb)
{
void *data = EVBUFFER_DATA(evb);
size_t size = EVBUFFER_LENGTH(evb);
struct window_pane *wp;
struct window_mode_entry *wme;
char *sanitized, *msg, *line, empty = '\0';
if (!parse) {
utf8_stravisx(&msg, data, size,
VIS_OCTAL|VIS_CSTYLE|VIS_NOSLASH);
} else {
if (size == 0)
msg = &empty;
else {
msg = EVBUFFER_DATA(evb);
if (msg[size - 1] != '\0')
evbuffer_add(evb, "", 1);
}
}
log_debug("%s: %s", __func__, msg);
if (c == NULL)
goto out;
if (c->session == NULL || (c->flags & CLIENT_CONTROL)) {
if (~c->flags & CLIENT_UTF8) {
sanitized = utf8_sanitize(msg);
if (c->flags & CLIENT_CONTROL)
control_write(c, "%s", sanitized);
else
file_print(c, "%s\n", sanitized);
free(sanitized);
} else {
if (c->flags & CLIENT_CONTROL)
control_write(c, "%s", msg);
else
file_print(c, "%s\n", msg);
}
goto out;
}
wp = c->session->curw->window->active;
wme = TAILQ_FIRST(&wp->modes);
if (wme == NULL || wme->mode != &window_view_mode)
window_pane_set_mode(wp, NULL, &window_view_mode, NULL, NULL,
NULL);
if (parse) {
do {
line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF);
if (line != NULL) {
window_copy_add(wp, 1, "%s", line);
free(line);
}
} while (line != NULL);
size = EVBUFFER_LENGTH(evb);
if (size != 0) {
line = EVBUFFER_DATA(evb);
window_copy_add(wp, 1, "%.*s", (int)size, line);
}
} else
window_copy_add(wp, 0, "%s", msg);
out:
if (!parse)
free(msg);
}
static void
server_client_report_theme(struct client *c, enum client_theme theme)
{
enum client_theme old = c->theme;
if (theme == THEME_LIGHT) {
c->theme = THEME_LIGHT;
events_fire_client("client-light-theme", c);
} else {
c->theme = THEME_DARK;
events_fire_client("client-dark-theme", c);
}
/*
* If the theme has changed, update the theme colours and redraw the
* client.
*/
if (c->theme != old) {
server_client_update_theme_colours(c);
if (c->tty.flags & TTY_OPENED)
tty_invalidate(&c->tty);
server_redraw_client(c);
}
/*
* Request foreground and background colour again. Don't forward 2031 to
* panes until a response is received.
*/
tty_repeat_requests(&c->tty, 1);
}