xref: /openbsd-src/usr.bin/vi/common/msg.c (revision d9a51c353c88dac7b4a389c112b4cfe97b8e3a46)
1 /*	$OpenBSD: msg.c,v 1.28 2022/12/26 19:16:03 jmc Exp $	*/
2 
3 /*-
4  * Copyright (c) 1991, 1993, 1994
5  *	The Regents of the University of California.  All rights reserved.
6  * Copyright (c) 1991, 1993, 1994, 1995, 1996
7  *	Keith Bostic.  All rights reserved.
8  *
9  * See the LICENSE file for redistribution information.
10  */
11 
12 #include "config.h"
13 
14 #include <sys/queue.h>
15 #include <sys/stat.h>
16 #include <sys/time.h>
17 
18 #include <bitstring.h>
19 #include <ctype.h>
20 #include <errno.h>
21 #include <fcntl.h>
22 #include <limits.h>
23 #include <stdarg.h>
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <unistd.h>
28 
29 #include "common.h"
30 #include "../vi/vi.h"
31 
32 /*
33  * msgq --
34  *	Display a message.
35  *
36  * PUBLIC: void msgq(SCR *, mtype_t, const char *, ...);
37  */
38 void
msgq(SCR * sp,mtype_t mt,const char * fmt,...)39 msgq(SCR *sp, mtype_t mt, const char *fmt, ...)
40 {
41 	static int reenter;		/* STATIC: Re-entrancy check. */
42 	GS *gp;
43 	size_t blen, len, mlen, nlen;
44 	const char *p;
45 	char *bp, *mp;
46         va_list ap;
47 
48 	/*
49 	 * !!!
50 	 * It's possible to enter msg when there's no screen to hold the
51 	 * message.  If sp is NULL, ignore the special cases and put the
52 	 * message out to stderr.
53 	 */
54 	if (sp == NULL) {
55 		gp = NULL;
56 		if (mt == M_BERR)
57 			mt = M_ERR;
58 		else if (mt == M_VINFO)
59 			mt = M_INFO;
60 	} else {
61 		gp = sp->gp;
62 		switch (mt) {
63 		case M_BERR:
64 			if (F_ISSET(sp, SC_VI) && !O_ISSET(sp, O_VERBOSE)) {
65 				F_SET(gp, G_BELLSCHED);
66 				return;
67 			}
68 			mt = M_ERR;
69 			break;
70 		case M_VINFO:
71 			if (!O_ISSET(sp, O_VERBOSE))
72 				return;
73 			mt = M_INFO;
74 			/* FALLTHROUGH */
75 		case M_INFO:
76 			if (F_ISSET(sp, SC_EX_SILENT))
77 				return;
78 			break;
79 		case M_ERR:
80 		case M_SYSERR:
81 			break;
82 		default:
83 			abort();
84 		}
85 	}
86 
87 	/*
88 	 * It's possible to reenter msg when it allocates space.  We're
89 	 * probably dead anyway, but there's no reason to drop core.
90 	 *
91 	 * XXX
92 	 * Yes, there's a race, but it should only be two instructions.
93 	 */
94 	if (reenter++)
95 		return;
96 
97 	/* Get space for the message. */
98 	nlen = 1024;
99 	if (0) {
100 retry:		FREE_SPACE(sp, bp, blen);
101 		nlen *= 2;
102 	}
103 	bp = NULL;
104 	blen = 0;
105 	GET_SPACE_GOTO(sp, bp, blen, nlen);
106 
107 	/*
108 	 * Error prefix.
109 	 *
110 	 * mp:	 pointer to the current next character to be written
111 	 * mlen: length of the already written characters
112 	 * blen: total length of the buffer
113 	 */
114 #define	REM	(blen - mlen)
115 	mp = bp;
116 	mlen = 0;
117 	if (mt == M_SYSERR) {
118 		p = "Error: ";
119 		len = strlen(p);
120 		if (REM < len)
121 			goto retry;
122 		memcpy(mp, p, len);
123 		mp += len;
124 		mlen += len;
125 	}
126 
127 	/*
128 	 * If we're running an ex command that the user didn't enter, display
129 	 * the file name and line number prefix.
130 	 */
131 	if ((mt == M_ERR || mt == M_SYSERR) &&
132 	    sp != NULL && gp != NULL && gp->if_name != NULL) {
133 		for (p = gp->if_name; *p != '\0'; ++p) {
134 			len = snprintf(mp, REM, "%s", KEY_NAME(sp, *p));
135 			mp += len;
136 			if ((mlen += len) > blen)
137 				goto retry;
138 		}
139 		len = snprintf(mp, REM, ", %d: ", gp->if_lno);
140 		mp += len;
141 		if ((mlen += len) > blen)
142 			goto retry;
143 	}
144 
145 	/* If nothing to format, we're done. */
146 	if (fmt == NULL) {
147 		len = 0;
148 		goto nofmt;
149 	}
150 
151 	/* Format the arguments into the string. */
152         va_start(ap, fmt);
153 	len = vsnprintf(mp, REM, fmt, ap);
154 	va_end(ap);
155 	if (len >= nlen)
156 		goto retry;
157 
158 nofmt:	mp += len;
159 	if ((mlen += len) > blen)
160 		goto retry;
161 	if (mt == M_SYSERR) {
162 		len = snprintf(mp, REM, ": %s", strerror(errno));
163 		mp += len;
164 		if ((mlen += len) > blen)
165 			goto retry;
166 		mt = M_ERR;
167 	}
168 
169 	/* Add trailing newline. */
170 	if ((mlen += 1) > blen)
171 		goto retry;
172 	*mp = '\n';
173 
174 	if (sp != NULL)
175 		(void)ex_fflush(sp);
176 	if (gp != NULL)
177 		gp->scr_msg(sp, mt, bp, mlen);
178 	else
179 		(void)fprintf(stderr, "%.*s", (int)mlen, bp);
180 
181 	/* Cleanup. */
182 	FREE_SPACE(sp, bp, blen);
183 alloc_err:
184 	reenter = 0;
185 }
186 
187 /*
188  * msgq_str --
189  *	Display a message with an embedded string.
190  *
191  * PUBLIC: void msgq_str(SCR *, mtype_t, char *, char *);
192  */
193 void
msgq_str(SCR * sp,mtype_t mtype,char * str,char * fmt)194 msgq_str(SCR *sp, mtype_t mtype, char *str, char *fmt)
195 {
196 	int nf, sv_errno;
197 	char *p;
198 
199 	if (str == NULL) {
200 		msgq(sp, mtype, fmt);
201 		return;
202 	}
203 
204 	sv_errno = errno;
205 	p = msg_print(sp, str, &nf);
206 	errno = sv_errno;
207 	msgq(sp, mtype, fmt, p);
208 	if (nf)
209 		FREE_SPACE(sp, p, 0);
210 }
211 
212 /*
213  * mod_rpt --
214  *	Report on the lines that changed.
215  *
216  * !!!
217  * Historic vi documentation (USD:15-8) claimed that "The editor will also
218  * always tell you when a change you make affects text which you cannot see."
219  * This wasn't true -- edit a large file and do "100d|1".  We don't implement
220  * this semantic since it requires tracking each line that changes during a
221  * command instead of just keeping count.
222  *
223  * Line counts weren't right in historic vi, either.  For example, given the
224  * file:
225  *	abc
226  *	def
227  * the command 2d}, from the 'b' would report that two lines were deleted,
228  * not one.
229  *
230  * PUBLIC: void mod_rpt(SCR *);
231  */
232 void
mod_rpt(SCR * sp)233 mod_rpt(SCR *sp)
234 {
235 	static char * const action[] = {
236 		"added",
237 		"changed",
238 		"deleted",
239 		"joined",
240 		"moved",
241 		"shifted",
242 		"yanked",
243 	};
244 	static char * const lines[] = {
245 		"line",
246 		"lines",
247 	};
248 	recno_t total;
249 	u_long rptval;
250 	int first, cnt;
251 	size_t blen, len, tlen;
252 	const char *t;
253 	char * const *ap;
254 	char *bp, *p;
255 
256 	/* Change reports are turned off in batch mode. */
257 	if (F_ISSET(sp, SC_EX_SILENT))
258 		return;
259 
260 	/* Reset changing line number. */
261 	sp->rptlchange = OOBLNO;
262 
263 	/*
264 	 * Don't build a message if not enough changed.
265 	 *
266 	 * !!!
267 	 * And now, a vi clone test.  Historically, vi reported if the number
268 	 * of changed lines was > than the value, not >=, unless it was a yank
269 	 * command, which used >=.  No lie.  Furthermore, an action was never
270 	 * reported for a single line action.  This is consistent for actions
271 	 * other than yank, but yank didn't report single line actions even if
272 	 * the report edit option was set to 1.  In addition, setting report to
273 	 * 0 in the 4BSD historic vi was equivalent to setting it to 1, for an
274 	 * unknown reason (this bug was fixed in System III/V at some point).
275 	 * I got complaints, so nvi conforms to System III/V historic practice
276 	 * except that we report a yank of 1 line if report is set to 1.
277 	 */
278 #define	ARSIZE(a)	sizeof(a) / sizeof (*a)
279 #define	MAXNUM		25
280 	rptval = O_VAL(sp, O_REPORT);
281 	for (cnt = 0, total = 0; cnt < ARSIZE(action); ++cnt)
282 		total += sp->rptlines[cnt];
283 	if (total == 0)
284 		return;
285 	if (total <= rptval && sp->rptlines[L_YANKED] < rptval) {
286 		for (cnt = 0; cnt < ARSIZE(action); ++cnt)
287 			sp->rptlines[cnt] = 0;
288 		return;
289 	}
290 
291 	/* Build and display the message. */
292 	GET_SPACE_GOTO(sp, bp, blen, sizeof(action) * MAXNUM + 1);
293 	for (p = bp, first = 1, tlen = 0,
294 	    ap = action, cnt = 0; cnt < ARSIZE(action); ++ap, ++cnt)
295 		if (sp->rptlines[cnt] != 0) {
296 			if (first)
297 				first = 0;
298 			else {
299 				*p++ = ';';
300 				*p++ = ' ';
301 				tlen += 2;
302 			}
303 			len = snprintf(p, MAXNUM, "%u ", sp->rptlines[cnt]);
304 			p += len;
305 			tlen += len;
306 			t = lines[sp->rptlines[cnt] == 1 ? 0 : 1];
307 			len = strlen(t);
308 			memcpy(p, t, len);
309 			p += len;
310 			tlen += len;
311 			*p++ = ' ';
312 			++tlen;
313 			len = strlen(*ap);
314 			memcpy(p, *ap, len);
315 			p += len;
316 			tlen += len;
317 			sp->rptlines[cnt] = 0;
318 		}
319 
320 	/* Add trailing newline. */
321 	*p = '\n';
322 	++tlen;
323 
324 	(void)ex_fflush(sp);
325 	sp->gp->scr_msg(sp, M_INFO, bp, tlen);
326 
327 	FREE_SPACE(sp, bp, blen);
328 alloc_err:
329 	return;
330 
331 #undef ARSIZE
332 #undef MAXNUM
333 }
334 
335 /*
336  * msgq_status --
337  *	Report on the file's status.
338  *
339  * PUBLIC: void msgq_status(SCR *, recno_t, u_int);
340  */
341 void
msgq_status(SCR * sp,recno_t lno,u_int flags)342 msgq_status(SCR *sp, recno_t lno, u_int flags)
343 {
344 	recno_t last;
345 	size_t blen, len;
346 	int cnt, needsep;
347 	const char *t;
348 	char **ap, *bp, *np, *p, *s, *ep;
349 
350 	/* Get sufficient memory. */
351 	len = strlen(sp->frp->name);
352 	GET_SPACE_GOTO(sp, bp, blen, len * MAX_CHARACTER_COLUMNS + 128);
353 	p = bp;
354 	ep = bp + blen;
355 
356 	/* Copy in the filename. */
357 	for (t = sp->frp->name; *t != '\0'; ++t) {
358 		len = KEY_LEN(sp, *t);
359 		memcpy(p, KEY_NAME(sp, *t), len);
360 		p += len;
361 	}
362 	np = p;
363 	*p++ = ':';
364 	*p++ = ' ';
365 
366 	/* Copy in the argument count. */
367 	if (F_ISSET(sp, SC_STATUS_CNT) && sp->argv != NULL) {
368 		for (cnt = 0, ap = sp->argv; *ap != NULL; ++ap, ++cnt);
369 		if (cnt > 1) {
370 			(void)snprintf(p, ep - p, "%d files to edit", cnt);
371 			p += strlen(p);
372 			*p++ = ':';
373 			*p++ = ' ';
374 		}
375 		F_CLR(sp, SC_STATUS_CNT);
376 	}
377 
378 	/*
379 	 * See nvi/exf.c:file_init() for a description of how and when the
380 	 * read-only bit is set.
381 	 *
382 	 * !!!
383 	 * The historic display for "name changed" was "[Not edited]".
384 	 */
385 	needsep = 0;
386 	if (F_ISSET(sp->frp, FR_NEWFILE)) {
387 		F_CLR(sp->frp, FR_NEWFILE);
388 		len = strlen("new file");
389 		memcpy(p, "new file", len);
390 		p += len;
391 		needsep = 1;
392 	} else {
393 		if (F_ISSET(sp->frp, FR_NAMECHANGE)) {
394 			len = strlen("name changed");
395 			memcpy(p, "name changed", len);
396 			p += len;
397 			needsep = 1;
398 		}
399 		if (needsep) {
400 			*p++ = ',';
401 			*p++ = ' ';
402 		}
403 		t = (F_ISSET(sp->ep, F_MODIFIED)) ? "modified" : "unmodified";
404 		len = strlen(t);
405 		memcpy(p, t, len);
406 		p += len;
407 		needsep = 1;
408 	}
409 	if (F_ISSET(sp->frp, FR_UNLOCKED)) {
410 		if (needsep) {
411 			*p++ = ',';
412 			*p++ = ' ';
413 		}
414 		len = strlen("UNLOCKED");
415 		memcpy(p, "UNLOCKED", len);
416 		p += len;
417 		needsep = 1;
418 	}
419 	if (O_ISSET(sp, O_READONLY)) {
420 		if (needsep) {
421 			*p++ = ',';
422 			*p++ = ' ';
423 		}
424 		len = strlen("readonly");
425 		memcpy(p, "readonly", len);
426 		p += len;
427 		needsep = 1;
428 	}
429 	if (needsep) {
430 		*p++ = ':';
431 		*p++ = ' ';
432 	}
433 	if (LF_ISSET(MSTAT_SHOWLAST)) {
434 		if (db_last(sp, &last))
435 			return;
436 		if (last == 0) {
437 			len = strlen("empty file");
438 			memcpy(p, "empty file", len);
439 			p += len;
440 		} else {
441 			(void)snprintf(p, ep - p, "line %lu of %lu [%lu%%]",
442 			    (unsigned long)lno, (unsigned long)last,
443 			    (unsigned long)(lno * 100) / last);
444 			p += strlen(p);
445 		}
446 	} else {
447 		(void)snprintf(p, ep - p, "line %lu", (unsigned long)lno);
448 		p += strlen(p);
449 	}
450 #ifdef DEBUG
451 	(void)snprintf(p, ep - p, " (pid %ld)", (long)getpid());
452 	p += strlen(p);
453 #endif
454 	*p++ = '\n';
455 	len = p - bp;
456 
457 	/*
458 	 * There's a nasty problem with long path names.  Tags files
459 	 * can result in long paths and vi will request a continuation key from
460 	 * the user as soon as it starts the screen.  Unfortunately, the user
461 	 * has already typed ahead, and chaos results.  If we assume that the
462 	 * characters in the filenames and informational messages only take a
463 	 * single screen column each, we can trim the filename.
464 	 *
465 	 * XXX
466 	 * Status lines get put up at fairly awkward times.  For example, when
467 	 * you do a filter read (e.g., :read ! echo foo) in the top screen of a
468 	 * split screen, we have to repaint the status lines for all the screens
469 	 * below the top screen.  We don't want users having to enter continue
470 	 * characters for those screens.  Make it really hard to screw this up.
471 	 */
472 	s = bp;
473 	if (LF_ISSET(MSTAT_TRUNCATE) && len > sp->cols) {
474 		for (; s < np && (*s != '/' || (p - s) > sp->cols - 3); ++s);
475 		if (s == np) {
476 			s = p - (sp->cols - 5);
477 			*--s = ' ';
478 		}
479 		*--s = '.';
480 		*--s = '.';
481 		*--s = '.';
482 		len = p - s;
483 	}
484 
485 	/* Flush any waiting ex messages. */
486 	(void)ex_fflush(sp);
487 
488 	sp->gp->scr_msg(sp, M_INFO, s, len);
489 
490 	FREE_SPACE(sp, bp, blen);
491 alloc_err:
492 	return;
493 }
494 
495 /*
496  * msg_cont --
497  *	Return common continuation messages.
498  *
499  * PUBLIC: const char *msg_cmsg(SCR *, cmsg_t, size_t *);
500  */
501 const char *
msg_cmsg(SCR * sp,cmsg_t which,size_t * lenp)502 msg_cmsg(SCR *sp, cmsg_t which, size_t *lenp)
503 {
504 	const char *s;
505 	switch (which) {
506 	case CMSG_CONF:
507 		s = "confirm? [ynq]";
508 		break;
509 	case CMSG_CONT:
510 		s = "Press any key to continue: ";
511 		break;
512 	case CMSG_CONT_EX:
513 		s = "Press any key to continue [: to enter more ex commands]: ";
514 		break;
515 	case CMSG_CONT_R:
516 		s = "Press Enter to continue: ";
517 		break;
518 	case CMSG_CONT_S:
519 		s = " cont?";
520 		break;
521 	case CMSG_CONT_Q:
522 		s = "Press any key to continue [q to quit]: ";
523 		break;
524 	default:
525 		abort();
526 	}
527 	*lenp = strlen(s);
528 	return s;
529 }
530 
531 /*
532  * msg_print --
533  *	Return a printable version of a string, in allocated memory.
534  *
535  * PUBLIC: char *msg_print(SCR *, const char *, int *);
536  */
537 char *
msg_print(SCR * sp,const char * s,int * needfree)538 msg_print(SCR *sp, const char *s, int *needfree)
539 {
540 	size_t blen, nlen;
541 	const char *cp;
542 	char *bp, *ep, *p, *t;
543 
544 	*needfree = 0;
545 
546 	for (cp = s; *cp != '\0'; ++cp)
547 		if (!isprint(*cp))
548 			break;
549 	if (*cp == '\0')
550 		return ((char *)s);	/* SAFE: needfree set to 0. */
551 
552 	nlen = 0;
553 	if (0) {
554 retry:		if (sp == NULL)
555 			free(bp);
556 		else
557 			FREE_SPACE(sp, bp, blen);
558 		*needfree = 0;
559 	}
560 	nlen += 256;
561 	if (sp == NULL) {
562 		if ((bp = malloc(nlen)) == NULL)
563 			goto alloc_err;
564 		blen = 0;
565 	} else
566 		GET_SPACE_GOTO(sp, bp, blen, nlen);
567 	if (0) {
568 alloc_err:	return ("");
569 	}
570 	*needfree = 1;
571 
572 	for (p = bp, ep = (bp + blen) - 1, cp = s; *cp != '\0' && p < ep; ++cp)
573 		for (t = KEY_NAME(sp, *cp); *t != '\0' && p < ep; *p++ = *t++);
574 	if (p == ep)
575 		goto retry;
576 	*p = '\0';
577 	return (bp);
578 }
579