1 /* vim:set cin ft=c sw=4 sts=4 ts=8 et ai cino=Ls\:0t0(0 : -*- mode:c;fill-column:80;tab-width:8;c-basic-offset:4;indent-tabs-mode:nil;c-file-style:"k&r" -*-*/
2 /* NetHack 3.6 cursmesg.c */
3 /* Copyright (c) Karl Garrison, 2010. */
4 /* NetHack may be freely redistributed. See license for details. */
13 * Note: references to "More>>" mean ">>", the curses rendition of "--More--".
16 /* player can type ESC at More>> prompt to avoid seeing more messages
17 for the current move; but hero might get more than one move per turn,
18 so the input routines need to be able to cancel this */
19 long curs_mesg_suppress_turn = -1;
21 /* Message window routines for curses interface */
23 /* Private declatations */
26 char *str; /* Message text */
27 long turn; /* Turn number for message */
28 struct nhpm *prev_mesg; /* Pointer to previous message */
29 struct nhpm *next_mesg; /* Pointer to next message */
32 static void scroll_window(winid wid);
33 static void unscroll_window(winid wid);
34 static void directional_scroll(winid wid, int nlines);
35 static void mesg_add_line(const char *mline);
36 static nhprev_mesg *get_msg_line(boolean reverse, int mindex);
38 static int turn_lines = 0;
40 static int my = 0; /* message window text location */
41 static nhprev_mesg *first_mesg = NULL;
42 static nhprev_mesg *last_mesg = NULL;
43 static int max_messages;
44 static int num_messages = 0;
45 static int last_messages = 0;
47 /* Write string to the message window. Attributes set by calling function. */
50 curses_message_win_puts(const char *message, boolean recursed)
52 int height, width, border_space, linespace;
54 WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
55 boolean bold, border = curses_window_has_border(MESSAGE_WIN);
56 int message_length = (int) strlen(message);
60 * This was useful when curses used genl_putmsghistory() but is not
61 * needed now that it has its own curses_putmsghistory() which is
62 * capable of putting something into the ^P recall history without
63 * displaying it at the same time.
65 if (strncmp("Count:", message, 6) == 0) {
66 curses_count_window(message);
71 if (curs_mesg_suppress_turn == moves) {
72 return; /* user has typed ESC to avoid seeing remaining messages. */
75 curses_get_window_size(MESSAGE_WIN, &height, &width);
76 border_space = (border ? 1 : 0);
77 if (mx < border_space)
79 if (my < border_space)
82 if (strcmp(message, "#") == 0) { /* Extended command or Count: */
83 if ((strcmp(toplines, "#") != 0)
84 /* Bottom of message window */
85 && (my >= (height - 1 + border_space)) && (height != 1)) {
86 scroll_window(MESSAGE_WIN);
89 strcpy(toplines, message);
95 strcpy(toplines, message);
96 mesg_add_line(message);
99 linespace = width - 3 - (mx - border_space);
101 if (linespace < message_length) {
102 if (my - border_space >= height - 1) {
103 /* bottom of message win */
104 if (++turn_lines >= height) { /* || height == 1) */
105 /* Pause until key is hit - Esc suppresses any further
106 messages that turn */
107 if (curses_more() == '\033') {
108 curs_mesg_suppress_turn = moves;
111 /* turn_lines reset to 0 by more()->block()->got_input() */
113 scroll_window(MESSAGE_WIN);
116 if (mx != border_space) {
124 bold = (height > 1 && !last_messages);
126 curses_toggle_color_attr(win, NONE, A_BOLD, ON);
128 /* will this message fit as-is or do we need to split it? */
129 if (mx == border_space && message_length > width - 2) {
131 tmpstr = curses_break_str(message, (width - 2), 1);
132 mvwprintw(win, my, mx, "%s", tmpstr), mx += (int) strlen(tmpstr);
133 /* one space to separate first part of message from rest [is this
139 curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
140 tmpstr = curses_str_remainder(message, (width - 2), 1);
141 curses_message_win_puts(tmpstr, TRUE);
144 mvwprintw(win, my, mx, "%s", message), mx += message_length;
145 /* two spaces to separate this message from next one if they happen
146 to fit on the same line; (FIXME: it would be better if this was
147 done at start of next message rather than end of this one since
148 it impacts placement of "More>>") */
149 if (mx < width - 2) {
150 if (++mx < width - 2)
154 curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
160 curses_got_input(void)
162 /* if messages are being suppressed, reenable them */
163 curs_mesg_suppress_turn = -1;
165 /* misleadingly named; represents number of lines delivered since
166 player was sure to have had a chance to read them; if player
167 has just given input then there aren't any such lines right;
168 that includes responding to More>> even though it stays same turn */
173 curses_block(boolean noscroll) /* noscroll - blocking because of msgtype
174 * = stop/alert else blocking because
175 * window is full, so need to scroll after */
177 static const char resp[] = " \r\n\033"; /* space, enter, esc */
178 static int prev_x = -1, prev_y = -1, blink = 0;
179 int height, width, moreattr, oldcrsr, ret = 0,
180 brdroffset = curses_window_has_border(MESSAGE_WIN) ? 1 : 0;
181 WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
183 curses_get_window_size(MESSAGE_WIN, &height, &width);
184 if (mx - brdroffset > width - 3) { /* -3: room for ">>_" */
185 if (my - brdroffset < height - 1)
186 ++my, mx = brdroffset;
188 mx = width - 3 + brdroffset;
190 /* if ">>" (--More--) is being rendered at the same spot as before,
191 toggle attributes so that the first '>' starts blinking if it wasn't
192 or stops blinking if it was */
193 if (mx == prev_x && my == prev_y) {
196 prev_x = mx, prev_y = my;
199 moreattr = !iflags.wc2_guicolor ? A_REVERSE : NONE;
200 curses_toggle_color_attr(win, MORECOLOR, moreattr, ON);
202 wattron(win, A_BLINK);
203 mvwprintw(win, my, mx, ">"), mx += 1;
204 wattroff(win, A_BLINK);
205 waddstr(win, ">"), mx += 1;
207 mvwprintw(win, my, mx, ">>"), mx += 2;
209 curses_toggle_color_attr(win, MORECOLOR, moreattr, OFF);
212 /* cancel mesg suppression; all messages will have had chance to be read */
215 oldcrsr = curs_set(1);
218 if (ret == ERR || ret == '\0')
220 /* msgtype=stop should require space/enter rather than any key,
221 as we want to prevent YASD from direction keys. */
222 } while (!index(resp, (char) ret));
224 (void) curs_set(oldcrsr);
227 curses_clear_unhighlight_message_window();
229 mx -= 2, mvwprintw(win, my, mx, " "); /* back up and blank out ">>" */
231 scroll_window(MESSAGE_WIN);
241 return curses_block(FALSE);
245 /* Clear the message window if one line; otherwise unhighlight old messages */
248 curses_clear_unhighlight_message_window()
251 brdroffset = curses_window_has_border(MESSAGE_WIN) ? 1 : 0;
252 WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
255 curses_get_window_size(MESSAGE_WIN, &mh, &mw);
258 curses_clear_nhwin(MESSAGE_WIN);
259 mx = my = brdroffset;
261 mx = mw + brdroffset; /* Force new line on new turn */
263 for (count = 0; count < mh; count++)
264 mvwchgat(win, count + brdroffset, brdroffset,
265 mw, COLOR_PAIR(8), A_NORMAL, NULL);
272 /* Reset message window cursor to starting position, and display most
276 curses_last_messages()
278 boolean border = curses_window_has_border(MESSAGE_WIN);
280 int i, j, height, width;
282 curses_get_window_size(MESSAGE_WIN, &height, &width);
290 for (j = 0, i = num_messages - 1; i > 0 && j < height; --i, ++j) {
291 mesg = get_msg_line(TRUE, i);
292 if (mesg && mesg->str && *mesg->str)
293 curses_message_win_puts(mesg->str, TRUE);
295 curses_message_win_puts(toplines, TRUE);
300 /* Initialize list for message history */
303 curses_init_mesg_history()
305 max_messages = iflags.msg_history;
307 if (max_messages < 1) {
311 if (max_messages > MESG_HISTORY_MAX) {
312 max_messages = MESG_HISTORY_MAX;
316 /* Delete message history at game end. */
319 curses_teardown_messages(void)
321 nhprev_mesg *current_mesg;
323 while ((current_mesg = first_mesg) != 0) {
324 first_mesg = current_mesg->next_mesg;
325 free(current_mesg->str);
328 last_mesg = (nhprev_mesg *) 0;
332 /* Display previous message window messages in reverse chron order */
342 menu_item *selected = NULL;
343 boolean do_lifo = (iflags.prevmsg_window != 'f');
345 wid = curses_get_wid(NHW_MENU);
346 curses_create_nhmenu(wid);
349 for (count = 0; count < num_messages; ++count) {
350 mesg = get_msg_line(do_lifo, count);
351 if (turn != mesg->turn && count != 0) {
352 curses_add_menu(wid, NO_GLYPH, &Id, 0, 0, A_NORMAL, "---", FALSE);
354 curses_add_menu(wid, NO_GLYPH, &Id, 0, 0, A_NORMAL, mesg->str, FALSE);
358 curses_add_menu(wid, NO_GLYPH, &Id, 0, 0, A_NORMAL,
359 "[No past messages available.]", FALSE);
361 curses_end_menu(wid, "");
363 curs_menu_set_bottom_heavy(wid);
364 curses_select_menu(wid, PICK_NONE, &selected);
369 /* Display at the bottom of the message window without remembering the
370 line for ^P recall. Used for putstr(WIN_MESSAGE,ATR_NOHISTORY,text)
371 which core currently uses for 'Count: 123' and dolook's autodescribe.
372 popup_dialog is not currently implemented for this function. */
375 curses_count_window(const char *count_text)
377 static WINDOW *countwin = NULL;
378 int startx, starty, winx, winy;
379 int messageh, messagew;
383 delwin(countwin), countwin = NULL;
389 curses_get_window_xy(MESSAGE_WIN, &winx, &winy);
390 curses_get_window_size(MESSAGE_WIN, &messageh, &messagew);
392 if (curses_window_has_border(MESSAGE_WIN)) {
397 winy += messageh - 1;
401 curses_destroy_win(countwin), countwin = NULL;
402 #endif /* PDCURSES */
403 /* this used to specify a width of 25; that was adequate for 'Count: 123'
404 but not for dolook's autodescribe when it refers to a named monster */
406 countwin = newwin(1, messagew, winy, winx);
410 mvwprintw(countwin, starty, startx, "%s", count_text);
414 /* Gets a "line" (buffer) of input. */
416 curses_message_win_getline(const char *prompt, char *answer, int buffer)
418 int height, width; /* of window */
419 char *tmpbuf, *p_answer; /* combined prompt + answer */
420 int nlines, maxlines, i; /* prompt + answer */
423 char **linestarts; /* pointers to start of each line */
424 char *tmpstr; /* for free() */
425 int maxy, maxx; /* linewrap / scroll */
427 WINDOW *win = curses_get_nhwin(MESSAGE_WIN);
428 int border_space = 0;
429 int len = 0; /* of answer string */
430 boolean border = curses_window_has_border(MESSAGE_WIN);
433 orig_cursor = curs_set(0);
435 curses_get_window_size(MESSAGE_WIN, &height, &width);
437 height -= 2, width -= 2;
444 maxy = height - 1 + border_space;
445 maxx = width - 1 + border_space;
447 tmpbuf = (char *) alloc((unsigned) ((int) strlen(prompt) + buffer + 2));
448 maxlines = buffer / width * 2;
449 Strcpy(tmpbuf, prompt);
451 nlines = curses_num_lines(tmpbuf, width);
452 maxlines += nlines * 2;
453 linestarts = (char **) alloc((unsigned) (maxlines * sizeof (char *)));
454 p_answer = tmpbuf + strlen(tmpbuf);
455 linestarts[0] = tmpbuf;
457 if (mx > border_space) { /* newline */
459 scroll_window(MESSAGE_WIN);
465 curses_toggle_color_attr(win, NONE, A_BOLD, ON);
467 for (i = 0; i < nlines - 1; i++) {
468 tmpstr = curses_break_str(linestarts[i], width - 1, 1);
469 linestarts[i + 1] = linestarts[i] + (int) strlen(tmpstr);
470 if (*linestarts[i + 1] == ' ')
472 mvwaddstr(win, my, mx, tmpstr);
475 scroll_window(MESSAGE_WIN);
479 mvwaddstr(win, my, mx, linestarts[nlines - 1]);
480 mx = promptx = (int) strlen(linestarts[nlines - 1]) + border_space;
481 promptline = nlines - 1;
484 mx = (int) strlen(linestarts[nlines - 1]) + border_space;
486 if (nlines < maxlines) {
487 tmpstr = curses_break_str(linestarts[nlines - 1],
489 mx = (int) strlen(tmpstr) + border_space;
490 mvwprintw(win, my, mx, "%*c", maxx - mx + 1, ' ');
492 scroll_window(MESSAGE_WIN);
496 linestarts[nlines] = linestarts[nlines - 1]
497 + (int) strlen(tmpstr);
498 if (*linestarts[nlines] == ' ')
499 linestarts[nlines]++;
500 mvwaddstr(win, my, mx, linestarts[nlines]);
501 mx = (int) strlen(linestarts[nlines]) + border_space;
505 p_answer[--len] = '\0';
506 mvwaddch(win, my, --mx, ' ');
512 curses_got_input(); /* despite its name, before rathre than after... */
518 #if 0 /* [erase_char (delete one character) and kill_char (delete all
519 * characters) are from tty and not currently set up for curses] */
520 if (ch == erase_char) {
521 ch = '\177'; /* match switch-case below */
523 /* honor kill_char if it's ^U or similar, but not if it's '@' */
524 } else if (ch == kill_char && (ch < ' ' || ch >= '\177')) { /*ASCII*/
525 if (len == 0) /* nothing to kill; just start over */
527 ch = '\033'; /* get rid of all current input, then start over */
532 case '\033': /* DOESCAPE */
533 /* if there isn't any input yet, return ESC */
535 Strcpy(answer, "\033");
538 /* otherwise, discard current input and start over;
539 first need to blank it from the screen */
540 while (nlines - 1 > promptline) {
541 if (nlines-- > height) {
542 unscroll_window(MESSAGE_WIN);
543 tmpstr = curses_break_str(linestarts[nlines - height],
545 mvwaddstr(win, border_space, border_space, tmpstr);
549 mvwprintw(win, my, mx, "%*c", maxx - mx, ' ');
554 mvwprintw(win, my, mx, "%*c", maxx - mx, ' ');
558 case ERR: /* should not happen */
562 curs_set(orig_cursor);
563 curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
568 (void) strncpy(answer, p_answer, buffer);
569 answer[buffer - 1] = '\0';
570 Strcpy(toplines, tmpbuf);
571 mesg_add_line(tmpbuf);
573 curs_set(orig_cursor);
574 curses_toggle_color_attr(win, NONE, A_BOLD, OFF);
576 scroll_window(MESSAGE_WIN);
581 case '\177': /* DEL/Rubout */
582 case KEY_DC: /* delete-character */
583 case '\b': /* ^H (Backspace: '\011') */
589 p_answer[--len] = '\0';
590 mvwaddch(win, my, --mx, ' ');
591 /* try to unwrap back to the previous line if there is one */
592 if (nlines > 1 && (int) strlen(linestarts[nlines - 2]) < width) {
593 mvwaddstr(win, my - 1, border_space, linestarts[nlines - 2]);
594 if (nlines-- > height) {
595 unscroll_window(MESSAGE_WIN);
596 tmpstr = curses_break_str(linestarts[nlines - height],
598 mvwaddstr(win, border_space, border_space, tmpstr);
601 /* clean up the leftovers on the next line,
602 if we didn't scroll it away */
603 mvwprintw(win, my--, border_space, "%*c",
604 (int) strlen(linestarts[nlines]), ' ');
609 p_answer[len++] = ch;
613 mvwaddch(win, my, mx, ch);
614 p_answer[len] = '\0';
619 /* Scroll lines upward in given window, or clear window if only one line. */
621 scroll_window(winid wid)
623 directional_scroll(wid, 1);
627 unscroll_window(winid wid)
629 directional_scroll(wid, -1);
633 directional_scroll(winid wid, int nlines)
635 int wh, ww, s_top, s_bottom;
636 boolean border = curses_window_has_border(wid);
637 WINDOW *win = curses_get_nhwin(wid);
639 curses_get_window_size(wid, &wh, &ww);
641 curses_clear_nhwin(wid);
652 wsetscrreg(win, s_top, s_bottom);
654 scrollok(win, FALSE);
655 if (wid == MESSAGE_WIN) {
668 /* Add given line to message history */
671 mesg_add_line(const char *mline)
673 nhprev_mesg *current_mesg;
676 * Messages are kept in a doubly linked list, with head 'first_mesg',
677 * tail 'last_mesg', and a maximum capacity of 'max_messages'.
679 if (num_messages < max_messages) {
680 /* create a new list element */
681 current_mesg = (nhprev_mesg *) alloc((unsigned) sizeof (nhprev_mesg));
682 current_mesg->str = dupstr(mline);
684 /* instead of discarding list element being forced out, reuse it */
685 current_mesg = first_mesg;
686 /* whenever new 'mline' is shorter, extra allocation size of the
687 original element will be frittered away, until eventually we'll
688 discard this 'str' and dupstr() a replacement; we could easily
689 track the allocation size but don't really need to do so */
690 if (strlen(mline) <= strlen(current_mesg->str)) {
691 Strcpy(current_mesg->str, mline);
693 free((genericptr_t) current_mesg->str);
694 current_mesg->str = dupstr(mline);
697 current_mesg->turn = moves;
699 if (num_messages == 0) {
700 /* very first message; set up head */
701 first_mesg = current_mesg;
703 /* not first message; tail exists */
704 last_mesg->next_mesg = current_mesg;
706 current_mesg->prev_mesg = last_mesg;
707 last_mesg = current_mesg; /* new tail */
709 if (num_messages < max_messages) {
710 /* wasn't at capacity yet */
713 /* at capacity; old head is being removed */
714 first_mesg = first_mesg->next_mesg; /* new head */
715 first_mesg->prev_mesg = NULL; /* head has no prev_mesg */
717 /* since 'current_mesg' might be reusing 'first_mesg' and has now
718 become 'last_mesg', this update must be after head replacement */
719 last_mesg->next_mesg = NULL; /* tail has no next_mesg */
723 /* Returns specified line from message history, or NULL if out of bounds */
726 get_msg_line(boolean reverse, int mindex)
729 nhprev_mesg *current_mesg;
732 current_mesg = last_mesg;
733 for (count = 0; count < mindex; count++) {
736 current_mesg = current_mesg->prev_mesg;
739 current_mesg = first_mesg;
740 for (count = 0; count < mindex; count++) {
743 current_mesg = current_mesg->next_mesg;
749 /* save/restore code retrieves one ^P message at a time during save and
750 puts it into save file; if any new messages are added to the list while
751 that is taking place, the results are likely to be scrambled */
753 curses_getmsghistory(init)
764 if (nxtidx < num_messages) {
765 /* we could encode mesg->turn with the text of the message,
766 but then that text might need to be truncated, and more
767 significantly, restoring the save file with another
768 interface wouldn't know how find and decode or remove it;
769 likewise, restoring another interface's save file with
770 curses wouldn't find the expected turn info;
771 so, we live without that */
772 mesg = get_msg_line(FALSE, nxtidx);
774 mesg = (nhprev_mesg *) 0;
776 return mesg ? mesg->str : (char *) 0;
780 * This is called by the core savefile restore routines.
781 * Each time we are called, we stuff the string into our message
782 * history recall buffer. The core will send the oldest message
783 * first (actually it sends them in the order they exist in the
784 * save file, but that is supposed to be the oldest first).
785 * These messages get pushed behind any which have been issued
786 * during this session since they come from a previous session
787 * and logically precede anything (like "Restoring save file...")
788 * that's happened now.
790 * Called with a null pointer to finish up restoration.
792 * It's also called by the quest pager code when a block message
793 * has a one-line summary specified. We put that line directly
794 * into message history for ^P recall without having displayed it.
797 curses_putmsghistory(msg, restoring_msghist)
799 boolean restoring_msghist;
801 static boolean initd = FALSE;
802 static int stash_count;
803 static nhprev_mesg *stash_head = 0;
805 if (restoring_msghist && !initd) {
806 /* hide any messages we've gathered since starting current session
807 so that the ^P data will start out empty as we add ones being
808 restored from save file; we'll put these back after that's done */
809 stash_count = num_messages, num_messages = 0;
810 stash_head = first_mesg, first_mesg = (nhprev_mesg *) 0;
811 last_mesg = (nhprev_mesg *) 0; /* no need to remember the tail */
817 /* treat all saved and restored messages as turn #1 */
818 last_mesg->turn = 1L;
819 } else if (stash_count) {
823 /* put any messages generated during the beginning of the current
824 session back; they logically follow any from the previous
825 session's save file */
826 while (stash_count > 0) {
827 /* we could manipulate the linked list directly but treating
828 stashed messages as newly occurring ones is much simpler;
829 we ignore the backlinks because the list is destroyed as it
830 gets processed hence there can't be any other traversals */
832 stash_head = mesg->next_mesg;
834 mesg_turn = mesg->turn;
835 mesg_add_line(mesg->str);
836 /* added line became new tail */
837 last_mesg->turn = mesg_turn;
838 free((genericptr_t) mesg->str);
839 free((genericptr_t) mesg);
841 initd = FALSE; /* reset */