xref: /openbsd/usr.bin/less/prompt.c (revision 021cd5d5)
1 /*
2  * Copyright (C) 1984-2012  Mark Nudelman
3  * Modified for use with illumos by Garrett D'Amore.
4  * Copyright 2014 Garrett D'Amore <garrett@damore.org>
5  *
6  * You may distribute under the terms of either the GNU General Public
7  * License or the Less License, as specified in the README file.
8  *
9  * For more information, see the README file.
10  */
11 
12 /*
13  * Prompting and other messages.
14  * There are three flavors of prompts, SHORT, MEDIUM and LONG,
15  * selected by the -m/-M options.
16  * There is also the "equals message", printed by the = command.
17  * A prompt is a message composed of various pieces, such as the
18  * name of the file being viewed, the percentage into the file, etc.
19  */
20 
21 #include "less.h"
22 #include "position.h"
23 
24 extern int pr_type;
25 extern int new_file;
26 extern int sc_width;
27 extern int so_s_width, so_e_width;
28 extern int linenums;
29 extern int hshift;
30 extern int sc_height;
31 extern int jump_sline;
32 extern int less_is_more;
33 extern IFILE curr_ifile;
34 extern char *editor;
35 extern char *editproto;
36 
37 /*
38  * Prototypes for the three flavors of prompts.
39  * These strings are expanded by pr_expand().
40  */
41 static const char s_proto[] =
42 	"?n?f%f .?m(%T %i of %m) ..?e(END) ?x- Next\\: %x..%t";
43 static const char m_proto[] =
44 	"?n?f%f .?m(%T %i of %m) ..?e(END) "
45 	"?x- Next\\: %x.:?pB%pB\\%:byte %bB?s/%s...%t";
46 static const char M_proto[] =
47 	"?f%f .?n?m(%T %i of %m) ..?"
48 	"ltlines %lt-%lb?L/%L. :byte %bB?s/%s. .?e(END)"
49 	" ?x- Next\\: %x.:?pB%pB\\%..%t";
50 static const char e_proto[] =
51 	"?f%f .?m(%T %i of %m) .?ltlines "
52 	"%lt-%lb?L/%L. .byte %bB?s/%s. ?e(END) :?pB%pB\\%..%t";
53 static const char h_proto[] =
54 	"HELP -- ?eEND -- Press g to see it again:"
55 	"Press RETURN for more., or q when done";
56 static const char w_proto[] =
57 	"Waiting for data";
58 static const char more_proto[] =
59 	"%f (?eEND ?x- Next\\: %x.:?pB%pB\\%:byte %bB?s/%s...%t)";
60 static const char more_M_proto[] =
61 	"%f (?eEND ?x- Next\\: %x.:?pB%pB\\%:byte %bB?s/%s...%t)"
62 	"[Press space to continue, q to quit, h for help]";
63 
64 char *prproto[3];
65 char const *eqproto = e_proto;
66 char const *hproto = h_proto;
67 char const *wproto = w_proto;
68 
69 static char message[PROMPT_SIZE];
70 static char *mp;
71 
72 /*
73  * Initialize the prompt prototype strings.
74  */
75 void
init_prompt(void)76 init_prompt(void)
77 {
78 	prproto[0] = estrdup(s_proto);
79 	prproto[1] = estrdup(less_is_more ? more_proto : m_proto);
80 	prproto[2] = estrdup(less_is_more ? more_M_proto : M_proto);
81 	eqproto = estrdup(e_proto);
82 	hproto = estrdup(h_proto);
83 	wproto = estrdup(w_proto);
84 }
85 
86 /*
87  * Append a string to the end of the message.
88  */
89 static void
ap_str(char * s)90 ap_str(char *s)
91 {
92 	int len;
93 
94 	len = strlen(s);
95 	if (mp + len >= message + PROMPT_SIZE)
96 		len = message + PROMPT_SIZE - mp - 1;
97 	(void) strncpy(mp, s, len);
98 	mp += len;
99 	*mp = '\0';
100 }
101 
102 /*
103  * Append a character to the end of the message.
104  */
105 static void
ap_char(char c)106 ap_char(char c)
107 {
108 	char buf[2];
109 
110 	buf[0] = c;
111 	buf[1] = '\0';
112 	ap_str(buf);
113 }
114 
115 /*
116  * Append a off_t (as a decimal integer) to the end of the message.
117  */
118 static void
ap_pos(off_t pos)119 ap_pos(off_t pos)
120 {
121 	char buf[23];
122 
123 	postoa(pos, buf, sizeof(buf));
124 	ap_str(buf);
125 }
126 
127 /*
128  * Append an integer to the end of the message.
129  */
130 static void
ap_int(int num)131 ap_int(int num)
132 {
133 	char buf[13];
134 
135 	inttoa(num, buf, sizeof buf);
136 	ap_str(buf);
137 }
138 
139 /*
140  * Append a question mark to the end of the message.
141  */
142 static void
ap_quest(void)143 ap_quest(void)
144 {
145 	ap_str("?");
146 }
147 
148 /*
149  * Return the "current" byte offset in the file.
150  */
151 static off_t
curr_byte(int where)152 curr_byte(int where)
153 {
154 	off_t pos;
155 
156 	pos = position(where);
157 	while (pos == -1 && where >= 0 && where < sc_height-1)
158 		pos = position(++where);
159 	if (pos == -1)
160 		pos = ch_length();
161 	return (pos);
162 }
163 
164 /*
165  * Return the value of a prototype conditional.
166  * A prototype string may include conditionals which consist of a
167  * question mark followed by a single letter.
168  * Here we decode that letter and return the appropriate boolean value.
169  */
170 static int
cond(char c,int where)171 cond(char c, int where)
172 {
173 	off_t len;
174 
175 	switch (c) {
176 	case 'a':	/* Anything in the message yet? */
177 		return (*message != '\0');
178 	case 'b':	/* Current byte offset known? */
179 		return (curr_byte(where) != -1);
180 	case 'c':
181 		return (hshift != 0);
182 	case 'e':	/* At end of file? */
183 		return (eof_displayed());
184 	case 'f':	/* Filename known? */
185 		return (strcmp(get_filename(curr_ifile), "-") != 0);
186 	case 'l':	/* Line number known? */
187 	case 'd':	/* Same as l */
188 		return (linenums);
189 	case 'L':	/* Final line number known? */
190 	case 'D':	/* Final page number known? */
191 		return (linenums && ch_length() != -1);
192 	case 'm':	/* More than one file? */
193 		return (ntags() ? (ntags() > 1) : (nifile() > 1));
194 	case 'n':	/* First prompt in a new file? */
195 		return (ntags() ? 1 : new_file);
196 	case 'p':	/* Percent into file (bytes) known? */
197 		return (curr_byte(where) != -1 && ch_length() > 0);
198 	case 'P':	/* Percent into file (lines) known? */
199 		return (currline(where) != 0 &&
200 		    (len = ch_length()) > 0 && find_linenum(len) != 0);
201 	case 's':	/* Size of file known? */
202 	case 'B':
203 		return (ch_length() != -1);
204 	case 'x':	/* Is there a "next" file? */
205 		if (ntags())
206 			return (0);
207 		return (next_ifile(curr_ifile) != NULL);
208 	}
209 	return (0);
210 }
211 
212 /*
213  * Decode a "percent" prototype character.
214  * A prototype string may include various "percent" escapes;
215  * that is, a percent sign followed by a single letter.
216  * Here we decode that letter and take the appropriate action,
217  * usually by appending something to the message being built.
218  */
219 static void
protochar(int c,int where)220 protochar(int c, int where)
221 {
222 	off_t pos;
223 	off_t len;
224 	int n;
225 	off_t linenum;
226 	off_t last_linenum;
227 	IFILE h;
228 
229 #undef	PAGE_NUM
230 #define	PAGE_NUM(linenum)  ((((linenum) - 1) / (sc_height - 1)) + 1)
231 
232 	switch (c) {
233 	case 'b':	/* Current byte offset */
234 		pos = curr_byte(where);
235 		if (pos != -1)
236 			ap_pos(pos);
237 		else
238 			ap_quest();
239 		break;
240 	case 'c':
241 		ap_int(hshift);
242 		break;
243 	case 'd':	/* Current page number */
244 		linenum = currline(where);
245 		if (linenum > 0 && sc_height > 1)
246 			ap_pos(PAGE_NUM(linenum));
247 		else
248 			ap_quest();
249 		break;
250 	case 'D':	/* Final page number */
251 		/* Find the page number of the last byte in the file (len-1). */
252 		len = ch_length();
253 		if (len == -1) {
254 			ap_quest();
255 		} else if (len == 0) {
256 			/* An empty file has no pages. */
257 			ap_pos(0);
258 		} else {
259 			linenum = find_linenum(len - 1);
260 			if (linenum <= 0)
261 				ap_quest();
262 			else
263 				ap_pos(PAGE_NUM(linenum));
264 		}
265 		break;
266 	case 'E':	/* Editor name */
267 		ap_str(editor);
268 		break;
269 	case 'f':	/* File name */
270 		ap_str(get_filename(curr_ifile));
271 		break;
272 	case 'F':	/* Last component of file name */
273 		ap_str(last_component(get_filename(curr_ifile)));
274 		break;
275 	case 'i':	/* Index into list of files */
276 		if (ntags())
277 			ap_int(curr_tag());
278 		else
279 			ap_int(get_index(curr_ifile));
280 		break;
281 	case 'l':	/* Current line number */
282 		linenum = currline(where);
283 		if (linenum != 0)
284 			ap_pos(linenum);
285 		else
286 			ap_quest();
287 		break;
288 	case 'L':	/* Final line number */
289 		len = ch_length();
290 		if (len == -1 || len == ch_zero() ||
291 		    (linenum = find_linenum(len)) <= 0)
292 			ap_quest();
293 		else
294 			ap_pos(linenum-1);
295 		break;
296 	case 'm':	/* Number of files */
297 		n = ntags();
298 		if (n)
299 			ap_int(n);
300 		else
301 			ap_int(nifile());
302 		break;
303 	case 'p':	/* Percent into file (bytes) */
304 		pos = curr_byte(where);
305 		len = ch_length();
306 		if (pos != -1 && len > 0)
307 			ap_int(percentage(pos, len));
308 		else
309 			ap_quest();
310 		break;
311 	case 'P':	/* Percent into file (lines) */
312 		linenum = currline(where);
313 		if (linenum == 0 ||
314 		    (len = ch_length()) == -1 || len == ch_zero() ||
315 		    (last_linenum = find_linenum(len)) <= 0)
316 			ap_quest();
317 		else
318 			ap_int(percentage(linenum, last_linenum));
319 		break;
320 	case 's':	/* Size of file */
321 	case 'B':
322 		len = ch_length();
323 		if (len != -1)
324 			ap_pos(len);
325 		else
326 			ap_quest();
327 		break;
328 	case 't':	/* Truncate trailing spaces in the message */
329 		while (mp > message && mp[-1] == ' ')
330 			mp--;
331 		*mp = '\0';
332 		break;
333 	case 'T':	/* Type of list */
334 		if (ntags())
335 			ap_str("tag");
336 		else
337 			ap_str("file");
338 		break;
339 	case 'x':	/* Name of next file */
340 		h = next_ifile(curr_ifile);
341 		if (h != NULL)
342 			ap_str(get_filename(h));
343 		else
344 			ap_quest();
345 		break;
346 	}
347 }
348 
349 /*
350  * Skip a false conditional.
351  * When a false condition is found (either a false IF or the ELSE part
352  * of a true IF), this routine scans the prototype string to decide
353  * where to resume parsing the string.
354  * We must keep track of nested IFs and skip them properly.
355  */
356 static const char *
skipcond(const char * p)357 skipcond(const char *p)
358 {
359 	int iflevel;
360 
361 	/*
362 	 * We came in here after processing a ? or :,
363 	 * so we start nested one level deep.
364 	 */
365 	iflevel = 1;
366 
367 	for (;;) {
368 		switch (*++p) {
369 		case '?':
370 			/*
371 			 * Start of a nested IF.
372 			 */
373 			iflevel++;
374 			break;
375 		case ':':
376 			/*
377 			 * Else.
378 			 * If this matches the IF we came in here with,
379 			 * then we're done.
380 			 */
381 			if (iflevel == 1)
382 				return (p);
383 			break;
384 		case '.':
385 			/*
386 			 * Endif.
387 			 * If this matches the IF we came in here with,
388 			 * then we're done.
389 			 */
390 			if (--iflevel == 0)
391 				return (p);
392 			break;
393 		case '\\':
394 			/*
395 			 * Backslash escapes the next character.
396 			 */
397 			++p;
398 			break;
399 		case '\0':
400 			/*
401 			 * Whoops.  Hit end of string.
402 			 * This is a malformed conditional, but just treat it
403 			 * as if all active conditionals ends here.
404 			 */
405 			return (p-1);
406 		}
407 	}
408 }
409 
410 /*
411  * Decode a char that represents a position on the screen.
412  */
413 static const char *
wherechar(const char * p,int * wp)414 wherechar(const char *p, int *wp)
415 {
416 	switch (*p) {
417 	case 'b': case 'd': case 'l': case 'p': case 'P':
418 		switch (*++p) {
419 		case 't':   *wp = TOP;			break;
420 		case 'm':   *wp = MIDDLE;		break;
421 		case 'b':   *wp = BOTTOM;		break;
422 		case 'B':   *wp = BOTTOM_PLUS_ONE;	break;
423 		case 'j':   *wp = adjsline(jump_sline);	break;
424 		default:    *wp = TOP; p--;		break;
425 		}
426 	}
427 	return (p);
428 }
429 
430 /*
431  * Construct a message based on a prototype string.
432  */
433 char *
pr_expand(const char * proto,int maxwidth)434 pr_expand(const char *proto, int maxwidth)
435 {
436 	const char *p;
437 	int c;
438 	int where;
439 
440 	mp = message;
441 
442 	if (*proto == '\0')
443 		return ("");
444 
445 	for (p = proto; *p != '\0'; p++) {
446 		switch (*p) {
447 		default:	/* Just put the character in the message */
448 			ap_char(*p);
449 			break;
450 		case '\\':	/* Backslash escapes the next character */
451 			p++;
452 			ap_char(*p);
453 			break;
454 		case '?':	/* Conditional (IF) */
455 			if ((c = *++p) == '\0') {
456 				--p;
457 			} else {
458 				where = 0;
459 				p = wherechar(p, &where);
460 				if (!cond(c, where))
461 					p = skipcond(p);
462 			}
463 			break;
464 		case ':':	/* ELSE */
465 			p = skipcond(p);
466 			break;
467 		case '.':	/* ENDIF */
468 			break;
469 		case '%':	/* Percent escape */
470 			if ((c = *++p) == '\0') {
471 				--p;
472 			} else {
473 				where = 0;
474 				p = wherechar(p, &where);
475 				protochar(c, where);
476 			}
477 			break;
478 		}
479 	}
480 
481 	if (*message == '\0')
482 		return ("");
483 	if (maxwidth > 0 && mp >= message + maxwidth) {
484 		/*
485 		 * Message is too long.
486 		 * Return just the final portion of it.
487 		 */
488 		return (mp - maxwidth);
489 	}
490 	return (message);
491 }
492 
493 /*
494  * Return a message suitable for printing by the "=" command.
495  */
496 char *
eq_message(void)497 eq_message(void)
498 {
499 	return (pr_expand(eqproto, 0));
500 }
501 
502 /*
503  * Return a prompt.
504  * This depends on the prompt type (SHORT, MEDIUM, LONG), etc.
505  * If we can't come up with an appropriate prompt, return NULL
506  * and the caller will prompt with a colon.
507  */
508 char *
prompt_string(void)509 prompt_string(void)
510 {
511 	char *prompt;
512 	int type;
513 
514 	type = pr_type;
515 	prompt = pr_expand((ch_getflags() & CH_HELPFILE) ?
516 	    hproto : prproto[type], sc_width-so_s_width-so_e_width-2);
517 	new_file = 0;
518 	return (prompt);
519 }
520 
521 /*
522  * Return a message suitable for printing while waiting in the F command.
523  */
524 char *
wait_message(void)525 wait_message(void)
526 {
527 	return (pr_expand(wproto, sc_width-so_s_width-so_e_width-2));
528 }
529