1 /*
2  * dinput.c - Dumb interface, input functions
3  *
4  * This file is part of Frotz.
5  *
6  * Frotz is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * Frotz is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19  * Or visit http://www.fsf.org/
20  */
21 
22 #include <string.h>
23 #include <libgen.h>
24 
25 #include "dfrotz.h"
26 
27 extern f_setup_t f_setup;
28 
29 static char runtime_usage[] =
30 	"DUMB-FROTZ runtime help:\n"
31 	"  General Commands:\n"
32 	"    \\help    Show this message.\n"
33 	"    \\set     Show the current values of runtime settings.\n"
34 	"    \\s       Show the current contents of the whole screen.\n"
35 	"    \\d       Discard the part of the input before the cursor.\n"
36 	"    \\wN      Advance clock N/10 seconds, possibly causing the current\n"
37 	"                and subsequent inputs to timeout.\n"
38 	"    \\w       Advance clock by the amount of real time since this input\n"
39 	"                started (times the current speed factor).\n"
40 	"    \\t       Advance clock just enough to timeout the current input\n"
41 	"  Reverse-Video Display Method Settings:\n"
42 	"    \\rn   none    \\rc   CAPS    \\rd   doublestrike    \\ru   underline\n"
43 	"    \\rbC  show rv blanks as char C (orthogonal to above modes)\n"
44 	"  Output Compression Settings:\n"
45 	"    \\cn      none: show whole screen before every input.\n"
46 	"    \\cm      max: show only lines that have new nonblank characters.\n"
47 	"    \\cs      spans: like max, but emit a blank line between each span of\n"
48 	"                screen lines shown.\n"
49 	"    \\chN     Hide top N lines (orthogonal to above modes).\n"
50 	"  Misc Settings:\n"
51 	"    \\sfX     Set speed factor to X.  (0 = never timeout automatically).\n"
52 	"    \\mp      Toggle use of MORE prompts\n"
53 	"    \\ln      Toggle display of line numbers.\n"
54 	"    \\lt      Toggle display of the line type identification chars.\n"
55 	"    \\vb      Toggle visual bell.\n"
56 	"    \\pb      Toggle display of picture outline boxes.\n"
57 	"    (Toggle commands can be followed by a 1 or 0 to set value ON or OFF.)\n"
58 	"  Character Escapes:\n"
59 	"    \\\\  backslash    \\#  backspace    \\[  escape    \\_  return\n"
60 	"    \\< \\> \\^ \\.  cursor motion        \\1 ..\\0  f1..f10\n"
61 	"    \\D ..\\X   Standard Frotz hotkeys.  Use \\H (help) to see the list.\n"
62 	"  Line Type Identification Characters:\n"
63 	"    Input lines:\n"
64 	"      untimed  timed\n"
65 	"      >        T      A regular line-oriented input\n"
66 	"      )        t      A single-character input\n"
67 	"      }        D      A line input with some input before the cursor.\n"
68 	"                         (Use \\d to discard it.)\n"
69 	"    Output lines:\n"
70 	"      ]     Output line that contains the cursor.\n"
71 	"      .     A blank line emitted as part of span compression.\n"
72 	"            (blank) Any other output line.\n"
73 ;
74 
75 static float speed = 1;
76 
77 enum input_type {
78 	INPUT_CHAR,
79 	INPUT_LINE,
80 	INPUT_LINE_CONTINUED,
81 };
82 
83 
84 /* get a character.  Exit with no fuss on EOF.  */
xgetchar(void)85 static int xgetchar(void)
86 {
87 	int c = getchar();
88 	if (c == EOF) {
89 		if (feof(stdin)) {
90 			fprintf(stderr, "\nEOT\n");
91 			os_quit(EXIT_SUCCESS);
92 		}
93 		os_fatal(strerror(errno));
94 	}
95 	return c;
96 }
97 
98 
99 /* Read one line, including the newline, into s.  Safely avoids buffer
100  * overruns (but that's kind of pointless because there are several
101  * other places where I'm not so careful).  */
dumb_getline(char * s)102 static void dumb_getline(char *s)
103 {
104 	int c;
105 	char *p = s;
106 	while (p < s + INPUT_BUFFER_SIZE - 1) {
107 		if ((*p++ = xgetchar()) == '\n') {
108 			*p = '\0';
109 			return;
110 		}
111 	}
112 	p[-1] = '\n';
113 	p[0] = '\0';
114 	while ((c = xgetchar()) != '\n')
115 		;
116 	printf("Line too long, truncated to %s\n", s - INPUT_BUFFER_SIZE);
117 }
118 
119 
120 /* Translate in place all the escape characters in s.  */
translate_special_chars(char * s)121 static void translate_special_chars(char *s)
122 {
123 	char *src = s, *dest = s;
124 	while (*src)
125 		switch(*src++) {
126 		default: *dest++ = src[-1]; break;
127 		case '\n': *dest++ = ZC_RETURN; break;
128 		case '\\':
129 			switch (*src++) {
130 			case '\n': *dest++ = ZC_RETURN; break;
131 			case '\\': *dest++ = '\\'; break;
132 			case '?': *dest++ = ZC_BACKSPACE; break;
133 			case '[': *dest++ = ZC_ESCAPE; break;
134 			case '_': *dest++ = ZC_RETURN; break;
135 			case '^': *dest++ = ZC_ARROW_UP; break;
136 			case '.': *dest++ = ZC_ARROW_DOWN; break;
137 			case '<': *dest++ = ZC_ARROW_LEFT; break;
138 			case '>': *dest++ = ZC_ARROW_RIGHT; break;
139 			case 'R': *dest++ = ZC_HKEY_RECORD; break;
140 			case 'P': *dest++ = ZC_HKEY_PLAYBACK; break;
141 			case 'S': *dest++ = ZC_HKEY_SEED; break;
142 			case 'U': *dest++ = ZC_HKEY_UNDO; break;
143 			case 'N': *dest++ = ZC_HKEY_RESTART; break;
144 			case 'X': *dest++ = ZC_HKEY_QUIT; break;
145 			case 'D': *dest++ = ZC_HKEY_DEBUG; break;
146 			case 'H': *dest++ = ZC_HKEY_HELP; break;
147 			case '1': *dest++ = ZC_FKEY_F1; break;
148 			case '2': *dest++ = ZC_FKEY_F2; break;
149 			case '3': *dest++ = ZC_FKEY_F3; break;
150 			case '4': *dest++ = ZC_FKEY_F4; break;
151 			case '5': *dest++ = ZC_FKEY_F5; break;
152 			case '6': *dest++ = ZC_FKEY_F6; break;
153 			case '7': *dest++ = ZC_FKEY_F7; break;
154 			case '8': *dest++ = ZC_FKEY_F8; break;
155 			case '9': *dest++ = ZC_FKEY_F9; break;
156 			case '0': *dest++ = ZC_FKEY_F10; break;
157 			default:
158 				fprintf(stderr, "DUMB-FROTZ: unknown escape char: %c\n", src[-1]);
159 				fprintf(stderr, "Enter \\help to see the list\n");
160 			}
161 		}
162 	*dest = '\0';
163 }
164 
165 
166 /* The time in tenths of seconds that the user is ahead of z time.  */
167 static int time_ahead = 0;
168 
169 /* Called from os_read_key and os_read_line if they have input from
170  * a previous call to dumb_read_line.
171  * Returns TRUE if we should timeout rather than use the read-ahead.
172  * (because the user is further ahead than the timeout).  */
check_timeout(int timeout)173 static bool check_timeout(int timeout)
174 {
175 	if ((timeout == 0) || (timeout > time_ahead))
176 		time_ahead = 0;
177 	else
178 		time_ahead -= timeout;
179 	return time_ahead != 0;
180 }
181 
182 
183 /* If val is '0' or '1', set *var accordingly, otherwise toggle it.  */
toggle(bool * var,char val)184 static void toggle(bool *var, char val)
185 {
186 	*var = val == '1' || (val != '0' && !*var);
187 }
188 
189 
190 /* Handle input-related user settings and call dumb_output_handle_setting.  */
dumb_handle_setting(const char * setting,bool show_cursor,bool startup)191 bool dumb_handle_setting(const char *setting, bool show_cursor, bool startup)
192 {
193 	if (!strncmp(setting, "sf", 2)) {
194 		speed = atof(&setting[2]);
195 		printf("Speed Factor %g\n", speed);
196 	} else if (!strncmp(setting, "mp", 2)) {
197 		toggle(&do_more_prompts, setting[2]);
198 		printf("More prompts %s\n", do_more_prompts ? "ON" : "OFF");
199 	} else {
200 		if (!strcmp(setting, "set")) {
201 			printf("Speed Factor %g\n", speed);
202 			printf("More Prompts %s\n",
203 				do_more_prompts ? "ON" : "OFF");
204 		}
205 		return dumb_output_handle_setting(setting, show_cursor, startup);
206 	}
207 	return TRUE;
208 }
209 
210 
211 /* Read a line, processing commands (lines that start with a backslash
212  * (that isn't the start of a special character)), and write the
213  * first non-command to s.
214  * Return true if timed-out.  */
dumb_read_line(char * s,char * prompt,bool show_cursor,int timeout,enum input_type type,zchar * continued_line_chars)215 static bool dumb_read_line(char *s, char *prompt, bool show_cursor,
216 			   int timeout, enum input_type type,
217 			   zchar *continued_line_chars)
218 {
219 	time_t start_time;
220 
221 	if (timeout) {
222 		if (time_ahead >= timeout) {
223 			time_ahead -= timeout;
224 			return TRUE;
225 		}
226 		timeout -= time_ahead;
227 		start_time = time(0);
228 	}
229 	time_ahead = 0;
230 
231 	dumb_show_screen(show_cursor);
232 	for (;;) {
233 		char *command;
234 		if (prompt)
235 			fputs(prompt, stdout);
236 		else
237 			dumb_show_prompt(show_cursor,
238 				(timeout ? "tTD" : ")>}")[type]);
239 
240 		/* Prompt only shows up after user input if we don't flush stdout */
241 		fflush(stdout);
242 		dumb_getline(s);
243 		if ((s[0] != '\\') || ((s[1] != '\0') && !islower(s[1]))) {
244 			/* Is not a command line.  */
245 			translate_special_chars(s);
246 			if (timeout) {
247 				int elapsed = (time(0) - start_time) * 10 * speed;
248 				if (elapsed > timeout) {
249 					time_ahead = elapsed - timeout;
250 					return TRUE;
251 				}
252 			}
253 			return FALSE;
254 		}
255 		/* Commands.  */
256 
257 		/* Remove the \ and the terminating newline.  */
258 		command = s + 1;
259 		command[strlen(command) - 1] = '\0';
260 
261 		if (!strcmp(command, "t")) {
262 			if (timeout) {
263 				time_ahead = 0;
264 				s[0] = '\0';
265 				return TRUE;
266 			}
267 		} else if (*command == 'w') {
268 			if (timeout) {
269 				int elapsed = atoi(&command[1]);
270 				time_t now = time(0);
271 				if (elapsed == 0)
272 					elapsed = (now - start_time) * 10 * speed;
273 				if (elapsed >= timeout) {
274 					time_ahead = elapsed - timeout;
275 					s[0] = '\0';
276 					return TRUE;
277 				}
278 				timeout -= elapsed;
279 				start_time = now;
280 			}
281 		} else if (!strcmp(command, "d")) {
282 			if (type != INPUT_LINE_CONTINUED)
283 				fprintf(stderr, "DUMB-FROTZ: No input to discard\n");
284 			else {
285 				dumb_discard_old_input(strlen((char*) continued_line_chars));
286 				continued_line_chars[0] = '\0';
287 				type = INPUT_LINE;
288 			}
289 		} else if (!strcmp(command, "help")) {
290 			if (!do_more_prompts)
291 				fputs(runtime_usage, stdout);
292 			else {
293 				char *current_page, *next_page;
294 				current_page = next_page = runtime_usage;
295 				for (;;) {
296 					int i;
297 					for (i = 0; (i < z_header.screen_rows - 2) && *next_page; i++)
298 						next_page = strchr(next_page, '\n') + 1;
299 					/* next_page - current_page is width */
300 					printf("%.*s", (int) (next_page - current_page), current_page);
301 					current_page = next_page;
302 					if (!*current_page)
303 						break;
304 					printf("HELP: Type <return> for more, or q <return> to stop: ");
305 					fflush(stdout);
306 					dumb_getline(s);
307 					if (!strcmp(s, "q\n"))
308 						break;
309 				}
310 			}
311 		} else if (!strcmp(command, "s")) {
312 			dumb_dump_screen();
313     		} else if (!dumb_handle_setting(command, show_cursor, FALSE)) {
314 			fprintf(stderr, "DUMB-FROTZ: unknown command: %s\n", s);
315 			fprintf(stderr, "Enter \\help to see the list of commands\n");
316 		}
317 	}
318 }
319 
320 
321 /* Read a line that is not part of z-machine input (more prompts and
322  * filename requests).  */
dumb_read_misc_line(char * s,char * prompt)323 static void dumb_read_misc_line(char *s, char *prompt)
324 {
325 	dumb_read_line(s, prompt, 0, 0, 0, 0);
326 	/* Remove terminating newline */
327 	s[strlen(s) - 1] = '\0';
328 }
329 
330 
331 /* For allowing the user to input in a single line keys to be returned
332  * for several consecutive calls to read_char, with no screen update
333  * in between.  Useful for traversing menus.  */
334 static char read_key_buffer[INPUT_BUFFER_SIZE];
335 
336 /* Similar.  Useful for using function key abbreviations.  */
337 static char read_line_buffer[INPUT_BUFFER_SIZE];
338 
339 #ifdef USE_UTF8
340 /* Convert UTF-8 encoded char starting at in[idx] to zchar (UCS-2) if
341  * representable in 16 bits or '?' otherwise and return index to next
342  * char of input array. */
utf8_to_zchar(zchar * out,char * in,int idx)343 static int utf8_to_zchar(zchar *out, char *in, int idx)
344 {
345 	zchar ch;
346 	int i;
347 	if ((in[idx] & 0x80) == 0) {
348 		ch = in[idx++];
349 	} else if ((in[idx] & 0xe0) == 0xc0) {
350 		ch = in[idx++] & 0x1f;
351 		if ((in[idx] & 0xc0) != 0x80)
352 			goto error;
353 		ch = (ch << 6) | (in[idx++] & 0x3f);
354 	} else if ((in[idx] & 0xf0) == 0xe0) {
355 		ch = in[idx++] & 0xf;
356 		for (i = 0; i < 2; i++) {
357 			if ((in[idx] & 0xc0) != 0x80)
358 				goto error;
359 			ch = (ch << 6) | (in[idx++] & 0x3f);
360 		}
361 	} else {
362 		/* Consume all subsequent continuation bytes. */
363 		while ((in[++idx] & 0xc0) == 0x80)
364 			;
365 error:
366 		ch = '?';
367 	}
368 	*out = ch;
369 	return idx;
370 }
371 #endif
372 
373 
os_read_key(int timeout,bool show_cursor)374 zchar os_read_key (int timeout, bool show_cursor)
375 {
376 	zchar c;
377 	int timed_out;
378 	int idx = 1;
379 
380 	/* Discard any keys read for line input.  */
381 	read_line_buffer[0] = '\0';
382 
383 	if (read_key_buffer[0] == '\0') {
384 		timed_out = dumb_read_line(read_key_buffer, NULL,
385 			show_cursor, timeout, INPUT_CHAR, NULL);
386 		/* An empty input line is reported as a single CR.
387 		 * If there's anything else in the line, we report
388 		 * only the line's contents and not the terminating CR.  */
389 		if (strlen(read_key_buffer) > 1)
390 			read_key_buffer[strlen(read_key_buffer) - 1] = '\0';
391 	} else
392 		timed_out = check_timeout(timeout);
393 
394 	if (timed_out)
395 		return ZC_TIME_OUT;
396 
397 #ifndef USE_UTF8
398 	c = read_key_buffer[0];
399 #else
400 	idx = utf8_to_zchar(&c, read_key_buffer, 0);
401 #endif
402 	memmove(read_key_buffer, read_key_buffer + idx,
403 		strlen(read_key_buffer) - idx + 1);
404 
405 	/* TODO: error messages for invalid special chars.  */
406 
407 	return c;
408 }
409 
410 
os_read_line(int UNUSED (max),zchar * buf,int timeout,int UNUSED (width),int continued)411 zchar os_read_line (int UNUSED (max), zchar *buf, int timeout, int UNUSED(width), int continued)
412 {
413 	char *p;
414 	int terminator;
415 	static bool timed_out_last_time;
416 	int timed_out;
417 #ifdef USE_UTF8
418 	int i, j, len;
419 #endif
420 
421 	/* Discard any keys read for single key input.  */
422 	read_key_buffer[0] = '\0';
423 
424 	/* After timing out, discard any further input unless
425 	 * we're continuing.  */
426 	if (timed_out_last_time && !continued)
427 		read_line_buffer[0] = '\0';
428 
429 	if (read_line_buffer[0] == '\0') {
430 		timed_out = dumb_read_line(read_line_buffer, NULL, TRUE,
431 			timeout, buf[0] ? INPUT_LINE_CONTINUED : INPUT_LINE,
432 			buf);
433 	} else
434 		timed_out = check_timeout(timeout);
435 
436 	if (timed_out) {
437 		timed_out_last_time = TRUE;
438 		return ZC_TIME_OUT;
439 	}
440 
441 	/* find the terminating character.  */
442 	for (p = read_line_buffer;; p++) {
443 		if (is_terminator(*p)) {
444 			terminator = *p;
445 			*p++ = '\0';
446 			break;
447 		}
448 	}
449 
450 	/* TODO: Truncate to width and max.  */
451 
452 	/* copy to screen */
453 	dumb_display_user_input(read_line_buffer);
454 
455 	/* copy to the buffer and save the rest for next time.  */
456 #ifndef USE_UTF8
457 	strncat((char*) buf, read_line_buffer,
458 		(INPUT_BUFFER_SIZE - strlen((char *)buf)) - 2);
459 #else
460 	for (len = 0;; len++) {
461 		if (!buf[len])
462 			break;
463 	}
464 	j = 0;
465 	for (i = len; i < INPUT_BUFFER_SIZE - 2; i++) {
466 		if (!read_line_buffer[j])
467 			break;
468 		j = utf8_to_zchar(&buf[i], read_line_buffer, j);
469 	}
470 	buf[i] = 0;
471 #endif
472 	p = read_line_buffer + strlen(read_line_buffer) + 1;
473 	memmove(read_line_buffer, p, strlen(p) + 1);
474 
475 	/* If there was just a newline after the terminating character,
476 	 * don't save it.  */
477 	if ((read_line_buffer[0] == '\r') && (read_line_buffer[1] == '\0'))
478 		read_line_buffer[0] = '\0';
479 
480 	timed_out_last_time = FALSE;
481 	return terminator;
482 }
483 
484 
os_read_file_name(const char * default_name,int flag)485 char *os_read_file_name (const char *default_name, int flag)
486 {
487 	char file_name[FILENAME_MAX + 1];
488 	char fullpath[INPUT_BUFFER_SIZE], prompt[INPUT_BUFFER_SIZE];
489 	char *buf;
490 	FILE *fp;
491 	char *tempname;
492 	char path_separator[2];
493 	int i;
494 
495 	path_separator[0] = PATH_SEPARATOR;
496 	path_separator[1] = 0;
497 
498 	/* If we're restoring a game before the interpreter starts,
499  	 * our filename is already provided.  Just go ahead silently.
500 	 */
501 	if (f_setup.restore_mode) {
502 		return strdup(default_name);
503 	} else {
504 		if (f_setup.restricted_path) {
505 			for (i = strlen(default_name); i > 0; i--) {
506 				if (default_name[i] == PATH_SEPARATOR) {
507 					i++;
508 					break;
509 				}
510 			}
511 			tempname = strdup(default_name + i);
512 			sprintf(prompt, "Please enter a filename [%s]: ", tempname);
513 		} else
514 			sprintf(prompt, "Please enter a filename [%s]: ", default_name);
515 		dumb_read_misc_line(fullpath, prompt);
516 		buf = basename (fullpath);
517 		if (strlen(buf) > MAX_FILE_NAME) {
518 			printf("Filename too long\n");
519 			return NULL;
520 		}
521 	}
522 
523 	if (buf[0])
524 		strncpy(file_name, fullpath, FILENAME_MAX);
525 	else
526 		strncpy(file_name, default_name, FILENAME_MAX);
527 
528 	/* Check if we're restricted to one directory. */
529 	if (f_setup.restricted_path != NULL) {
530 		for (i = strlen(file_name); i > 0; i--) {
531 			if (file_name[i] == PATH_SEPARATOR) {
532 				i++;
533 				break;
534 			}
535 		}
536 		tempname = strdup(file_name + i);
537 		strncpy(file_name, f_setup.restricted_path, FILENAME_MAX);
538 
539 		/* Make sure the final character is the path separator. */
540 		if (file_name[strlen(file_name)-1] != PATH_SEPARATOR) {
541 			strncat(file_name, path_separator, FILENAME_MAX - strlen(file_name) - 2);
542 		}
543 		strncat(file_name, tempname, strlen(file_name) - strlen(tempname) - 1);
544 	}
545 
546 	/* Warn if overwriting a file.  */
547 	if ((flag == FILE_SAVE || flag == FILE_SAVE_AUX || flag == FILE_RECORD)
548 		&& ((fp = fopen(file_name, "rb")) != NULL)) {
549 		fclose (fp);
550 		dumb_read_misc_line(fullpath, "Overwrite existing file? ");
551 		if (tolower(fullpath[0]) != 'y')
552 			return NULL;
553 	}
554 	return strdup(file_name);
555 }
556 
557 
os_more_prompt(void)558 void os_more_prompt (void)
559 {
560 	if (do_more_prompts) {
561 		char buf[INPUT_BUFFER_SIZE];
562 		dumb_read_misc_line(buf, "***MORE***");
563 	} else
564 		dumb_elide_more_prompt();
565 }
566 
567 
dumb_init_input(void)568 void dumb_init_input(void)
569 {
570 	if ((z_header.version >= V4) && (speed != 0))
571 		z_header.config |= CONFIG_TIMEDINPUT;
572 
573 	if (z_header.version >= V5)
574 		z_header.flags &= ~(MOUSE_FLAG | MENU_FLAG);
575 }
576 
577 
os_read_mouse(void)578 zword os_read_mouse(void)
579 {
580 	/* NOT IMPLEMENTED */
581 	return 0;
582 }
583 
os_tick()584 void os_tick()
585 {
586 	/* Nothing here yet */
587 }
588