root/usr.bin/tmux/popup.c
/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 nicm Exp $ */

/*
 * Copyright (c) 2020 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/wait.h>

#include <paths.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "tmux.h"

struct popup_data {
        struct client            *c;
        struct cmdq_item         *item;
        int                       flags;
        char                     *title;

        char                     *style;
        char                     *border_style;
        struct grid_cell          border_cell;
        enum box_lines            border_lines;

        struct screen             s;
        struct grid_cell          defaults;
        struct colour_palette     palette;

        struct visible_ranges     r;

        struct job               *job;
        struct input_ctx         *ictx;
        int                       status;
        popup_close_cb            cb;
        void                     *arg;

        int                       close;

        /* Current position and size. */
        u_int                     px;
        u_int                     py;
        u_int                     sx;
        u_int                     sy;

        /* Preferred position and size. */
        u_int                     ppx;
        u_int                     ppy;
        u_int                     psx;
        u_int                     psy;

        enum { OFF, MOVE, SIZE }  dragging;
        u_int                     dx;
        u_int                     dy;

        u_int                     lx;
        u_int                     ly;
        u_int                     lb;
};

static void
popup_free(struct popup_data *pd)
{
        server_client_unref(pd->c);

        if (pd->job != NULL)
                job_free(pd->job);
        input_free(pd->ictx);

        free(pd->r.ranges);
        screen_free(&pd->s);
        colour_palette_free(&pd->palette);

        free(pd->title);
        free(pd->style);
        free(pd->border_style);
        free(pd);
}

static void
popup_reapply_styles(struct popup_data *pd)
{
        struct client           *c = pd->c;
        struct session          *s = c->session;
        struct options          *o;
        struct format_tree      *ft;
        struct style             sytmp;

        if (s == NULL)
                return;
        o = s->curw->window->options;

        ft = format_create_defaults(NULL, c, s, s->curw, NULL);

        /* Reapply popup style from options. */
        memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
        style_apply(&pd->defaults, o, "popup-style", ft);
        if (pd->style != NULL) {
                style_set(&sytmp, &grid_default_cell);
                if (style_parse(&sytmp, &pd->defaults, pd->style) == 0) {
                        pd->defaults.fg = sytmp.gc.fg;
                        pd->defaults.bg = sytmp.gc.bg;
                }
        }
        pd->defaults.attr = 0;

        /* Reapply border style from options. */
        memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
        style_apply(&pd->border_cell, o, "popup-border-style", ft);
        if (pd->border_style != NULL) {
                style_set(&sytmp, &grid_default_cell);
                if (style_parse(&sytmp, &pd->border_cell,
                    pd->border_style) == 0) {
                        pd->border_cell.fg = sytmp.gc.fg;
                        pd->border_cell.bg = sytmp.gc.bg;
                }
        }
        pd->border_cell.attr = 0;

        format_free(ft);
}

static void
popup_redraw_cb(const struct tty_ctx *ttyctx)
{
        struct popup_data       *pd = ttyctx->arg;

        pd->c->flags |= CLIENT_REDRAWOVERLAY;
}

static int
popup_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
        struct popup_data       *pd = ttyctx->arg;

        if (c != pd->c)
                return (0);
        if (pd->c->flags & CLIENT_REDRAWOVERLAY)
                return (0);

        ttyctx->wox = 0;
        ttyctx->woy = 0;
        ttyctx->wsx = c->tty.sx;
        ttyctx->wsy = c->tty.sy;

        if (pd->border_lines == BOX_LINES_NONE) {
                ttyctx->xoff = ttyctx->rxoff = pd->px;
                ttyctx->yoff = ttyctx->ryoff = pd->py;
        } else {
                ttyctx->xoff = ttyctx->rxoff = pd->px + 1;
                ttyctx->yoff = ttyctx->ryoff = pd->py + 1;
        }

        return (1);
}

static void
popup_init_ctx_cb(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
{
        struct popup_data       *pd = ctx->arg;

        memcpy(&ttyctx->defaults, &pd->defaults, sizeof ttyctx->defaults);
        ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
        ttyctx->style_ctx.defaults = &ttyctx->defaults;
        ttyctx->style_ctx.palette = &pd->palette;
        ttyctx->redraw_cb = popup_redraw_cb;
        ttyctx->set_client_cb = popup_set_client_cb;
        ttyctx->arg = pd;
}

static struct screen *
popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
{
        struct popup_data       *pd = data;

        if (pd->border_lines == BOX_LINES_NONE) {
                *cx = pd->px + pd->s.cx;
                *cy = pd->py + pd->s.cy;
        } else {
                *cx = pd->px + 1 + pd->s.cx;
                *cy = pd->py + 1 + pd->s.cy;
        }
        return (&pd->s);
}

/* Return parts of the input range which are not obstructed by the popup. */
static struct visible_ranges *
popup_check_cb(__unused struct client* c, void *data, u_int px, u_int py,
    u_int nx)
{
        struct popup_data       *pd = data;
        struct visible_ranges   *r = &pd->r;

        server_client_overlay_range(pd->px, pd->py, pd->sx, pd->sy, px, py, nx,
            r);
        return (r);
}

static void
popup_draw_cb(struct client *c, void *data)
{
        struct popup_data       *pd = data;
        struct tty              *tty = &c->tty;
        struct screen            s;
        struct screen_write_ctx  ctx;
        u_int                    i, px = pd->px, py = pd->py;
        struct grid_cell         defaults;
        struct tty_style_ctx     style_ctx;

        popup_reapply_styles(pd);

        screen_init(&s, pd->sx, pd->sy, 0);
        if (pd->s.hyperlinks != NULL) {
                hyperlinks_free(s.hyperlinks);
                s.hyperlinks = hyperlinks_copy(pd->s.hyperlinks);
        }
        screen_write_start(&ctx, &s);
        screen_write_clearscreen(&ctx, 8);

        if (pd->border_lines == BOX_LINES_NONE) {
                screen_write_cursormove(&ctx, 0, 0, 0);
                screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx, pd->sy);
        } else if (pd->sx > 2 && pd->sy > 2) {
                screen_write_box(&ctx, pd->sx, pd->sy, pd->border_lines,
                    &pd->border_cell, pd->title);
                screen_write_cursormove(&ctx, 1, 1, 0);
                screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx - 2,
                    pd->sy - 2);
        }
        screen_write_stop(&ctx);

        memcpy(&defaults, &pd->defaults, sizeof defaults);
        if (defaults.fg == 8)
                defaults.fg = pd->palette.fg;
        if (defaults.bg == 8)
                defaults.bg = pd->palette.bg;
        style_ctx.defaults = &defaults;
        style_ctx.palette = &pd->palette;
        style_ctx.dim = 0;
        style_ctx.hyperlinks = s.hyperlinks;

        c->overlay_check = NULL;
        c->overlay_data = NULL;
        for (i = 0; i < pd->sy; i++)
                tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i, &style_ctx);
        screen_free(&s);
        c->overlay_check = popup_check_cb;
        c->overlay_data = pd;
}

static void
popup_free_cb(__unused struct client *c, void *data)
{
        struct popup_data       *pd = data;
        struct cmdq_item        *item = pd->item;

        if (pd->cb != NULL)
                pd->cb(pd->status, pd->arg);

        if (item != NULL) {
                if (cmdq_get_client(item) != NULL &&
                    cmdq_get_client(item)->session == NULL)
                        cmdq_get_client(item)->retval = pd->status;
                cmdq_continue(item);
        }

        popup_free(pd);
}

static void
popup_resize_cb(__unused struct client *c, void *data)
{
        struct popup_data       *pd = data;
        struct tty              *tty = &c->tty;

        if (pd == NULL)
                return;

        /* Adjust position and size. */
        if (pd->psy > tty->sy)
                pd->sy = tty->sy;
        else
                pd->sy = pd->psy;
        if (pd->psx > tty->sx)
                pd->sx = tty->sx;
        else
                pd->sx = pd->psx;
        if (pd->ppy + pd->sy > tty->sy)
                pd->py = tty->sy - pd->sy;
        else
                pd->py = pd->ppy;
        if (pd->ppx + pd->sx > tty->sx)
                pd->px = tty->sx - pd->sx;
        else
                pd->px = pd->ppx;

        /* Avoid zero size screens. */
        if (pd->border_lines == BOX_LINES_NONE) {
                screen_resize(&pd->s, pd->sx, pd->sy, 0);
                if (pd->job != NULL)
                        job_resize(pd->job, pd->sx, pd->sy );
        } else if (pd->sx > 2 && pd->sy > 2) {
                screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
                if (pd->job != NULL)
                        job_resize(pd->job, pd->sx - 2, pd->sy - 2);
        }
}

static void
popup_handle_drag(struct client *c, struct popup_data *pd,
    struct mouse_event *m)
{
        u_int   px, py;

        if (!MOUSE_DRAG(m->b))
                pd->dragging = OFF;
        else if (pd->dragging == MOVE) {
                if (m->x < pd->dx)
                        px = 0;
                else if (m->x - pd->dx + pd->sx > c->tty.sx)
                        px = c->tty.sx - pd->sx;
                else
                        px = m->x - pd->dx;
                if (m->y < pd->dy)
                        py = 0;
                else if (m->y - pd->dy + pd->sy > c->tty.sy)
                        py = c->tty.sy - pd->sy;
                else
                        py = m->y - pd->dy;
                pd->px = px;
                pd->py = py;
                pd->dx = m->x - pd->px;
                pd->dy = m->y - pd->py;
                pd->ppx = px;
                pd->ppy = py;
                server_redraw_client(c);
        } else if (pd->dragging == SIZE) {
                if (pd->border_lines == BOX_LINES_NONE) {
                        if (m->x < pd->px + 1)
                                return;
                        if (m->y < pd->py + 1)
                                return;
                } else {
                        if (m->x < pd->px + 3)
                                return;
                        if (m->y < pd->py + 3)
                                return;
                }
                pd->sx = m->x - pd->px;
                pd->sy = m->y - pd->py;
                pd->psx = pd->sx;
                pd->psy = pd->sy;

                if (pd->border_lines == BOX_LINES_NONE) {
                        screen_resize(&pd->s, pd->sx, pd->sy, 0);
                        if (pd->job != NULL)
                                job_resize(pd->job, pd->sx, pd->sy);
                } else {
                        screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
                        if (pd->job != NULL)
                                job_resize(pd->job, pd->sx - 2, pd->sy - 2);
                }
                server_redraw_client(c);
        }
}

static int
popup_key_cb(struct client *c, void *data, struct key_event *event)
{
        struct popup_data       *pd = data;
        struct mouse_event      *m = &event->m;
        const char              *buf;
        size_t                   len;
        u_int                    px, py;
        enum { NONE, LEFT, RIGHT, TOP, BOTTOM } border = NONE;

        if (KEYC_IS_MOUSE(event->key)) {
                if (pd->dragging != OFF) {
                        popup_handle_drag(c, pd, m);
                        goto out;
                }
                if (m->x < pd->px ||
                    m->x > pd->px + pd->sx - 1 ||
                    m->y < pd->py ||
                    m->y > pd->py + pd->sy - 1) {
                        return (0);
                }
                if (pd->border_lines != BOX_LINES_NONE) {
                        if (m->x == pd->px)
                                border = LEFT;
                        else if (m->x == pd->px + pd->sx - 1)
                                border = RIGHT;
                        else if (m->y == pd->py)
                                border = TOP;
                        else if (m->y == pd->py + pd->sy - 1)
                                border = BOTTOM;
                }
                if ((m->b & MOUSE_MASK_MODIFIERS) == 0 &&
                    MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3 &&
                    (border == LEFT || border == TOP))
                        goto out;
                if (((m->b & MOUSE_MASK_MODIFIERS) == MOUSE_MASK_META) ||
                    (border != NONE && !MOUSE_DRAG(m->lb))) {
                        if (!MOUSE_DRAG(m->b))
                                goto out;
                        if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_1)
                                pd->dragging = MOVE;
                        else if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_3)
                                pd->dragging = SIZE;
                        pd->dx = m->lx - pd->px;
                        pd->dy = m->ly - pd->py;
                        goto out;
                }
        }
        if ((((pd->flags & (POPUP_CLOSEEXIT|POPUP_CLOSEEXITZERO)) == 0) ||
            pd->job == NULL) &&
            (event->key == '\033' || event->key == ('c'|KEYC_CTRL)))
                return (1);
        if (pd->job == NULL && (pd->flags & POPUP_CLOSEANYKEY) &&
            !KEYC_IS_MOUSE(event->key) && !KEYC_IS_PASTE(event->key))
                return (1);
        if (pd->job != NULL) {
                if (KEYC_IS_MOUSE(event->key)) {
                        /* Must be inside, checked already. */
                        if (pd->border_lines == BOX_LINES_NONE) {
                                px = m->x - pd->px;
                                py = m->y - pd->py;
                        } else {
                                px = m->x - pd->px - 1;
                                py = m->y - pd->py - 1;
                        }
                        if (!input_key_get_mouse(&pd->s, m, px, py, &buf, &len))
                                return (0);
                        bufferevent_write(job_get_event(pd->job), buf, len);
                        return (0);
                }
                input_key(&pd->s, job_get_event(pd->job), event->key);
        }
        return (0);

out:
        pd->lx = m->x;
        pd->ly = m->y;
        pd->lb = m->b;
        return (0);
}

static void
popup_job_update_cb(struct job *job)
{
        struct popup_data       *pd = job_get_data(job);
        struct evbuffer         *evb = job_get_event(job)->input;
        struct client           *c = pd->c;
        struct screen           *s = &pd->s;
        void                    *data = EVBUFFER_DATA(evb);
        size_t                   size = EVBUFFER_LENGTH(evb);

        if (size == 0)
                return;

        c->overlay_check = NULL;
        c->overlay_data = NULL;
        input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size);
        c->overlay_check = popup_check_cb;
        c->overlay_data = pd;

        evbuffer_drain(evb, size);
}

static void
popup_job_complete_cb(struct job *job)
{
        struct popup_data       *pd = job_get_data(job);
        int                      status;

        status = job_get_status(pd->job);
        if (WIFEXITED(status))
                pd->status = WEXITSTATUS(status);
        else if (WIFSIGNALED(status))
                pd->status = WTERMSIG(status);
        else
                pd->status = 0;
        pd->job = NULL;

        if ((pd->flags & POPUP_CLOSEEXIT) ||
            ((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
                server_client_clear_overlay(pd->c);
}

int
popup_present(struct client *c)
{
        return (c->overlay_draw == popup_draw_cb);
}

int
popup_modify(struct client *c, const char *title, const char *style,
        const char *border_style, enum box_lines lines, int flags)
{
        struct popup_data               *pd = c->overlay_data;
        struct style            sytmp;

        if (title != NULL) {
                if (pd->title != NULL)
                        free(pd->title);
                pd->title = xstrdup(title);
        }
        if (border_style != NULL) {
                free(pd->border_style);
                pd->border_style = xstrdup(border_style);
                style_set(&sytmp, &pd->border_cell);
                if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
                        pd->border_cell.fg = sytmp.gc.fg;
                        pd->border_cell.bg = sytmp.gc.bg;
                }
        }
        if (style != NULL) {
                free(pd->style);
                pd->style = xstrdup(style);
                style_set(&sytmp, &pd->defaults);
                if (style_parse(&sytmp, &pd->defaults, style) == 0) {
                        pd->defaults.fg = sytmp.gc.fg;
                        pd->defaults.bg = sytmp.gc.bg;
                }
        }
        if (lines != BOX_LINES_DEFAULT) {
                if (lines == BOX_LINES_NONE && pd->border_lines != lines) {
                        screen_resize(&pd->s, pd->sx, pd->sy, 1);
                        job_resize(pd->job, pd->sx, pd->sy);
                } else if (pd->border_lines == BOX_LINES_NONE &&
                    pd->border_lines != lines) {
                        screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 1);
                        job_resize(pd->job, pd->sx - 2, pd->sy - 2);
                }
                pd->border_lines = lines;
                tty_resize(&c->tty);
        }
        if (flags != -1)
                pd->flags = flags;

        server_redraw_client(c);
        return (0);
}

int
popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
    u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd,
    int argc, char **argv, const char *cwd, const char *title, struct client *c,
    struct session *s, const char *style, const char *border_style,
    popup_close_cb cb, void *arg)
{
        struct popup_data       *pd;
        u_int                    jx, jy;
        struct options          *o;
        struct style             sytmp;

        if (s != NULL)
                o = s->curw->window->options;
        else
                o = c->session->curw->window->options;

        if (lines == BOX_LINES_DEFAULT)
                lines = options_get_number(o, "popup-border-lines");
        if (lines == BOX_LINES_NONE) {
                if (sx < 1 || sy < 1)
                        return (-1);
                jx = sx;
                jy = sy;
        } else {
                if (sx < 3 || sy < 3)
                        return (-1);
                jx = sx - 2;
                jy = sy - 2;
        }
        if (c->tty.sx < sx || c->tty.sy < sy)
                return (-1);

        pd = xcalloc(1, sizeof *pd);
        pd->item = item;
        pd->flags = flags;

        if (title != NULL)
                pd->title = xstrdup(title);
        if (style != NULL)
                pd->style = xstrdup(style);
        if (border_style != NULL)
                pd->border_style = xstrdup(border_style);

        pd->c = c;
        pd->c->references++;

        pd->cb = cb;
        pd->arg = arg;
        pd->status = 128 + SIGHUP;

        pd->border_lines = lines;
        memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
        style_apply(&pd->border_cell, o, "popup-border-style", NULL);
        if (border_style != NULL) {
                style_set(&sytmp, &grid_default_cell);
                if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
                        pd->border_cell.fg = sytmp.gc.fg;
                        pd->border_cell.bg = sytmp.gc.bg;
                }
        }
        pd->border_cell.attr = 0;

        screen_init(&pd->s, jx, jy, 0);
        screen_set_default_cursor(&pd->s, global_w_options);
        colour_palette_init(&pd->palette);
        colour_palette_from_option(&pd->palette, global_w_options);

        memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
        style_apply(&pd->defaults, o, "popup-style", NULL);
        if (style != NULL) {
                style_set(&sytmp, &grid_default_cell);
                if (style_parse(&sytmp, &pd->defaults, style) == 0) {
                        pd->defaults.fg = sytmp.gc.fg;
                        pd->defaults.bg = sytmp.gc.bg;
                }
        }
        pd->defaults.attr = 0;

        pd->px = px;
        pd->py = py;
        pd->sx = sx;
        pd->sy = sy;

        pd->ppx = px;
        pd->ppy = py;
        pd->psx = sx;
        pd->psy = sy;

        pd->job = job_run(shellcmd, argc, argv, env, s, cwd,
            popup_job_update_cb, popup_job_complete_cb, NULL, pd,
            JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy);
        if (pd->job == NULL) {
                popup_free(pd);
                return (-1);
        }
        pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette, c);

        server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
            popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
        return (0);
}