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