mirror of
https://github.com/tmux/tmux.git
synced 2026-07-22 16:52:42 +00:00
additional payload (to reduce problems with lifetime of objects) and are delivered to one or more event sinks. This is more powerful and reduces the complex dependencies between control mode and hooks. Events are now used for hooks, control mode notifications and for monitors (set-hook -B). wait-for can now wait for an event to fire (-E flag, with -F to for filter), with -v to print the payload, as well as listing (-l) waiting clients on an event and forcing one to wake (-w). A few additional hooks are also now available (pane-created, pane-resized, etc) and some of the existing ones have additional format variables available.
637 lines
16 KiB
C
637 lines
16 KiB
C
/* $OpenBSD: window-switch.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2026 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 <ctype.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "tmux.h"
|
|
|
|
static struct screen *window_switch_init(struct window_mode_entry *,
|
|
struct cmd_find_state *, struct args *);
|
|
static void window_switch_free(struct window_mode_entry *);
|
|
static void window_switch_resize(struct window_mode_entry *, u_int,
|
|
u_int);
|
|
static void window_switch_key(struct window_mode_entry *,
|
|
struct client *, struct session *,
|
|
struct winlink *, key_code, struct mouse_event *);
|
|
static enum prompt_result window_switch_prompt_callback(void *, const char *,
|
|
enum prompt_key_result);
|
|
|
|
#define WINDOW_SWITCH_DEFAULT_COMMAND "switch-client -Zt '%%'"
|
|
|
|
#define WINDOW_SWITCH_DEFAULT_FORMAT \
|
|
"#{?window_format," \
|
|
"#{window_name} " \
|
|
"#[dim]#{session_name}:#{window_index}#{window_flags}#[default] " \
|
|
"#[dim]#{pane_current_command}#[default] " \
|
|
"#[dim]#{?#{!=:#{pane_title},#{host_short}},#{pane_title},}#[default]" \
|
|
"," \
|
|
"#{session_name} " \
|
|
"#[dim]#{session_windows} windows#[default] " \
|
|
"#{?session_attached,attached,#[dim]detached#[default]} " \
|
|
"#[dim]#{window_name}#[default]" \
|
|
"}"
|
|
|
|
const struct window_mode window_switch_mode = {
|
|
.name = "switch-mode",
|
|
.default_format = WINDOW_SWITCH_DEFAULT_FORMAT,
|
|
|
|
.init = window_switch_init,
|
|
.free = window_switch_free,
|
|
.resize = window_switch_resize,
|
|
.key = window_switch_key,
|
|
};
|
|
|
|
enum window_switch_type {
|
|
WINDOW_SWITCH_TYPE_SESSION,
|
|
WINDOW_SWITCH_TYPE_WINDOW
|
|
};
|
|
|
|
struct window_switch_itemdata {
|
|
enum window_switch_type type;
|
|
int session;
|
|
int winlink;
|
|
|
|
uint64_t tag;
|
|
char *text;
|
|
bitstr_t *match;
|
|
|
|
u_int score;
|
|
u_int order;
|
|
};
|
|
|
|
struct window_switch_modedata {
|
|
struct window_pane *wp;
|
|
struct screen screen;
|
|
int zoomed;
|
|
|
|
char *format;
|
|
char *command;
|
|
|
|
enum window_switch_type type;
|
|
char *filter;
|
|
struct prompt *prompt;
|
|
u_int prompt_cx;
|
|
|
|
struct window_switch_itemdata **item_list;
|
|
u_int item_size;
|
|
|
|
struct window_switch_itemdata **matches;
|
|
u_int matches_size;
|
|
|
|
u_int current;
|
|
u_int offset;
|
|
};
|
|
|
|
static void
|
|
window_switch_free_item(struct window_switch_itemdata *item)
|
|
{
|
|
free(item->match);
|
|
free(item->text);
|
|
free(item);
|
|
}
|
|
|
|
static struct window_switch_itemdata *
|
|
window_switch_add_item(struct window_switch_modedata *data)
|
|
{
|
|
struct window_switch_itemdata *item;
|
|
|
|
data->item_list = xreallocarray(data->item_list, data->item_size + 1,
|
|
sizeof *data->item_list);
|
|
item = data->item_list[data->item_size++] = xcalloc(1, sizeof *item);
|
|
return (item);
|
|
}
|
|
|
|
static void
|
|
window_switch_add_session(struct window_switch_modedata *data,
|
|
struct session *s, u_int *order)
|
|
{
|
|
struct window_switch_itemdata *item;
|
|
struct format_tree *ft;
|
|
|
|
ft = format_create(NULL, NULL, FORMAT_NONE, 0);
|
|
format_defaults(ft, NULL, s, NULL, NULL);
|
|
|
|
item = window_switch_add_item(data);
|
|
item->type = WINDOW_SWITCH_TYPE_SESSION;
|
|
item->session = s->id;
|
|
item->winlink = -1;
|
|
item->tag = (uint64_t)s;
|
|
item->order = (*order)++;
|
|
item->text = format_expand(ft, data->format);
|
|
|
|
format_free(ft);
|
|
}
|
|
|
|
static void
|
|
window_switch_add_window(struct window_switch_modedata *data,
|
|
struct winlink *wl, u_int *order)
|
|
{
|
|
struct window_switch_itemdata *item;
|
|
struct format_tree *ft;
|
|
|
|
ft = format_create(NULL, NULL, FORMAT_NONE, 0);
|
|
format_defaults(ft, NULL, wl->session, wl, NULL);
|
|
|
|
item = window_switch_add_item(data);
|
|
item->type = WINDOW_SWITCH_TYPE_WINDOW;
|
|
item->session = wl->session->id;
|
|
item->winlink = wl->idx;
|
|
item->tag = (uint64_t)wl;
|
|
item->order = (*order)++;
|
|
item->text = format_expand(ft, data->format);
|
|
|
|
format_free(ft);
|
|
}
|
|
|
|
static int
|
|
window_switch_compare(const void *a0, const void *b0)
|
|
{
|
|
struct window_switch_itemdata *const *a = a0;
|
|
struct window_switch_itemdata *const *b = b0;
|
|
|
|
if ((*a)->score > (*b)->score)
|
|
return (-1);
|
|
if ((*a)->score < (*b)->score)
|
|
return (1);
|
|
if ((*a)->order < (*b)->order)
|
|
return (-1);
|
|
if ((*a)->order > (*b)->order)
|
|
return (1);
|
|
return (0);
|
|
}
|
|
|
|
static void
|
|
window_switch_build(struct window_switch_modedata *data)
|
|
{
|
|
struct window_switch_itemdata *item, **m = NULL;
|
|
const char *f = data->filter;
|
|
u_int ns, nw, i, n = 0, order = 0;
|
|
u_int sx = screen_size_x(&data->screen);
|
|
struct session **sl;
|
|
struct winlink **wl;
|
|
struct sort_criteria sort_crit;
|
|
|
|
sort_crit.order = SORT_NAME;
|
|
sort_crit.reversed = 0;
|
|
|
|
for (i = 0; i < data->item_size; i++)
|
|
window_switch_free_item(data->item_list[i]);
|
|
free(data->item_list);
|
|
data->item_list = NULL;
|
|
data->item_size = 0;
|
|
|
|
switch (data->type) {
|
|
case WINDOW_SWITCH_TYPE_SESSION:
|
|
sl = sort_get_sessions(&ns, &sort_crit);
|
|
for (i = 0; i < ns; i++)
|
|
window_switch_add_session(data, sl[i], &order);
|
|
break;
|
|
case WINDOW_SWITCH_TYPE_WINDOW:
|
|
wl = sort_get_winlinks(&nw, &sort_crit);
|
|
for (i = 0; i < nw; i++)
|
|
window_switch_add_window(data, wl[i], &order);
|
|
break;
|
|
}
|
|
|
|
for (i = 0; i < data->item_size; i++) {
|
|
item = data->item_list[i];
|
|
if (*f == '\0') {
|
|
m = xreallocarray(m, n + 1, sizeof *m);
|
|
m[n++] = item;
|
|
continue;
|
|
}
|
|
|
|
item->match = fuzzy_match(f, item->text, sx, &item->score);
|
|
if (item->match == NULL)
|
|
continue;
|
|
m = xreallocarray(m, n + 1, sizeof *m);
|
|
m[n++] = item;
|
|
}
|
|
qsort(m, n, sizeof *m, window_switch_compare);
|
|
|
|
free(data->matches);
|
|
data->matches = m;
|
|
data->matches_size = n;
|
|
}
|
|
|
|
static u_int
|
|
window_switch_visible(struct window_switch_modedata *data)
|
|
{
|
|
u_int sy = screen_size_y(&data->screen);
|
|
|
|
if (sy <= 1)
|
|
return (0);
|
|
return (sy - 1);
|
|
}
|
|
|
|
static void
|
|
window_switch_set_current(struct window_switch_modedata *data, u_int current)
|
|
{
|
|
u_int visible = window_switch_visible(data);
|
|
|
|
if (data->matches_size == 0) {
|
|
data->current = 0;
|
|
data->offset = 0;
|
|
return;
|
|
}
|
|
|
|
if (current > data->matches_size - 1)
|
|
current = data->matches_size - 1;
|
|
data->current = current;
|
|
|
|
if (data->current < data->offset)
|
|
data->offset = data->current;
|
|
else if (visible != 0 && data->current >= data->offset + visible)
|
|
data->offset = data->current - visible + 1;
|
|
}
|
|
|
|
static void
|
|
window_switch_draw_screen(struct window_mode_entry *wme)
|
|
{
|
|
struct window_pane *wp = wme->wp;
|
|
struct window_switch_modedata *data = wme->data;
|
|
struct options *oo = wp->options;
|
|
struct screen_write_ctx ctx;
|
|
struct screen *s = &data->screen;
|
|
u_int sx = screen_size_x(s), i, j;
|
|
u_int sy = screen_size_y(s), visible, idx;
|
|
struct window_switch_itemdata *item;
|
|
struct grid_cell mgc, sgc, gc;
|
|
const struct grid_cell *dgc = &grid_default_cell;
|
|
struct prompt_draw_data pdd;
|
|
screen_write_start(&ctx, s);
|
|
screen_write_clearscreen(&ctx, 8);
|
|
|
|
if (sy <= 1) {
|
|
screen_write_stop(&ctx);
|
|
return;
|
|
}
|
|
|
|
style_apply(&mgc, oo, "switch-mode-match-style", NULL);
|
|
style_apply(&sgc, oo, "mode-style", NULL);
|
|
|
|
visible = window_switch_visible(data);
|
|
for (i = 0; i < visible; i++) {
|
|
idx = data->offset + i;
|
|
if (idx >= data->matches_size)
|
|
break;
|
|
item = data->matches[idx];
|
|
|
|
screen_write_cursormove(&ctx, 0, i, 0);
|
|
if (idx != data->current)
|
|
format_draw(&ctx, dgc, sx, item->text, NULL, 0);
|
|
else {
|
|
screen_write_clearendofline(&ctx, sgc.bg);
|
|
format_draw(&ctx, &sgc, sx, item->text, NULL, 0);
|
|
}
|
|
|
|
if (item->match == NULL)
|
|
continue;
|
|
for (j = 0; j < sx; j++) {
|
|
if (!bit_test(item->match, j))
|
|
continue;
|
|
grid_get_cell(s->grid, j, i, &gc);
|
|
gc.attr = mgc.attr;
|
|
gc.fg = mgc.fg;
|
|
gc.bg = mgc.bg;
|
|
screen_write_cursormove(&ctx, j, i, 0);
|
|
screen_write_cell(&ctx, &gc);
|
|
}
|
|
}
|
|
|
|
if (data->prompt != NULL) {
|
|
pdd.ctx = &ctx;
|
|
pdd.cursor_x = &data->prompt_cx;
|
|
pdd.area_x = 0;
|
|
pdd.area_width = sx;
|
|
pdd.prompt_line = sy - 1;
|
|
s->mode |= MODE_CURSOR;
|
|
prompt_draw(data->prompt, &pdd);
|
|
screen_write_cursormove(&ctx, data->prompt_cx, sy - 1, 0);
|
|
}
|
|
screen_write_stop(&ctx);
|
|
}
|
|
|
|
static struct screen *
|
|
window_switch_init(struct window_mode_entry *wme,
|
|
struct cmd_find_state *fs, struct args *args)
|
|
{
|
|
struct window_pane *wp = wme->wp;
|
|
struct window_switch_modedata *data;
|
|
struct screen *s;
|
|
struct prompt_create_data pd;
|
|
|
|
wme->data = data = xcalloc(1, sizeof *data);
|
|
data->wp = wp;
|
|
|
|
if (args_has(args, 'w'))
|
|
data->type = WINDOW_SWITCH_TYPE_WINDOW;
|
|
else
|
|
data->type = WINDOW_SWITCH_TYPE_SESSION;
|
|
|
|
data->filter = xstrdup("");
|
|
if (args == NULL || !args_has(args, 'F'))
|
|
data->format = xstrdup(WINDOW_SWITCH_DEFAULT_FORMAT);
|
|
else
|
|
data->format = xstrdup(args_get(args, 'F'));
|
|
if (args == NULL || args_count(args) == 0)
|
|
data->command = xstrdup(WINDOW_SWITCH_DEFAULT_COMMAND);
|
|
else
|
|
data->command = xstrdup(args_string(args, 0));
|
|
|
|
memset(&pd, 0, sizeof pd);
|
|
prompt_set_options(&pd, fs->s);
|
|
pd.fs = fs;
|
|
pd.prompt = "(search) ";
|
|
pd.input = "";
|
|
pd.type = PROMPT_TYPE_SEARCH;
|
|
pd.flags = PROMPT_INCREMENTAL|PROMPT_NOFORMAT|PROMPT_ISMODE|
|
|
PROMPT_EDITARROWS;
|
|
pd.inputcb = window_switch_prompt_callback;
|
|
pd.data = data;
|
|
data->prompt = prompt_create(&pd);
|
|
prompt_update(data->prompt, "(search) ", data->filter);
|
|
|
|
if (!args_has(args, 'Z'))
|
|
data->zoomed = -1;
|
|
else {
|
|
data->zoomed = (wp->window->flags & WINDOW_ZOOMED);
|
|
if (!data->zoomed && window_zoom(wp) == 0)
|
|
server_redraw_window(wp->window);
|
|
}
|
|
|
|
s = &data->screen;
|
|
screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0);
|
|
|
|
window_switch_build(data);
|
|
prompt_incremental_start(data->prompt);
|
|
window_switch_draw_screen(wme);
|
|
|
|
return (s);
|
|
}
|
|
|
|
static void
|
|
window_switch_free(struct window_mode_entry *wme)
|
|
{
|
|
struct window_switch_modedata *data = wme->data;
|
|
u_int i;
|
|
|
|
if (data->zoomed == 0)
|
|
server_unzoom_window(wme->wp->window);
|
|
|
|
for (i = 0; i < data->item_size; i++)
|
|
window_switch_free_item(data->item_list[i]);
|
|
free(data->item_list);
|
|
|
|
free(data->matches);
|
|
free(data->filter);
|
|
prompt_free(data->prompt);
|
|
free(data->format);
|
|
free(data->command);
|
|
screen_free(&data->screen);
|
|
|
|
free(data);
|
|
}
|
|
|
|
static void
|
|
window_switch_resize(struct window_mode_entry *wme, u_int sx, u_int sy)
|
|
{
|
|
struct window_switch_modedata *data = wme->data;
|
|
struct screen *s = &data->screen;
|
|
|
|
screen_resize(s, sx, sy, 0);
|
|
window_switch_build(data);
|
|
window_switch_set_current(data, data->current);
|
|
window_switch_draw_screen(wme);
|
|
}
|
|
|
|
static int
|
|
window_switch_run_command(struct window_switch_modedata *data, struct client *c)
|
|
{
|
|
struct window_switch_itemdata *item;
|
|
struct cmd_find_state fs;
|
|
struct session *s;
|
|
struct winlink *wl;
|
|
char *target = NULL;
|
|
struct cmdq_state *state;
|
|
char *command, *error;
|
|
enum cmd_parse_status status;
|
|
|
|
if (data->matches_size == 0)
|
|
return (0);
|
|
item = data->matches[data->current];
|
|
|
|
cmd_find_clear_state(&fs, 0);
|
|
switch (item->type) {
|
|
case WINDOW_SWITCH_TYPE_SESSION:
|
|
s = session_find_by_id(item->session);
|
|
if (s != NULL) {
|
|
xasprintf(&target, "=%s:", s->name);
|
|
cmd_find_from_session(&fs, s, 0);
|
|
}
|
|
break;
|
|
case WINDOW_SWITCH_TYPE_WINDOW:
|
|
s = session_find_by_id(item->session);
|
|
if (s != NULL) {
|
|
wl = winlink_find_by_index(&s->windows, item->winlink);
|
|
if (s != NULL && wl != NULL) {
|
|
xasprintf(&target, "=%s:%u.", s->name, wl->idx);
|
|
cmd_find_from_winlink(&fs, wl, 0);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (target == NULL)
|
|
return (0);
|
|
|
|
command = cmd_template_replace(data->command, target, 1);
|
|
if (command != NULL && *command != '\0') {
|
|
state = cmdq_new_state(&fs, NULL, 0);
|
|
status = cmd_parse_and_append(command, NULL, c, state, &error);
|
|
if (status == CMD_PARSE_ERROR) {
|
|
if (c != NULL) {
|
|
*error = toupper((u_char)*error);
|
|
status_message_set(c, -1, 1, 0, 0, "%s", error);
|
|
}
|
|
free(error);
|
|
}
|
|
cmdq_free_state(state);
|
|
}
|
|
free(command);
|
|
free(target);
|
|
return (1);
|
|
}
|
|
|
|
static enum prompt_result
|
|
window_switch_prompt_callback(void *arg, const char *s,
|
|
enum prompt_key_result key)
|
|
{
|
|
struct window_switch_modedata *data = arg;
|
|
|
|
if (key != PROMPT_KEY_HANDLED)
|
|
return (PROMPT_CONTINUE);
|
|
|
|
if (s == NULL)
|
|
s = "";
|
|
else if (*s != '\0')
|
|
s++;
|
|
|
|
free(data->filter);
|
|
data->filter = xstrdup(s);
|
|
window_switch_build(data);
|
|
data->current = 0;
|
|
data->offset = 0;
|
|
|
|
return (PROMPT_CONTINUE);
|
|
}
|
|
|
|
static void
|
|
window_switch_key(struct window_mode_entry *wme, struct client *c,
|
|
__unused struct session *s, __unused struct winlink *wl, key_code key,
|
|
struct mouse_event *m)
|
|
{
|
|
struct window_pane *wp = wme->wp;
|
|
struct window_switch_modedata *data = wme->data;
|
|
u_int visible, current = data->current;
|
|
u_int x, y, size = data->matches_size;
|
|
enum prompt_key_result result;
|
|
int redraw = 0;
|
|
|
|
if (KEYC_IS_MOUSE(key)) {
|
|
if (m == NULL || cmd_mouse_at(wp, m, &x, &y, 0) != 0)
|
|
return;
|
|
if (data->prompt != NULL && screen_size_y(&data->screen) != 0 &&
|
|
y == screen_size_y(&data->screen) - 1 &&
|
|
MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_1 && !MOUSE_DRAG(m->b) &&
|
|
!MOUSE_RELEASE(m->b)) {
|
|
result = prompt_mouse(data->prompt, x, 0,
|
|
screen_size_x(&data->screen), &redraw);
|
|
if (redraw || result == PROMPT_KEY_HANDLED) {
|
|
window_switch_draw_screen(wme);
|
|
wp->flags |= PANE_REDRAW;
|
|
}
|
|
return;
|
|
}
|
|
switch (key) {
|
|
case KEYC_WHEELUP_PANE:
|
|
if (size != 0 && current != 0)
|
|
window_switch_set_current(data, current - 1);
|
|
goto moved;
|
|
case KEYC_WHEELDOWN_PANE:
|
|
if (size != 0 && current != size - 1)
|
|
window_switch_set_current(data, current + 1);
|
|
goto moved;
|
|
case KEYC_MOUSEDOWN1_PANE:
|
|
case KEYC_DOUBLECLICK1_PANE:
|
|
if (y >= window_switch_visible(data) ||
|
|
data->offset + y >= size)
|
|
return;
|
|
window_switch_set_current(data, data->offset + y);
|
|
if (key == KEYC_DOUBLECLICK1_PANE) {
|
|
if (window_switch_run_command(data, c))
|
|
window_pane_reset_mode(wp);
|
|
return;
|
|
}
|
|
goto moved;
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (key) {
|
|
case 'p'|KEYC_CTRL:
|
|
case 'k'|KEYC_CTRL:
|
|
key = KEYC_UP;
|
|
break;
|
|
case 'n'|KEYC_CTRL:
|
|
case 'j'|KEYC_CTRL:
|
|
key = KEYC_DOWN;
|
|
break;
|
|
}
|
|
|
|
switch (key) {
|
|
case '\r':
|
|
if (window_switch_run_command(data, c))
|
|
window_pane_reset_mode(wp);
|
|
return;
|
|
case '\033': /* Escape */
|
|
case '['|KEYC_CTRL:
|
|
case 'c'|KEYC_CTRL:
|
|
case 'g'|KEYC_CTRL:
|
|
window_pane_reset_mode(wp);
|
|
return;
|
|
}
|
|
|
|
if (data->prompt != NULL) {
|
|
result = prompt_key(data->prompt, key, &redraw);
|
|
if (redraw) {
|
|
window_switch_draw_screen(wme);
|
|
wp->flags |= PANE_REDRAW;
|
|
}
|
|
if (result == PROMPT_KEY_HANDLED ||
|
|
result == PROMPT_KEY_NOT_HANDLED)
|
|
return;
|
|
current = data->current;
|
|
size = data->matches_size;
|
|
}
|
|
|
|
switch (key) {
|
|
case KEYC_UP:
|
|
if (size == 0)
|
|
goto moved;
|
|
if (current == 0)
|
|
window_switch_set_current(data, size - 1);
|
|
else
|
|
window_switch_set_current(data, current - 1);
|
|
goto moved;
|
|
case KEYC_DOWN:
|
|
if (size == 0)
|
|
goto moved;
|
|
if (current == size - 1)
|
|
window_switch_set_current(data, 0);
|
|
else
|
|
window_switch_set_current(data, current + 1);
|
|
goto moved;
|
|
case KEYC_PPAGE:
|
|
visible = window_switch_visible(data);
|
|
if (current >= visible)
|
|
window_switch_set_current(data, current - visible);
|
|
else
|
|
window_switch_set_current(data, 0);
|
|
goto moved;
|
|
case KEYC_NPAGE:
|
|
visible = window_switch_visible(data);
|
|
window_switch_set_current(data, current + visible);
|
|
goto moved;
|
|
case KEYC_HOME:
|
|
window_switch_set_current(data, 0);
|
|
goto moved;
|
|
case KEYC_END:
|
|
if (size > 0)
|
|
window_switch_set_current(data, size - 1);
|
|
goto moved;
|
|
}
|
|
|
|
moved:
|
|
window_switch_draw_screen(wme);
|
|
wp->flags |= PANE_REDRAW;
|
|
}
|