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