xref: /openbsd/usr.bin/tmux/popup.c (revision 4cfece93)
1 /* $OpenBSD: popup.c,v 1.19 2020/05/16 16:35:13 nicm Exp $ */
2 
3 /*
4  * Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
15  * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
16  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/types.h>
20 #include <sys/wait.h>
21 
22 #include <paths.h>
23 #include <signal.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <unistd.h>
27 
28 #include "tmux.h"
29 
30 struct popup_data {
31 	struct client		 *c;
32 	struct cmdq_item	 *item;
33 	int			  flags;
34 
35 	char			**lines;
36 	u_int			  nlines;
37 
38 	char			 *cmd;
39 	struct cmd_find_state	  fs;
40 	struct screen		  s;
41 
42 	struct job		 *job;
43 	struct input_ctx	 *ictx;
44 	int			  status;
45 	popup_close_cb		  cb;
46 	void			 *arg;
47 
48 	u_int			  px;
49 	u_int			  py;
50 	u_int			  sx;
51 	u_int			  sy;
52 
53 	enum { OFF, MOVE, SIZE }  dragging;
54 	u_int			  dx;
55 	u_int			  dy;
56 
57 	u_int			  lx;
58 	u_int			  ly;
59 	u_int			  lb;
60 };
61 
62 struct popup_editor {
63 	char			*path;
64 	popup_finish_edit_cb	 cb;
65 	void			*arg;
66 };
67 
68 static void
69 popup_redraw_cb(const struct tty_ctx *ttyctx)
70 {
71 	struct popup_data	*pd = ttyctx->arg;
72 
73 	pd->c->flags |= CLIENT_REDRAWOVERLAY;
74 }
75 
76 static int
77 popup_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
78 {
79 	struct popup_data	*pd = ttyctx->arg;
80 
81 	if (c != pd->c)
82 		return (0);
83 	if (pd->c->flags & CLIENT_REDRAWOVERLAY)
84 		return (0);
85 
86 	ttyctx->bigger = 0;
87 	ttyctx->wox = 0;
88 	ttyctx->woy = 0;
89 	ttyctx->wsx = c->tty.sx;
90 	ttyctx->wsy = c->tty.sy;
91 
92 	ttyctx->xoff = ttyctx->rxoff = pd->px + 1;
93 	ttyctx->yoff = ttyctx->ryoff = pd->py + 1;
94 
95 	return (1);
96 }
97 
98 static void
99 popup_init_ctx_cb(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
100 {
101 	struct popup_data	*pd = ctx->arg;
102 
103 	ttyctx->redraw_cb = popup_redraw_cb;
104 	ttyctx->set_client_cb = popup_set_client_cb;
105 	ttyctx->arg = pd;
106 }
107 
108 static void
109 popup_write_screen(struct client *c, struct popup_data *pd)
110 {
111 	struct cmdq_item	*item = pd->item;
112 	struct screen_write_ctx	 ctx;
113 	char			*copy, *next, *loop, *tmp;
114 	struct format_tree	*ft;
115 	u_int			 i, y;
116 
117 	ft = format_create(c, item, FORMAT_NONE, 0);
118 	if (cmd_find_valid_state(&pd->fs))
119 		format_defaults(ft, c, pd->fs.s, pd->fs.wl, pd->fs.wp);
120 	else
121 		format_defaults(ft, c, NULL, NULL, NULL);
122 
123 	screen_write_start(&ctx, &pd->s);
124 	screen_write_clearscreen(&ctx, 8);
125 
126 	y = 0;
127 	for (i = 0; i < pd->nlines; i++) {
128 		if (y == pd->sy - 2)
129 			break;
130 		copy = next = xstrdup(pd->lines[i]);
131 		while ((loop = strsep(&next, "\n")) != NULL) {
132 			if (y == pd->sy - 2)
133 				break;
134 			tmp = format_expand(ft, loop);
135 			screen_write_cursormove(&ctx, 0, y, 0);
136 			format_draw(&ctx, &grid_default_cell, pd->sx - 2, tmp,
137 			    NULL);
138 			free(tmp);
139 			y++;
140 		}
141 		free(copy);
142 	}
143 
144 	format_free(ft);
145 	screen_write_cursormove(&ctx, 0, y, 0);
146 	screen_write_stop(&ctx);
147 }
148 
149 static struct screen *
150 popup_mode_cb(struct client *c, u_int *cx, u_int *cy)
151 {
152 	struct popup_data	*pd = c->overlay_data;
153 
154 	if (pd->ictx == NULL)
155 		return (0);
156 	*cx = pd->px + 1 + pd->s.cx;
157 	*cy = pd->py + 1 + pd->s.cy;
158 	return (&pd->s);
159 }
160 
161 static int
162 popup_check_cb(struct client *c, u_int px, u_int py)
163 {
164 	struct popup_data	*pd = c->overlay_data;
165 
166 	if (px < pd->px || px > pd->px + pd->sx - 1)
167 		return (1);
168 	if (py < pd->py || py > pd->py + pd->sy - 1)
169 		return (1);
170 	return (0);
171 }
172 
173 static void
174 popup_draw_cb(struct client *c, __unused struct screen_redraw_ctx *ctx0)
175 {
176 	struct popup_data	*pd = c->overlay_data;
177 	struct tty		*tty = &c->tty;
178 	struct screen		 s;
179 	struct screen_write_ctx	 ctx;
180 	u_int			 i, px = pd->px, py = pd->py;
181 
182 	screen_init(&s, pd->sx, pd->sy, 0);
183 	screen_write_start(&ctx, &s);
184 	screen_write_clearscreen(&ctx, 8);
185 	screen_write_box(&ctx, pd->sx, pd->sy);
186 	screen_write_cursormove(&ctx, 1, 1, 0);
187 	screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx - 2, pd->sy - 2);
188 	screen_write_stop(&ctx);
189 
190 	c->overlay_check = NULL;
191 	for (i = 0; i < pd->sy; i++){
192 		tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i,
193 		    &grid_default_cell, NULL);
194 	}
195 	c->overlay_check = popup_check_cb;
196 }
197 
198 static void
199 popup_free_cb(struct client *c)
200 {
201 	struct popup_data	*pd = c->overlay_data;
202 	struct cmdq_item	*item = pd->item;
203 	u_int			 i;
204 
205 	if (pd->cb != NULL)
206 		pd->cb(pd->status, pd->arg);
207 
208 	if (item != NULL) {
209 		if (pd->ictx != NULL &&
210 		    cmdq_get_client(item) != NULL &&
211 		    cmdq_get_client(item)->session == NULL)
212 			cmdq_get_client(item)->retval = pd->status;
213 		cmdq_continue(item);
214 	}
215 	server_client_unref(pd->c);
216 
217 	if (pd->job != NULL)
218 		job_free(pd->job);
219 	if (pd->ictx != NULL)
220 		input_free(pd->ictx);
221 
222 	for (i = 0; i < pd->nlines; i++)
223 		free(pd->lines[i]);
224 	free(pd->lines);
225 
226 	screen_free(&pd->s);
227 	free(pd->cmd);
228 	free(pd);
229 }
230 
231 static void
232 popup_handle_drag(struct client *c, struct popup_data *pd,
233     struct mouse_event *m)
234 {
235 	u_int	px, py;
236 
237 	if (!MOUSE_DRAG(m->b))
238 		pd->dragging = OFF;
239 	else if (pd->dragging == MOVE) {
240 		if (m->x < pd->dx)
241 			px = 0;
242 		else if (m->x - pd->dx + pd->sx > c->tty.sx)
243 			px = c->tty.sx - pd->sx;
244 		else
245 			px = m->x - pd->dx;
246 		if (m->y < pd->dy)
247 			py = 0;
248 		else if (m->y - pd->dy + pd->sy > c->tty.sy)
249 			py = c->tty.sy - pd->sy;
250 		else
251 			py = m->y - pd->dy;
252 		pd->px = px;
253 		pd->py = py;
254 		pd->dx = m->x - pd->px;
255 		pd->dy = m->y - pd->py;
256 		server_redraw_client(c);
257 	} else if (pd->dragging == SIZE) {
258 		if (m->x < pd->px + 3)
259 			return;
260 		if (m->y < pd->py + 3)
261 			return;
262 		pd->sx = m->x - pd->px;
263 		pd->sy = m->y - pd->py;
264 
265 		screen_resize(&pd->s, pd->sx, pd->sy, 0);
266 		if (pd->ictx == NULL)
267 			popup_write_screen(c, pd);
268 		else if (pd->job != NULL)
269 			job_resize(pd->job, pd->sx - 2, pd->sy - 2);
270 		server_redraw_client(c);
271 	}
272 }
273 
274 static int
275 popup_key_cb(struct client *c, struct key_event *event)
276 {
277 	struct popup_data	*pd = c->overlay_data;
278 	struct mouse_event	*m = &event->m;
279 	struct cmd_find_state	*fs = &pd->fs;
280 	struct format_tree	*ft;
281 	const char		*cmd, *buf;
282 	size_t			 len;
283 	struct cmdq_state	*state;
284 	enum cmd_parse_status	 status;
285 	char			*error;
286 
287 	if (KEYC_IS_MOUSE(event->key)) {
288 		if (pd->dragging != OFF) {
289 			popup_handle_drag(c, pd, m);
290 			goto out;
291 		}
292 		if (m->x < pd->px ||
293 		    m->x > pd->px + pd->sx - 1 ||
294 		    m->y < pd->py ||
295 		    m->y > pd->py + pd->sy - 1) {
296 			if (MOUSE_BUTTONS (m->b) == 1)
297 				return (1);
298 			return (0);
299 		}
300 		if ((m->b & MOUSE_MASK_META) ||
301 		    m->x == pd->px ||
302 		    m->x == pd->px + pd->sx - 1 ||
303 		    m->y == pd->py ||
304 		    m->y == pd->py + pd->sy - 1) {
305 			if (!MOUSE_DRAG(m->b))
306 				goto out;
307 			if (MOUSE_BUTTONS(m->lb) == 0)
308 				pd->dragging = MOVE;
309 			else if (MOUSE_BUTTONS(m->lb) == 2)
310 				pd->dragging = SIZE;
311 			pd->dx = m->lx - pd->px;
312 			pd->dy = m->ly - pd->py;
313 			goto out;
314 		}
315 	}
316 
317 	if (pd->ictx != NULL && (pd->flags & POPUP_WRITEKEYS)) {
318 		if (((pd->flags & (POPUP_CLOSEEXIT|POPUP_CLOSEEXITZERO)) == 0 ||
319 		    pd->job == NULL) &&
320 		    (event->key == '\033' || event->key == '\003'))
321 			return (1);
322 		if (pd->job == NULL)
323 			return (0);
324 		if (KEYC_IS_MOUSE(event->key)) {
325 			/* Must be inside, checked already. */
326 			if (!input_key_get_mouse(&pd->s, m, m->x - pd->px,
327 			    m->y - pd->py, &buf, &len))
328 				return (0);
329 			bufferevent_write(job_get_event(pd->job), buf, len);
330 			return (0);
331 		}
332 		input_key(&pd->s, job_get_event(pd->job), event->key);
333 		return (0);
334 	}
335 
336 	if (pd->cmd == NULL)
337 		return (1);
338 
339 	ft = format_create(NULL, pd->item, FORMAT_NONE, 0);
340 	if (cmd_find_valid_state(fs))
341 		format_defaults(ft, c, fs->s, fs->wl, fs->wp);
342 	else
343 		format_defaults(ft, c, NULL, NULL, NULL);
344 	format_add(ft, "popup_key", "%s", key_string_lookup_key(event->key, 0));
345 	if (KEYC_IS_MOUSE(event->key)) {
346 		format_add(ft, "popup_mouse", "1");
347 		format_add(ft, "popup_mouse_x", "%u", m->x - pd->px);
348 		format_add(ft, "popup_mouse_y", "%u", m->y - pd->py);
349 	}
350 	cmd = format_expand(ft, pd->cmd);
351 	format_free(ft);
352 
353 	if (pd->item != NULL)
354 		event = cmdq_get_event(pd->item);
355 	else
356 		event = NULL;
357 	state = cmdq_new_state(&pd->fs, event, 0);
358 
359 	status = cmd_parse_and_append(cmd, NULL, c, state, &error);
360 	if (status == CMD_PARSE_ERROR) {
361 		cmdq_append(c, cmdq_get_error(error));
362 		free(error);
363 	}
364 	cmdq_free_state(state);
365 
366 	return (1);
367 
368 out:
369 	pd->lx = m->x;
370 	pd->ly = m->y;
371 	pd->lb = m->b;
372 	return (0);
373 }
374 
375 static void
376 popup_job_update_cb(struct job *job)
377 {
378 	struct popup_data	*pd = job_get_data(job);
379 	struct evbuffer		*evb = job_get_event(job)->input;
380 	struct client		*c = pd->c;
381 	struct screen		*s = &pd->s;
382 	void			*data = EVBUFFER_DATA(evb);
383 	size_t			 size = EVBUFFER_LENGTH(evb);
384 
385 	if (size == 0)
386 		return;
387 
388 	c->overlay_check = NULL;
389 	c->tty.flags &= ~TTY_FREEZE;
390 
391 	input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size);
392 
393 	c->tty.flags |= TTY_FREEZE;
394 	c->overlay_check = popup_check_cb;
395 
396 	evbuffer_drain(evb, size);
397 }
398 
399 static void
400 popup_job_complete_cb(struct job *job)
401 {
402 	struct popup_data	*pd = job_get_data(job);
403 	int			 status;
404 
405 	status = job_get_status(pd->job);
406 	if (WIFEXITED(status))
407 		pd->status = WEXITSTATUS(status);
408 	else if (WIFSIGNALED(status))
409 		pd->status = WTERMSIG(status);
410 	else
411 		pd->status = 0;
412 	pd->job = NULL;
413 
414 	if ((pd->flags & POPUP_CLOSEEXIT) ||
415 	    ((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
416 		server_client_clear_overlay(pd->c);
417 }
418 
419 u_int
420 popup_height(u_int nlines, const char **lines)
421 {
422 	char	*copy, *next, *loop;
423 	u_int	 i, height = 0;
424 
425 	for (i = 0; i < nlines; i++) {
426 		copy = next = xstrdup(lines[i]);
427 		while ((loop = strsep(&next, "\n")) != NULL)
428 			height++;
429 		free(copy);
430 	}
431 
432 	return (height);
433 }
434 
435 u_int
436 popup_width(struct cmdq_item *item, u_int nlines, const char **lines,
437     struct client *c, struct cmd_find_state *fs)
438 {
439 	char			*copy, *next, *loop, *tmp;
440 	struct format_tree	*ft;
441 	u_int			 i, width = 0, tmpwidth;
442 
443 	ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
444 	if (fs != NULL && cmd_find_valid_state(fs))
445 		format_defaults(ft, c, fs->s, fs->wl, fs->wp);
446 	else
447 		format_defaults(ft, c, NULL, NULL, NULL);
448 
449 	for (i = 0; i < nlines; i++) {
450 		copy = next = xstrdup(lines[i]);
451 		while ((loop = strsep(&next, "\n")) != NULL) {
452 			tmp = format_expand(ft, loop);
453 			tmpwidth = format_width(tmp);
454 			if (tmpwidth > width)
455 				width = tmpwidth;
456 			free(tmp);
457 		}
458 		free(copy);
459 	}
460 
461 	format_free(ft);
462 	return (width);
463 }
464 
465 int
466 popup_display(int flags, struct cmdq_item *item, u_int px, u_int py, u_int sx,
467     u_int sy, u_int nlines, const char **lines, const char *shellcmd,
468     const char *cmd, const char *cwd, struct client *c,
469     struct cmd_find_state *fs, popup_close_cb cb, void *arg)
470 {
471 	struct popup_data	*pd;
472 	u_int			 i;
473 	struct session		*s;
474 	int			 jobflags;
475 
476 	if (sx < 3 || sy < 3)
477 		return (-1);
478 	if (c->tty.sx < sx || c->tty.sy < sy)
479 		return (-1);
480 
481 	pd = xcalloc(1, sizeof *pd);
482 	pd->item = item;
483 	pd->flags = flags;
484 
485 	pd->c = c;
486 	pd->c->references++;
487 
488 	pd->cb = cb;
489 	pd->arg = arg;
490 	pd->status = 128 + SIGHUP;
491 
492 	if (fs != NULL)
493 		cmd_find_copy_state(&pd->fs, fs);
494 	screen_init(&pd->s, sx - 2, sy - 2, 0);
495 
496 	if (cmd != NULL)
497 		pd->cmd = xstrdup(cmd);
498 
499 	pd->px = px;
500 	pd->py = py;
501 	pd->sx = sx;
502 	pd->sy = sy;
503 
504 	pd->nlines = nlines;
505 	if (pd->nlines != 0)
506 		pd->lines = xreallocarray(NULL, pd->nlines, sizeof *pd->lines);
507 
508 	for (i = 0; i < pd->nlines; i++)
509 		pd->lines[i] = xstrdup(lines[i]);
510 	popup_write_screen(c, pd);
511 
512 	if (shellcmd != NULL) {
513 		if (fs != NULL)
514 			s = fs->s;
515 		else
516 			s = NULL;
517 		jobflags = JOB_NOWAIT|JOB_PTY;
518 		if (flags & POPUP_WRITEKEYS)
519 		    jobflags |= JOB_KEEPWRITE;
520 		pd->job = job_run(shellcmd, s, cwd, popup_job_update_cb,
521 		    popup_job_complete_cb, NULL, pd, jobflags, pd->sx - 2,
522 		    pd->sy - 2);
523 		pd->ictx = input_init(NULL, job_get_event(pd->job));
524 	}
525 
526 	server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
527 	    popup_draw_cb, popup_key_cb, popup_free_cb, pd);
528 	return (0);
529 }
530 
531 static void
532 popup_editor_free(struct popup_editor *pe)
533 {
534 	unlink(pe->path);
535 	free(pe->path);
536 	free(pe);
537 }
538 
539 static void
540 popup_editor_close_cb(int status, void *arg)
541 {
542 	struct popup_editor	*pe = arg;
543 	FILE			*f;
544 	char			*buf = NULL;
545 	off_t			 len = 0;
546 
547 	if (status != 0) {
548 		pe->cb(NULL, 0, pe->arg);
549 		popup_editor_free(pe);
550 		return;
551 	}
552 
553 	f = fopen(pe->path, "r");
554 	if (f != NULL) {
555 		fseeko(f, 0, SEEK_END);
556 		len = ftello(f);
557 		fseeko(f, 0, SEEK_SET);
558 
559 		if (len == 0 ||
560 		    (uintmax_t)len > (uintmax_t)SIZE_MAX ||
561 		    (buf = malloc(len)) == NULL ||
562 		    fread(buf, len, 1, f) != 1) {
563 			free(buf);
564 			buf = NULL;
565 			len = 0;
566 		}
567 		fclose(f);
568 	}
569 	pe->cb(buf, len, pe->arg); /* callback now owns buffer */
570 	popup_editor_free(pe);
571 }
572 
573 int
574 popup_editor(struct client *c, const char *buf, size_t len,
575     popup_finish_edit_cb cb, void *arg)
576 {
577 	struct popup_editor	*pe;
578 	int			 fd;
579 	FILE			*f;
580 	char			*cmd;
581 	char			 path[] = _PATH_TMP "tmux.XXXXXXXX";
582 	const char		*editor;
583 	u_int			 px, py, sx, sy;
584 
585 	editor = options_get_string(global_options, "editor");
586 	if (*editor == '\0')
587 		return (-1);
588 
589 	fd = mkstemp(path);
590 	if (fd == -1)
591 		return (-1);
592 	f = fdopen(fd, "w");
593 	if (fwrite(buf, len, 1, f) != 1) {
594 		fclose(f);
595 		return (-1);
596 	}
597 	fclose(f);
598 
599 	pe = xcalloc(1, sizeof *pe);
600 	pe->path = xstrdup(path);
601 	pe->cb = cb;
602 	pe->arg = arg;
603 
604 	sx = c->tty.sx * 9 / 10;
605 	sy = c->tty.sy * 9 / 10;
606 	px = (c->tty.sx / 2) - (sx / 2);
607 	py = (c->tty.sy / 2) - (sy / 2);
608 
609 	xasprintf(&cmd, "%s %s", editor, path);
610 	if (popup_display(POPUP_WRITEKEYS|POPUP_CLOSEEXIT, NULL, px, py, sx, sy,
611 	    0, NULL, cmd, NULL, _PATH_TMP, c, NULL, popup_editor_close_cb,
612 	    pe) != 0) {
613 		popup_editor_free(pe);
614 		free(cmd);
615 		return (-1);
616 	}
617 	free(cmd);
618 	return (0);
619 }
620