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