Merge master into floating_panes.

This commit is contained in:
Dane Jensen
2026-06-09 20:33:15 +00:00
committed by GitHub
15 changed files with 567 additions and 227 deletions

View File

@@ -27,13 +27,17 @@
*/ */
static enum cmd_retval cmd_kill_pane_exec(struct cmd *, struct cmdq_item *); static enum cmd_retval cmd_kill_pane_exec(struct cmd *, struct cmdq_item *);
static enum cmd_retval cmd_kill_pane_all(struct cmdq_item *, const char *);
static int cmd_kill_pane_filter(struct cmdq_item *,
struct session *, struct winlink *,
struct window_pane *, const char *);
const struct cmd_entry cmd_kill_pane_entry = { const struct cmd_entry cmd_kill_pane_entry = {
.name = "kill-pane", .name = "kill-pane",
.alias = "killp", .alias = "killp",
.args = { "at:", 0, 0, NULL }, .args = { "af:t:", 0, 0, NULL },
.usage = "[-a] " CMD_TARGET_PANE_USAGE, .usage = "[-a] [-f filter] " CMD_TARGET_PANE_USAGE,
.target = { 't', CMD_FIND_PANE, 0 }, .target = { 't', CMD_FIND_PANE, 0 },
@@ -46,22 +50,19 @@ cmd_kill_pane_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s;
struct winlink *wl = target->wl; struct winlink *wl = target->wl;
struct window_pane *loopwp, *tmpwp, *wp = target->wp; struct window_pane *wp = target->wp;
const char *filter = args_get(args, 'f');
if (args_has(args, 'a')) { if (filter != NULL && !args_has(args, 'a')) {
server_unzoom_window(wl->window); cmdq_error(item, "-f only valid with -a");
TAILQ_FOREACH_SAFE(loopwp, &wl->window->panes, entry, tmpwp) { return (CMD_RETURN_ERROR);
if (loopwp == wp)
continue;
server_client_remove_pane(loopwp);
layout_close_pane(loopwp);
window_remove_pane(wl->window, loopwp);
}
server_redraw_window(wl->window);
return (CMD_RETURN_NORMAL);
} }
if (args_has(args, 'a'))
return (cmd_kill_pane_all(item, filter));
if (wp == NULL) { if (wp == NULL) {
cmdq_error(item, "no active pane to kill"); cmdq_error(item, "no active pane to kill");
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
@@ -69,3 +70,48 @@ cmd_kill_pane_exec(struct cmd *self, struct cmdq_item *item)
server_kill_pane(wp); server_kill_pane(wp);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
static enum cmd_retval
cmd_kill_pane_all(struct cmdq_item *item, const char *filter)
{
struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s;
struct winlink *wl = target->wl;
struct window_pane *wp = target->wp;
struct window_pane *loopwp, *tmpwp;
server_unzoom_window(wl->window);
TAILQ_FOREACH_SAFE(loopwp, &wl->window->panes, entry, tmpwp) {
if (loopwp == wp)
continue;
if (!cmd_kill_pane_filter(item, s, wl, loopwp, filter))
continue;
server_client_remove_pane(loopwp);
layout_close_pane(loopwp);
window_remove_pane(wl->window, loopwp);
}
server_redraw_window(wl->window);
return (CMD_RETURN_NORMAL);
}
static int
cmd_kill_pane_filter(struct cmdq_item *item, struct session *s,
struct winlink *wl, struct window_pane *wp, const char *filter)
{
struct format_tree *ft;
char *expanded;
int flag;
if (filter == NULL)
return (1);
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_defaults(ft, NULL, s, wl, wp);
expanded = format_expand(ft, filter);
flag = format_true(expanded);
free(expanded);
format_free(ft);
return (flag);
}

View File

@@ -18,6 +18,8 @@
#include <sys/types.h> #include <sys/types.h>
#include <stdlib.h>
#include "tmux.h" #include "tmux.h"
/* /*
@@ -28,13 +30,16 @@
*/ */
static enum cmd_retval cmd_kill_session_exec(struct cmd *, struct cmdq_item *); static enum cmd_retval cmd_kill_session_exec(struct cmd *, struct cmdq_item *);
static enum cmd_retval cmd_kill_session_all(struct cmdq_item *, const char *);
static int cmd_kill_session_filter(struct cmdq_item *,
struct session *, const char *);
const struct cmd_entry cmd_kill_session_entry = { const struct cmd_entry cmd_kill_session_entry = {
.name = "kill-session", .name = "kill-session",
.alias = NULL, .alias = NULL,
.args = { "aCgt:", 0, 0, NULL }, .args = { "aCgf:t:", 0, 0, NULL },
.usage = "[-aCg] " CMD_TARGET_SESSION_USAGE, .usage = "[-aCg] [-f filter] " CMD_TARGET_SESSION_USAGE,
.target = { 't', CMD_FIND_SESSION, 0 }, .target = { 't', CMD_FIND_SESSION, 0 },
@@ -50,6 +55,12 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item)
struct session *s = target->s, *sloop, *stmp; struct session *s = target->s, *sloop, *stmp;
struct session_group *sg; struct session_group *sg;
struct winlink *wl; struct winlink *wl;
const char *filter = args_get(args, 'f');
if (filter != NULL && (!args_has(args, 'a') || args_has(args, 'C'))) {
cmdq_error(item, "-f only valid with -a");
return (CMD_RETURN_ERROR);
}
if (args_has(args, 'C')) { if (args_has(args, 'C')) {
RB_FOREACH(wl, winlinks, &s->windows) { RB_FOREACH(wl, winlinks, &s->windows) {
@@ -57,14 +68,9 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item)
wl->flags &= ~WINLINK_ALERTFLAGS; wl->flags &= ~WINLINK_ALERTFLAGS;
} }
server_redraw_session(s); server_redraw_session(s);
} else if (args_has(args, 'a')) { } else if (args_has(args, 'a'))
RB_FOREACH_SAFE(sloop, sessions, &sessions, stmp) { return (cmd_kill_session_all(item, filter));
if (sloop != s) { else if (args_has(args, 'g') &&
server_destroy_session(sloop);
session_destroy(sloop, 1, __func__);
}
}
} else if (args_has(args, 'g') &&
(sg = session_group_contains(s)) != NULL) { (sg = session_group_contains(s)) != NULL) {
TAILQ_FOREACH_SAFE(sloop, &sg->sessions, gentry, stmp) { TAILQ_FOREACH_SAFE(sloop, &sg->sessions, gentry, stmp) {
server_destroy_session(sloop); server_destroy_session(sloop);
@@ -76,3 +82,42 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item)
} }
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
static enum cmd_retval
cmd_kill_session_all(struct cmdq_item *item, const char *filter)
{
struct session *s = cmdq_get_target(item)->s;
struct session *sloop, *stmp;
RB_FOREACH_SAFE(sloop, sessions, &sessions, stmp) {
if (sloop == s)
continue;
if (!cmd_kill_session_filter(item, sloop, filter))
continue;
server_destroy_session(sloop);
session_destroy(sloop, 1, __func__);
}
return (CMD_RETURN_NORMAL);
}
static int
cmd_kill_session_filter(struct cmdq_item *item, struct session *s,
const char *filter)
{
struct format_tree *ft;
char *expanded;
int flag;
if (filter == NULL)
return (1);
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_defaults(ft, NULL, s, NULL, NULL);
expanded = format_expand(ft, filter);
flag = format_true(expanded);
free(expanded);
format_free(ft);
return (flag);
}

View File

@@ -18,6 +18,8 @@
#include <sys/types.h> #include <sys/types.h>
#include <stdlib.h>
#include "tmux.h" #include "tmux.h"
/* /*
@@ -25,13 +27,16 @@
*/ */
static enum cmd_retval cmd_kill_window_exec(struct cmd *, struct cmdq_item *); static enum cmd_retval cmd_kill_window_exec(struct cmd *, struct cmdq_item *);
static enum cmd_retval cmd_kill_window_all(struct cmdq_item *, const char *);
static int cmd_kill_window_filter(struct cmdq_item *,
struct session *, struct winlink *, const char *);
const struct cmd_entry cmd_kill_window_entry = { const struct cmd_entry cmd_kill_window_entry = {
.name = "kill-window", .name = "kill-window",
.alias = "killw", .alias = "killw",
.args = { "at:", 0, 0, NULL }, .args = { "af:t:", 0, 0, NULL },
.usage = "[-a] " CMD_TARGET_WINDOW_USAGE, .usage = "[-a] [-f filter] " CMD_TARGET_WINDOW_USAGE,
.target = { 't', CMD_FIND_WINDOW, 0 }, .target = { 't', CMD_FIND_WINDOW, 0 },
@@ -57,10 +62,15 @@ cmd_kill_window_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct winlink *wl = target->wl, *loop; struct winlink *wl = target->wl;
struct window *w = wl->window; struct window *w = wl->window;
struct session *s = target->s; struct session *s = target->s;
u_int found; const char *filter = args_get(args, 'f');
if (filter != NULL && !args_has(args, 'a')) {
cmdq_error(item, "-f only valid with -a");
return (CMD_RETURN_ERROR);
}
if (cmd_get_entry(self) == &cmd_unlink_window_entry) { if (cmd_get_entry(self) == &cmd_unlink_window_entry) {
if (!args_has(args, 'k') && !session_is_linked(s, w)) { if (!args_has(args, 'k') && !session_is_linked(s, w)) {
@@ -72,39 +82,76 @@ cmd_kill_window_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (args_has(args, 'a')) { if (args_has(args, 'a'))
if (RB_PREV(winlinks, &s->windows, wl) == NULL && return (cmd_kill_window_all(item, filter));
RB_NEXT(winlinks, &s->windows, wl) == NULL)
return (CMD_RETURN_NORMAL);
/* Kill all windows except the current one. */
do {
found = 0;
RB_FOREACH(loop, winlinks, &s->windows) {
if (loop->window != wl->window) {
server_kill_window(loop->window, 0);
found++;
break;
}
}
} while (found != 0);
/*
* If the current window appears in the session more than once,
* kill it as well.
*/
found = 0;
RB_FOREACH(loop, winlinks, &s->windows) {
if (loop->window == wl->window)
found++;
}
if (found > 1)
server_kill_window(wl->window, 0);
server_renumber_all();
return (CMD_RETURN_NORMAL);
}
server_kill_window(wl->window, 1); server_kill_window(wl->window, 1);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
static enum cmd_retval
cmd_kill_window_all(struct cmdq_item *item, const char *filter)
{
struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s;
struct winlink *wl = target->wl;
struct winlink *loop;
u_int found, kill_current;
if (RB_PREV(winlinks, &s->windows, wl) == NULL &&
RB_NEXT(winlinks, &s->windows, wl) == NULL)
return (CMD_RETURN_NORMAL);
/* Kill all windows except the current one. */
do {
found = 0;
RB_FOREACH(loop, winlinks, &s->windows) {
if (loop->window != wl->window &&
cmd_kill_window_filter(item, s, loop, filter)) {
server_kill_window(loop->window, 0);
found++;
break;
}
}
} while (found != 0);
/*
* If the current window appears in the session more than once, kill it
* as well if it matches the filter.
*/
found = kill_current = 0;
RB_FOREACH(loop, winlinks, &s->windows) {
if (loop->window == wl->window) {
found++;
if (cmd_kill_window_filter(item, s, loop, filter))
kill_current = 1;
}
}
if (kill_current && found > 1)
server_kill_window(wl->window, 0);
server_renumber_all();
return (CMD_RETURN_NORMAL);
}
static int
cmd_kill_window_filter(struct cmdq_item *item, struct session *s,
struct winlink *wl, const char *filter)
{
struct format_tree *ft;
char *expanded;
int flag;
if (filter == NULL)
return (1);
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_defaults(ft, NULL, s, wl, NULL);
expanded = format_expand(ft, filter);
flag = format_true(expanded);
free(expanded);
format_free(ft);
return (flag);
}

View File

@@ -19,6 +19,7 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <grp.h>
#include <pwd.h> #include <pwd.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -37,67 +38,71 @@ const struct cmd_entry cmd_server_access_entry = {
.name = "server-access", .name = "server-access",
.alias = NULL, .alias = NULL,
.args = { "adlrw", 0, 1, NULL }, .args = { "adglrw", 0, 1, NULL },
.usage = "[-adlrw] " CMD_TARGET_PANE_USAGE " [user]", .usage = "[-adglrw] " CMD_TARGET_PANE_USAGE " [user|group]",
.flags = CMD_CLIENT_CANFAIL, .flags = CMD_CLIENT_CANFAIL,
.exec = cmd_server_access_exec .exec = cmd_server_access_exec
}; };
static enum cmd_retval static enum cmd_retval
cmd_server_access_deny(struct cmdq_item *item, struct passwd *pw) cmd_server_access_deny(struct cmdq_item *item, id_t id, int flags,
const char *type, const char *name)
{ {
struct client *loop; if (!server_acl_find(id, flags)) {
struct server_acl_user *user; cmdq_error(item, "%s %s not found", type, name);
uid_t uid;
if ((user = server_acl_user_find(pw->pw_uid)) == NULL) {
cmdq_error(item, "user %s not found", pw->pw_name);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
TAILQ_FOREACH(loop, &clients, entry) { server_acl_deny(id, flags);
uid = proc_get_peer_uid(loop->peer);
if (uid == server_acl_get_uid(user)) {
loop->exit_message = xstrdup("access not allowed");
loop->flags |= CLIENT_EXIT;
}
}
server_acl_user_deny(pw->pw_uid);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
static enum cmd_retval static enum cmd_retval
cmd_server_access_exec(struct cmd *self, struct cmdq_item *item) cmd_server_access_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_target_client(item); struct client *c = cmdq_get_target_client(item);
char *name; char *arg;
struct passwd *pw = NULL; const char *name = NULL, *type;
struct passwd *pw;
struct group *gr;
id_t id;
int flags = 0;
if (args_has(args, 'l')) { if (args_has(args, 'l')) {
server_acl_display(item); server_acl_display(item);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (args_count(args) == 0) { if (args_count(args) == 0) {
cmdq_error(item, "missing user argument"); cmdq_error(item, "missing user or group argument");
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
name = format_single(item, args_string(args, 0), c, NULL, NULL, NULL); arg = format_single(item, args_string(args, 0), c, NULL, NULL, NULL);
if (*name != '\0') if (args_has(args, 'g')) {
pw = getpwnam(name); type = "group";
if (pw == NULL) { if ((gr = getgrnam(arg)) != NULL) {
cmdq_error(item, "unknown user: %s", name); id = gr->gr_gid;
free(name); name = gr->gr_name;
flags |= SERVER_ACL_IS_GROUP;
}
} else {
type = "user";
if ((pw = getpwnam(arg)) != NULL) {
id = pw->pw_uid;
name = pw->pw_name;
}
}
if (name == NULL) {
cmdq_error(item, "unknown %s: %s", type, arg);
free(arg);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
free(name); free(arg);
if (pw->pw_uid == 0 || pw->pw_uid == getuid()) { if ((~flags & SERVER_ACL_IS_GROUP) && (id == 0 || id == getuid())) {
cmdq_error(item, "%s owns the server, can't change access", cmdq_error(item, "%s owns the server, can't change access",
pw->pw_name); name);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
@@ -111,36 +116,35 @@ cmd_server_access_exec(struct cmd *self, struct cmdq_item *item)
} }
if (args_has(args, 'd')) if (args_has(args, 'd'))
return (cmd_server_access_deny(item, pw)); return (cmd_server_access_deny(item, id, flags, type, name));
if (args_has(args, 'a')) { if (args_has(args, 'a')) {
if (server_acl_user_find(pw->pw_uid) != NULL) { if (server_acl_find(id, flags)) {
cmdq_error(item, "user %s is already added", cmdq_error(item, "%s %s is already added", type, name);
pw->pw_name);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
server_acl_user_allow(pw->pw_uid); server_acl_allow(id, flags);
/* Do not return - allow -r or -w with -a. */ /* Do not return - allow -r or -w with -a. */
} else if (args_has(args, 'r') || args_has(args, 'w')) { } else if (args_has(args, 'r') || args_has(args, 'w')) {
/* -r or -w implies -a if user does not exist. */ /* -r or -w implies -a if the entry does not exist. */
if (server_acl_user_find(pw->pw_uid) == NULL) if (!server_acl_find(id, flags))
server_acl_user_allow(pw->pw_uid); server_acl_allow(id, flags);
} }
if (args_has(args, 'w')) { if (args_has(args, 'w')) {
if (server_acl_user_find(pw->pw_uid) == NULL) { if (!server_acl_find(id, flags)) {
cmdq_error(item, "user %s not found", pw->pw_name); cmdq_error(item, "%s %s not found", type, name);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
server_acl_user_allow_write(pw->pw_uid); server_acl_allow_write(id, flags);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (args_has(args, 'r')) { if (args_has(args, 'r')) {
if (server_acl_user_find(pw->pw_uid) == NULL) { if (!server_acl_find(id, flags)) {
cmdq_error(item, "user %s not found", pw->pw_name); cmdq_error(item, "%s %s not found", type, name);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
server_acl_user_deny_write(pw->pw_uid); server_acl_deny_write(id, flags);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }

View File

@@ -38,7 +38,7 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane", .name = "new-pane",
.alias = "newp", .alias = "newp",
.args = { "bc:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vx:X:y:Y:Z", 0, -1, NULL }, .args = { "bBc:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] " .usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] " "[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] " "[-s style] [-S active-border-style] "
@@ -56,7 +56,7 @@ const struct cmd_entry cmd_split_window_entry = {
.name = "split-window", .name = "split-window",
.alias = "splitw", .alias = "splitw",
.args = { "bc:de:EfF:hIkl:m:p:PR:s:S:t:T:vZ", 0, -1, NULL }, .args = { "bBc:de:EfF:hIkl:m:p:PR:s:S:t:T:vZ", 0, -1, NULL },
.usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] " .usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] " "[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] " "[-s style] [-S active-border-style] "
@@ -244,5 +244,15 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
environ_free(sc.environ); environ_free(sc.environ);
if (input) if (input)
return (CMD_RETURN_WAIT); return (CMD_RETURN_WAIT);
if (args_has(args, 'B')) {
/*
* With -B, block this command queue item until the pane's
* command exits; window_pane_block_finish will be called to
* continue it.
*/
new_wp->block_item = item;
return (CMD_RETURN_WAIT);
}
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }

12
proc.c
View File

@@ -57,6 +57,7 @@ struct tmuxpeer {
struct imsgbuf ibuf; struct imsgbuf ibuf;
struct event event; struct event event;
uid_t uid; uid_t uid;
gid_t gid;
int flags; int flags;
#define PEER_BAD 0x1 #define PEER_BAD 0x1
@@ -301,7 +302,6 @@ proc_add_peer(struct tmuxproc *tp, int fd,
void (*dispatchcb)(struct imsg *, void *), void *arg) void (*dispatchcb)(struct imsg *, void *), void *arg)
{ {
struct tmuxpeer *peer; struct tmuxpeer *peer;
gid_t gid;
peer = xcalloc(1, sizeof *peer); peer = xcalloc(1, sizeof *peer);
peer->parent = tp; peer->parent = tp;
@@ -314,8 +314,10 @@ proc_add_peer(struct tmuxproc *tp, int fd,
imsgbuf_allow_fdpass(&peer->ibuf); imsgbuf_allow_fdpass(&peer->ibuf);
event_set(&peer->event, fd, EV_READ, proc_event_cb, peer); event_set(&peer->event, fd, EV_READ, proc_event_cb, peer);
if (getpeereid(fd, &peer->uid, &gid) != 0) if (getpeereid(fd, &peer->uid, &peer->gid) != 0) {
peer->uid = (uid_t)-1; peer->uid = (uid_t)-1;
peer->gid = (gid_t)-1;
}
log_debug("add peer %p: %d (%p)", peer, fd, arg); log_debug("add peer %p: %d (%p)", peer, fd, arg);
TAILQ_INSERT_TAIL(&tp->peers, peer, entry); TAILQ_INSERT_TAIL(&tp->peers, peer, entry);
@@ -384,3 +386,9 @@ proc_get_peer_uid(struct tmuxpeer *peer)
{ {
return (peer->uid); return (peer->uid);
} }
gid_t
proc_get_peer_gid(struct tmuxpeer *peer)
{
return (peer->gid);
}

View File

@@ -22,6 +22,7 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <ctype.h> #include <ctype.h>
#include <grp.h>
#include <pwd.h> #include <pwd.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -29,158 +30,203 @@
#include "tmux.h" #include "tmux.h"
struct server_acl_user { struct server_acl_entry {
uid_t uid; id_t id;
int flags; int flags;
#define SERVER_ACL_READONLY 0x1
RB_ENTRY(server_acl_user) entry; RB_ENTRY(server_acl_entry) entry;
}; };
static int static int
server_acl_cmp(struct server_acl_user *user1, struct server_acl_user *user2) server_acl_cmp(struct server_acl_entry *entry1,
struct server_acl_entry *entry2)
{ {
if (user1->uid < user2->uid) if ((entry1->flags ^ entry2->flags) & SERVER_ACL_IS_GROUP) {
if (entry1->flags & SERVER_ACL_IS_GROUP)
return (1);
return (-1); return (-1);
return (user1->uid > user2->uid); }
if (entry1->id < entry2->id)
return (-1);
return (entry1->id > entry2->id);
} }
RB_HEAD(server_acl_entries, server_acl_user) server_acl_entries; RB_HEAD(server_acl_entries, server_acl_entry) server_acl_entries;
RB_GENERATE_STATIC(server_acl_entries, server_acl_user, entry, server_acl_cmp); RB_GENERATE_STATIC(server_acl_entries, server_acl_entry, entry, server_acl_cmp);
/* Initialize server_acl tree. */ static struct server_acl_entry *
server_acl_entry_find(id_t id, int flags)
{
struct server_acl_entry find = {
.id = id,
.flags = flags & SERVER_ACL_IS_GROUP
};
return (RB_FIND(server_acl_entries, &server_acl_entries, &find));
}
static struct server_acl_entry *
server_acl_check(struct client *c)
{
struct server_acl_entry *entry;
uid_t uid;
gid_t gid;
uid = proc_get_peer_uid(c->peer);
if (uid == (uid_t)-1)
return (NULL);
entry = server_acl_entry_find(uid, 0);
if (entry != NULL)
return (entry);
gid = proc_get_peer_gid(c->peer);
if (gid == (gid_t)-1)
return (NULL);
return (server_acl_entry_find(gid, SERVER_ACL_IS_GROUP));
}
static void
server_acl_update(void)
{
struct server_acl_entry *entry;
struct client *c;
TAILQ_FOREACH(c, &clients, entry) {
entry = server_acl_check(c);
if (entry == NULL) {
c->exit_message = xstrdup("access not allowed");
c->flags |= CLIENT_EXIT;
} else if (entry->flags & SERVER_ACL_READONLY)
c->flags |= CLIENT_READONLY;
else
c->flags &= ~CLIENT_READONLY;
}
}
/* Initialize ACL tree. */
void void
server_acl_init(void) server_acl_init(void)
{ {
RB_INIT(&server_acl_entries); RB_INIT(&server_acl_entries);
if (getuid() != 0) if (getuid() != 0)
server_acl_user_allow(0); server_acl_allow(0, 0);
server_acl_user_allow(getuid()); server_acl_allow(getuid(), 0);
} }
/* Find user entry. */ /* Check if an ACL entry exists. */
struct server_acl_user* int
server_acl_user_find(uid_t uid) server_acl_find(id_t id, int flags)
{ {
struct server_acl_user find = { .uid = uid }; return (server_acl_entry_find(id, flags) != NULL);
return (RB_FIND(server_acl_entries, &server_acl_entries, &find));
} }
/* Display the tree. */ /* Display the tree. */
void void
server_acl_display(struct cmdq_item *item) server_acl_display(struct cmdq_item *item)
{ {
struct server_acl_user *loop; struct server_acl_entry *loop;
struct passwd *pw; struct passwd *pw;
struct group *gr;
const char *name; const char *name;
char type;
RB_FOREACH(loop, server_acl_entries, &server_acl_entries) { RB_FOREACH(loop, server_acl_entries, &server_acl_entries) {
if (loop->uid == 0) if (~loop->flags & SERVER_ACL_IS_GROUP) {
continue; if (loop->id == 0)
if ((pw = getpwuid(loop->uid)) != NULL) continue;
name = pw->pw_name; if ((pw = getpwuid(loop->id)) != NULL)
name = pw->pw_name;
else
name = "unknown";
type = 'U';
} else {
if ((gr = getgrgid(loop->id)) != NULL)
name = gr->gr_name;
else
name = "unknown";
type = 'G';
}
if (loop->flags & SERVER_ACL_READONLY)
cmdq_print(item, "%s (%c,R)", name, type);
else else
name = "unknown"; cmdq_print(item, "%s (%c,W)", name, type);
if (loop->flags == SERVER_ACL_READONLY)
cmdq_print(item, "%s (R)", name);
else
cmdq_print(item, "%s (W)", name);
} }
} }
/* Allow a user. */ /* Allow an ACL entry. */
void void
server_acl_user_allow(uid_t uid) server_acl_allow(id_t id, int flags)
{ {
struct server_acl_user *user; struct server_acl_entry *entry;
user = server_acl_user_find(uid); entry = server_acl_entry_find(id, flags);
if (user == NULL) { if (entry == NULL) {
user = xcalloc(1, sizeof *user); entry = xcalloc(1, sizeof *entry);
user->uid = uid; entry->id = id;
RB_INSERT(server_acl_entries, &server_acl_entries, user); entry->flags = flags & SERVER_ACL_IS_GROUP;
RB_INSERT(server_acl_entries, &server_acl_entries, entry);
} }
} }
/* Deny a user (remove from the tree). */ /* Deny an ACL entry (remove it from the tree). */
void void
server_acl_user_deny(uid_t uid) server_acl_deny(id_t id, int flags)
{ {
struct server_acl_user *user; struct server_acl_entry *entry;
user = server_acl_user_find(uid); entry = server_acl_entry_find(id, flags);
if (user != NULL) { if (entry != NULL) {
RB_REMOVE(server_acl_entries, &server_acl_entries, user); RB_REMOVE(server_acl_entries, &server_acl_entries, entry);
free(user); free(entry);
server_acl_update();
} }
} }
/* Allow this user write access. */ /* Allow this ACL entry write access. */
void void
server_acl_user_allow_write(uid_t uid) server_acl_allow_write(id_t id, int flags)
{ {
struct server_acl_user *user; struct server_acl_entry *entry;
struct client *c;
user = server_acl_user_find(uid); entry = server_acl_entry_find(id, flags);
if (user == NULL) if (entry == NULL)
return; return;
user->flags &= ~SERVER_ACL_READONLY; entry->flags &= ~SERVER_ACL_READONLY;
server_acl_update();
TAILQ_FOREACH(c, &clients, entry) {
uid = proc_get_peer_uid(c->peer);
if (uid != (uid_t)-1 && uid == user->uid)
c->flags &= ~CLIENT_READONLY;
}
} }
/* Deny this user write access. */ /* Deny this ACL entry write access. */
void void
server_acl_user_deny_write(uid_t uid) server_acl_deny_write(id_t id, int flags)
{ {
struct server_acl_user *user; struct server_acl_entry *entry;
struct client *c;
user = server_acl_user_find(uid); entry = server_acl_entry_find(id, flags);
if (user == NULL) if (entry == NULL)
return; return;
user->flags |= SERVER_ACL_READONLY; entry->flags |= SERVER_ACL_READONLY;
server_acl_update();
TAILQ_FOREACH(c, &clients, entry) {
uid = proc_get_peer_uid(c->peer);
if (uid != (uid_t)-1 && uid == user->uid)
c->flags |= CLIENT_READONLY;
}
} }
/* /*
* Check if the client's UID exists in the ACL list and if so, set as read only * Check if the client's UID or GID exists in the ACL list and if so, set as
* if needed. Return false if the user does not exist. * read only if needed. UID entries take precedence over GID entries. Return
* false if no entry exists.
*/ */
int int
server_acl_join(struct client *c) server_acl_join(struct client *c)
{ {
struct server_acl_user *user; struct server_acl_entry *entry;
uid_t uid;
uid = proc_get_peer_uid(c->peer); entry = server_acl_check(c);
if (uid == (uid_t)-1) if (entry == NULL)
return (0); return (0);
if (entry->flags & SERVER_ACL_READONLY)
user = server_acl_user_find(uid);
if (user == NULL)
return (0);
if (user->flags & SERVER_ACL_READONLY)
c->flags |= CLIENT_READONLY; c->flags |= CLIENT_READONLY;
return (1); return (1);
} }
/* Get UID for user entry. */
uid_t
server_acl_get_uid(struct server_acl_user *user)
{
return (user->uid);
}

View File

@@ -695,12 +695,8 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py,
bdr_bottom = fwp->yoff + fwp->sy; bdr_bottom = fwp->yoff + fwp->sy;
if (py == bdr_bottom) if (py == bdr_bottom)
break; break;
if (window_pane_is_floating(wp)) { if (py == bdr_top)
/* Floating pane, check top border. */ break;
bdr_top = fwp->yoff - 1;
if (py == bdr_top)
break;
}
} }
} }
if (fwp != NULL) if (fwp != NULL)

View File

@@ -497,6 +497,8 @@ server_child_exited(pid_t pid, int status)
log_debug("%%%u exited", wp->id); log_debug("%%%u exited", wp->id);
wp->flags |= PANE_EXITED; wp->flags |= PANE_EXITED;
window_pane_block_finish(wp);
if (window_pane_destroy_ready(wp)) if (window_pane_destroy_ready(wp))
server_destroy_pane(wp, 1); server_destroy_pane(wp, 1);
break; break;

76
tmux.1
View File

@@ -1207,6 +1207,7 @@ Kill the
server and clients and destroy all sessions. server and clients and destroy all sessions.
.It Xo Ic kill\-session .It Xo Ic kill\-session
.Op Fl aCg .Op Fl aCg
.Op Fl f Ar filter
.Op Fl t Ar target\-session .Op Fl t Ar target\-session
.Xc .Xc
Destroy the given session, closing any windows linked to it and no other Destroy the given session, closing any windows linked to it and no other
@@ -1214,6 +1215,12 @@ sessions, and detaching all clients attached to it.
If If
.Fl a .Fl a
is given, all sessions but the specified one is killed. is given, all sessions but the specified one is killed.
When
.Fl a
is given,
.Fl f
specifies a filter.
Only sessions for which the filter is true are killed.
If If
.Fl g .Fl g
is given and the session is in a session group, all sessions in the group are is given and the session is in a session group, all sessions in the group are
@@ -1578,35 +1585,57 @@ option.
Rename the session to Rename the session to
.Ar new\-name . .Ar new\-name .
.It Xo Ic server\-access .It Xo Ic server\-access
.Op Fl adlrw .Op Fl adglrw
.Op Ar user .Op Ar user | group
.Xc .Xc
Change the access or read/write permission of Change the access or read/write permission of
.Ar user . .Ar user
or
.Ar group .
The user running the The user running the
.Nm .Nm
server (its owner) and the root user cannot be changed and are always server (its owner) and the root user cannot be changed and are always
permitted access. permitted access.
.Fl g
changes a group rather than a user.
.Pp .Pp
.Fl a .Fl a
and and
.Fl d .Fl d
are used to give or revoke access for the specified user. are used to give or revoke access for the specified user or group.
If the user is already attached, the If a client is already attached, the
.Fl d .Fl d
flag causes their clients to be detached. flag causes it to be detached unless it is still permitted by another entry.
.Pp .Pp
.Fl r .Fl r
and and
.Fl w .Fl w
change the permissions for change the permissions for
.Ar user : .Ar user
or
.Ar group :
.Fl r .Fl r
makes their clients read-only and makes matching clients read-only and
.Fl w .Fl w
writable. writable.
.Fl l .Fl l
lists current access permissions. lists current access permissions.
User entries are shown with
.Ql U ,
group entries with
.Ql G ,
and read-only or writable entries with
.Ql R
or
.Ql W ,
for example
.Ql user1 (U,W)
or
.Ql testgroup (G,R) .
If both a user and group entry match a client, the user entry takes
precedence.
Only the effective group ID of the client is used, not its supplementary
groups.
.Pp .Pp
By default, the access list is empty and By default, the access list is empty and
.Nm .Nm
@@ -3132,6 +3161,7 @@ the marked pane is used rather than the current pane.
.Tg killp .Tg killp
.It Xo Ic kill\-pane .It Xo Ic kill\-pane
.Op Fl a .Op Fl a
.Op Fl f Ar filter
.Op Fl t Ar target\-pane .Op Fl t Ar target\-pane
.Xc .Xc
.D1 Pq alias: Ic killp .D1 Pq alias: Ic killp
@@ -3139,11 +3169,18 @@ Destroy the given pane.
If no panes remain in the containing window, it is also destroyed. If no panes remain in the containing window, it is also destroyed.
The The
.Fl a .Fl a
option kills all but the pane given with option kills all panes in the window except the pane given with
.Fl t . .Fl t .
When
.Fl a
is given,
.Fl f
specifies a filter.
Only panes for which the filter is true are killed.
.Tg killw .Tg killw
.It Xo Ic kill\-window .It Xo Ic kill\-window
.Op Fl a .Op Fl a
.Op Fl f Ar filter
.Op Fl t Ar target\-window .Op Fl t Ar target\-window
.Xc .Xc
.D1 Pq alias: Ic killw .D1 Pq alias: Ic killw
@@ -3152,8 +3189,14 @@ Kill the current window or the window at
removing it from any sessions to which it is linked. removing it from any sessions to which it is linked.
The The
.Fl a .Fl a
option kills all but the window given with option kills all windows in the session except the window given with
.Fl t . .Fl t .
When
.Fl a
is given,
.Fl f
specifies a filter.
Only windows for which the filter is true are killed.
.Tg lastp .Tg lastp
.It Xo Ic last\-pane .It Xo Ic last\-pane
.Op Fl deZ .Op Fl deZ
@@ -3414,7 +3457,7 @@ but a different format may be specified with
.Fl F . .Fl F .
.Tg newp .Tg newp
.It Xo Ic new\-pane .It Xo Ic new\-pane
.Op Fl bdefhIkPvZ .Op Fl bBdefhIkPvZ
.Op Fl c Ar start\-directory .Op Fl c Ar start\-directory
.Op Fl e Ar environment .Op Fl e Ar environment
.Op Fl F Ar format .Op Fl F Ar format
@@ -3490,6 +3533,17 @@ but also sets the
option for this pane to option for this pane to
.Ar message . .Ar message .
.Pp .Pp
.Fl B
blocks until
.Ar shell\-command
exits, then returns its exit status.
For example:
.Bd -literal -offset indent
$ tmux new-pane -B 'vi afile'
$ echo $?
0
.Ed
.Pp
.Fl E , .Fl E ,
or an empty or an empty
.Ar shell\-command , .Ar shell\-command ,

16
tmux.h
View File

@@ -1305,6 +1305,7 @@ struct window_pane {
char tty[TTY_NAME_MAX]; char tty[TTY_NAME_MAX];
int status; int status;
struct timeval dead_time; struct timeval dead_time;
struct cmdq_item *block_item; /* new-pane -B: waiting for pane exit */
int fd; int fd;
struct bufferevent *event; struct bufferevent *event;
@@ -2436,6 +2437,7 @@ void proc_flush_peer(struct tmuxpeer *);
void proc_toggle_log(struct tmuxproc *); void proc_toggle_log(struct tmuxproc *);
pid_t proc_fork_and_daemon(int *); pid_t proc_fork_and_daemon(int *);
uid_t proc_get_peer_uid(struct tmuxpeer *); uid_t proc_get_peer_uid(struct tmuxpeer *);
gid_t proc_get_peer_gid(struct tmuxpeer *);
/* cfg.c */ /* cfg.c */
extern int cfg_finished; extern int cfg_finished;
@@ -3433,6 +3435,7 @@ struct window *window_find_by_id(u_int);
void window_update_activity(struct window *); void window_update_activity(struct window *);
struct window *window_create(u_int, u_int, u_int, u_int); struct window *window_create(u_int, u_int, u_int, u_int);
void window_pane_set_event(struct window_pane *); void window_pane_set_event(struct window_pane *);
void window_pane_block_finish(struct window_pane *);
struct window_pane *window_get_active_at(struct window *, u_int, u_int); struct window_pane *window_get_active_at(struct window *, u_int, u_int);
struct window_pane *window_find_string(struct window *, const char *); struct window_pane *window_find_string(struct window *, const char *);
int window_has_floating_panes(struct window *); int window_has_floating_panes(struct window *);
@@ -3875,15 +3878,16 @@ struct screen *sixel_to_screen(struct sixel_image *);
#endif #endif
/* server-acl.c */ /* server-acl.c */
#define SERVER_ACL_READONLY 0x1
#define SERVER_ACL_IS_GROUP 0x2
void server_acl_init(void); void server_acl_init(void);
struct server_acl_user *server_acl_user_find(uid_t); int server_acl_find(id_t, int);
void server_acl_display(struct cmdq_item *); void server_acl_display(struct cmdq_item *);
void server_acl_user_allow(uid_t); void server_acl_allow(id_t, int);
void server_acl_user_deny(uid_t); void server_acl_deny(id_t, int);
void server_acl_user_allow_write(uid_t); void server_acl_allow_write(id_t, int);
void server_acl_user_deny_write(uid_t); void server_acl_deny_write(id_t, int);
int server_acl_join(struct client *); int server_acl_join(struct client *);
uid_t server_acl_get_uid(struct server_acl_user *);
/* hyperlink.c */ /* hyperlink.c */
u_int hyperlinks_put(struct hyperlinks *, const char *, u_int hyperlinks_put(struct hyperlinks *, const char *,

2
tty.c
View File

@@ -2051,6 +2051,8 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx)
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
tty_margin_off(tty); tty_margin_off(tty);
if (ctx->flags & TTY_CTX_CELL_INVALIDATE)
tty_invalidate(tty);
tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy);
tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette, tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette,

View File

@@ -82,6 +82,23 @@ utf8_is_hangul_filler(const struct utf8_data *ud)
return (memcmp(ud->data, "\343\205\244", 3) == 0); return (memcmp(ud->data, "\343\205\244", 3) == 0);
} }
/* Count regional indicator characters. */
static u_int
utf8_regional_count(const struct utf8_data *ud)
{
u_int count = 0, i;
for (i = 0; i + 4 <= ud->size; i++) {
if (ud->data[i] == 0xf0 &&
ud->data[i + 1] == 0x9f &&
ud->data[i + 2] == 0x87 &&
ud->data[i + 3] >= 0xa6 &&
ud->data[i + 3] <= 0xbf)
count++;
}
return (count);
}
/* Should these two characters combine? */ /* Should these two characters combine? */
int int
utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add)
@@ -94,8 +111,13 @@ utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add)
return (0); return (0);
/* Regional indicators. */ /* Regional indicators. */
if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) {
if (utf8_regional_count(with) != 1)
return (0);
if (utf8_regional_count(add) != 1)
return (0);
return (1); return (1);
}
/* Emoji skin tone modifiers. */ /* Emoji skin tone modifiers. */
switch (a) { switch (a) {

View File

@@ -355,6 +355,7 @@ window_tree_build(void *modedata, struct sort_criteria *sort_crit,
uint64_t *tag, const char *filter) uint64_t *tag, const char *filter)
{ {
struct window_tree_modedata *data = modedata; struct window_tree_modedata *data = modedata;
int squash_groups = data->squash_groups;
struct session *s, **l; struct session *s, **l;
struct session_group *sg, *current; struct session_group *sg, *current;
u_int n, i; u_int n, i;
@@ -370,15 +371,14 @@ window_tree_build(void *modedata, struct sort_criteria *sort_crit,
l = sort_get_sessions(&n, sort_crit); l = sort_get_sessions(&n, sort_crit);
if (n == 0) if (n == 0)
return; return;
s = l[n - 1];
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
if (data->squash_groups && s = l[i];
(sg = session_group_contains(s)) != NULL) { if (squash_groups && (sg = session_group_contains(s)) != NULL) {
if ((sg == current && s != data->fs.s) || if ((sg == current && s != data->fs.s) ||
(sg != current && s != TAILQ_FIRST(&sg->sessions))) (sg != current && s != TAILQ_FIRST(&sg->sessions)))
continue; continue;
} }
window_tree_build_session(l[i], modedata, sort_crit, filter); window_tree_build_session(s, modedata, sort_crit, filter);
} }
switch (data->type) { switch (data->type) {

View File

@@ -18,6 +18,7 @@
#include <sys/types.h> #include <sys/types.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <sys/wait.h>
#include <ctype.h> #include <ctype.h>
#include <errno.h> #include <errno.h>
@@ -380,6 +381,13 @@ window_pane_destroy_ready(struct window_pane *wp)
if (~wp->flags & PANE_EXITED) if (~wp->flags & PANE_EXITED)
return (0); return (0);
/*
* If a command queue item is blocked on this pane, wait for the
* child's exit status before destroying it.
*/
if (wp->block_item != NULL && (~wp->flags & PANE_STATUSREADY))
return (0);
return (1); return (1);
} }
@@ -642,12 +650,32 @@ window_get_active_at(struct window *w, u_int x, u_int y)
pane_status = options_get_number(w->options, "pane-border-status"); pane_status = options_get_number(w->options, "pane-border-status");
if (pane_status == PANE_STATUS_TOP) {
/*
* Prefer a pane's top border status line over the pane above's
* bottom border.
*/
TAILQ_FOREACH(wp, &w->z_index, zentry) {
if (!window_pane_visible(wp) || window_pane_is_floating(wp))
continue;
window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy);
if ((int)x < xoff || x > xoff + sx)
continue;
if ((int)y == yoff - 1)
return (wp);
}
}
TAILQ_FOREACH(wp, &w->z_index, zentry) { TAILQ_FOREACH(wp, &w->z_index, zentry) {
if (!window_pane_visible(wp)) if (!window_pane_visible(wp))
continue; continue;
window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy); window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy);
if (!window_pane_is_floating(wp)) { if (!window_pane_is_floating(wp)) {
/* Tiled - to and including bottom or right border. */ /*
* Tiled - to and including the right border, excluding
* the bottom border.
*/
if ((int)x < xoff || x > xoff + sx) if ((int)x < xoff || x > xoff + sx)
continue; continue;
if (pane_status == PANE_STATUS_TOP) { if (pane_status == PANE_STATUS_TOP) {
@@ -1120,12 +1148,38 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit)
return (wp); return (wp);
} }
void
window_pane_block_finish(struct window_pane *wp)
{
struct cmdq_item *item = wp->block_item;
struct client *c;
int retval = 0;
if (item == NULL)
return;
wp->block_item = NULL;
if (wp->flags & PANE_STATUSREADY) {
if (WIFEXITED(wp->status))
retval = WEXITSTATUS(wp->status);
else if (WIFSIGNALED(wp->status))
retval = WTERMSIG(wp->status) + 128;
}
c = cmdq_get_client(item);
if (c != NULL && c->session == NULL)
c->retval = retval;
cmdq_continue(item);
}
static void static void
window_pane_destroy(struct window_pane *wp) window_pane_destroy(struct window_pane *wp)
{ {
struct window_pane_resize *r; struct window_pane_resize *r;
struct window_pane_resize *r1; struct window_pane_resize *r1;
window_pane_block_finish(wp);
window_pane_reset_mode_all(wp); window_pane_reset_mode_all(wp);
free(wp->searchstr); free(wp->searchstr);
@@ -1280,7 +1334,7 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp,
TAILQ_INSERT_HEAD(&wp->modes, wme, entry); TAILQ_INSERT_HEAD(&wp->modes, wme, entry);
wme->screen = wme->mode->init(wme, fs, args); wme->screen = wme->mode->init(wme, fs, args);
} }
wme->kill = args_has(args, 'k'); wme->kill = args != NULL ? args_has(args, 'k') : 0;
wp->screen = wme->screen; wp->screen = wme->screen;
wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED);