cfad47cfa3/src/frobtadsappcurses.cc

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
/* FrobTadsApplicationCurses class implementation.
2
 */
3
#include "common.h"
4
5
#include <string.h>
6
#if HAVE_TIOCGWINSZ || HAVE_TIOCGSIZE
7
# if (!defined(GWINSZ_IN_SYS_IOCTL) || HAVE_TIOCGSIZE) && HAVE_TERMIOS_H
8
#  include <termios.h>
9
# endif
10
# if HAVE_SYS_IOCTL_H
11
#  include <sys/ioctl.h>
12
# endif
13
#endif
14
15
#include "frobtadsappcurses.h"
16
#include "colors.h"
17
18
19
/* Service routine.  Gets the terminal's size.
20
 * On success, stores the terminal dimensions in 'width' and
21
 * 'height' and returns true.  On failure, returns false and
22
 * 'width' nor 'height' are modified.
23
 */
24
static bool
25
getTermSize( unsigned& width, unsigned& height )
26
{
27
	// If the system has support for the TIOCGWINSZ or TIOCGSIZE
28
	// ioctl, use it to obtain the terminal's dimensions.
29
	int res = -1;
30
	unsigned w = 0;
31
	unsigned h = 0;
32
#if HAVE_TIOCGWINSZ
33
	winsize size;
34
	size.ws_col = size.ws_row = 0;
35
	res = ioctl(0, TIOCGWINSZ, &size);
36
	w = size.ws_col;
37
	h = size.ws_row;
38
#elif HAVE_TIOCGSIZE
39
	ttysize size;
40
	size.ts_cols = size.ts_lines = 0;
41
	res = ioctl(0, TIOCGSIZE, &size);
42
	w = size.ts_cols;
43
	h = size.ts_lines;
44
#endif
45
	if (res < 0 or w == 0 or h == 0) {
46
		// The call failed or the system doesn't seem to know
47
		// the terminal's size.
48
		return false;
49
	}
50
	width = w;
51
	height = h;
52
	return true;
53
}
54
55
56
FrobTadsApplicationCurses::FrobTadsApplicationCurses( const FrobOptions& opts )
57
: FrobTadsApplication(opts), fGameWindow(0), fDispBuf(0)
58
{
59
	// Before starting curses, we set the LINES and COLUMNS env.
60
	// variables to a "maximum" value.  This is needed in case we're
61
	// running on a system with an old/broken curses implementation
62
	// (Solaris curses, for example).  This forces curses to
63
	// allocate enough internal memory for the windows in case the
64
	// terminal gets resized.  Note that we only do this if
65
	// detection of the terminal size is possible at all.
66
#if HAVE_PUTENV
67
	unsigned bogusX, bogusY; // Dummies.
68
	if (getTermSize(bogusX, bogusY)) {
69
		// The strings become part of the environment; they have to be
70
		// static.
71
		static char lines[] = "LINES=225";
72
		static char columns[] = "COLUMNS=300";
73
		putenv(lines);
74
		putenv(columns);
75
	}
76
#endif
77
78
	// Fire-up curses.
79
	initscr();
80
81
	// Make characters typed by the user immediately available to
82
	// the program.
83
	cbreak();
84
85
	// Tell courses not to echo user-input (TADS does the echoing).
86
	noecho();
87
88
	// Hide the cursor.  This avoids the "crazy cursor jumping"
89
	// problem on *really* slow terminals, as TADS constantly moves
90
	// the cursor all over the place.
91
	curs_set(0);
92
93
	// Tell courses not to try to translate the RETURN/ENTER key on
94
	// input (or else getch() and friends would never report that
95
	// key), and the '\n' character on output (since the osgen layer
96
	// handles newlines).
97
	nonl();
98
99
	// Initialize colors if we want color support and the terminal supports it.
100
	if ((opts.useColors and has_colors()) or opts.forceColors) {
101
		this->fColorsEnabled = true;
102
		start_color();
103
		// Assign terminal default foreground/background colors
104
		// to color number -1 / color pair 0.
105
#ifdef HAVE_USE_DEFAULT_COLORS
106
		if (opts.defColors) {
107
			use_default_colors();
108
		}
109
#endif
110
		// Initialize the curses color-pairs we use.
111
		//
112
		// This is paranoia, but better to make sure that curses
113
		// will use the right colors.  The paranoid part of this
114
		// is that we assume that there might be some curses
115
		// version out there that does not use monotonically
116
		// increasing values for the COLOR_* symbols.
117
		init_pair(makeColorPair(FROB_BLACK,   FROB_BLACK), COLOR_BLACK,   COLOR_BLACK);
118
		init_pair(makeColorPair(FROB_RED,     FROB_BLACK), COLOR_RED,     COLOR_BLACK);
119
		init_pair(makeColorPair(FROB_GREEN,   FROB_BLACK), COLOR_GREEN,   COLOR_BLACK);
120
		init_pair(makeColorPair(FROB_YELLOW,  FROB_BLACK), COLOR_YELLOW,  COLOR_BLACK);
121
		init_pair(makeColorPair(FROB_BLUE,    FROB_BLACK), COLOR_BLUE,    COLOR_BLACK);
122
		init_pair(makeColorPair(FROB_MAGENTA, FROB_BLACK), COLOR_MAGENTA, COLOR_BLACK);
123
		init_pair(makeColorPair(FROB_CYAN,    FROB_BLACK), COLOR_CYAN,    COLOR_BLACK);
124
		// White on black is wired to color pair 0 by
125
		// definition; we can't and don't need to initialize it.
126
		//init_pair(makeColorPair(FROB_WHITE,   FROB_BLACK), COLOR_WHITE,   COLOR_BLACK);
127
128
		init_pair(makeColorPair(FROB_BLACK,   FROB_RED), COLOR_BLACK,   COLOR_RED);
129
		init_pair(makeColorPair(FROB_RED,     FROB_RED), COLOR_RED,     COLOR_RED);
130
		init_pair(makeColorPair(FROB_GREEN,   FROB_RED), COLOR_GREEN,   COLOR_RED);
131
		init_pair(makeColorPair(FROB_YELLOW,  FROB_RED), COLOR_YELLOW,  COLOR_RED);
132
		init_pair(makeColorPair(FROB_BLUE,    FROB_RED), COLOR_BLUE,    COLOR_RED);
133
		init_pair(makeColorPair(FROB_MAGENTA, FROB_RED), COLOR_MAGENTA, COLOR_RED);
134
		init_pair(makeColorPair(FROB_CYAN,    FROB_RED), COLOR_CYAN,    COLOR_RED);
135
		init_pair(makeColorPair(FROB_WHITE,   FROB_RED), COLOR_WHITE,   COLOR_RED);
136
137
		init_pair(makeColorPair(FROB_BLACK,   FROB_GREEN), COLOR_BLACK,   COLOR_GREEN);
138
		init_pair(makeColorPair(FROB_RED,     FROB_GREEN), COLOR_RED,     COLOR_GREEN);
139
		init_pair(makeColorPair(FROB_GREEN,   FROB_GREEN), COLOR_GREEN,   COLOR_GREEN);
140
		init_pair(makeColorPair(FROB_YELLOW,  FROB_GREEN), COLOR_YELLOW,  COLOR_GREEN);
141
		init_pair(makeColorPair(FROB_BLUE,    FROB_GREEN), COLOR_BLUE,    COLOR_GREEN);
142
		init_pair(makeColorPair(FROB_MAGENTA, FROB_GREEN), COLOR_MAGENTA, COLOR_GREEN);
143
		init_pair(makeColorPair(FROB_CYAN,    FROB_GREEN), COLOR_CYAN,    COLOR_GREEN);
144
		init_pair(makeColorPair(FROB_WHITE,   FROB_GREEN), COLOR_WHITE,   COLOR_GREEN);
145
146
		init_pair(makeColorPair(FROB_BLACK,   FROB_YELLOW), COLOR_BLACK,   COLOR_YELLOW);
147
		init_pair(makeColorPair(FROB_RED,     FROB_YELLOW), COLOR_RED,     COLOR_YELLOW);
148
		init_pair(makeColorPair(FROB_GREEN,   FROB_YELLOW), COLOR_GREEN,   COLOR_YELLOW);
149
		init_pair(makeColorPair(FROB_YELLOW,  FROB_YELLOW), COLOR_YELLOW,  COLOR_YELLOW);
150
		init_pair(makeColorPair(FROB_BLUE,    FROB_YELLOW), COLOR_BLUE,    COLOR_YELLOW);
151
		init_pair(makeColorPair(FROB_MAGENTA, FROB_YELLOW), COLOR_MAGENTA, COLOR_YELLOW);
152
		init_pair(makeColorPair(FROB_CYAN,    FROB_YELLOW), COLOR_CYAN,    COLOR_YELLOW);
153
		init_pair(makeColorPair(FROB_WHITE,   FROB_YELLOW), COLOR_WHITE,   COLOR_YELLOW);
154
155
		init_pair(makeColorPair(FROB_BLACK,   FROB_BLUE), COLOR_BLACK,   COLOR_BLUE);
156
		init_pair(makeColorPair(FROB_RED,     FROB_BLUE), COLOR_RED,     COLOR_BLUE);
157
		init_pair(makeColorPair(FROB_GREEN,   FROB_BLUE), COLOR_GREEN,   COLOR_BLUE);
158
		init_pair(makeColorPair(FROB_YELLOW,  FROB_BLUE), COLOR_YELLOW,  COLOR_BLUE);
159
		init_pair(makeColorPair(FROB_BLUE,    FROB_BLUE), COLOR_BLUE,    COLOR_BLUE);
160
		init_pair(makeColorPair(FROB_MAGENTA, FROB_BLUE), COLOR_MAGENTA, COLOR_BLUE);
161
		init_pair(makeColorPair(FROB_CYAN,    FROB_BLUE), COLOR_CYAN,    COLOR_BLUE);
162
		init_pair(makeColorPair(FROB_WHITE,   FROB_BLUE), COLOR_WHITE,   COLOR_BLUE);
163
164
		init_pair(makeColorPair(FROB_BLACK,   FROB_MAGENTA), COLOR_BLACK,   COLOR_MAGENTA);
165
		init_pair(makeColorPair(FROB_RED,     FROB_MAGENTA), COLOR_RED,     COLOR_MAGENTA);
166
		init_pair(makeColorPair(FROB_GREEN,   FROB_MAGENTA), COLOR_GREEN,   COLOR_MAGENTA);
167
		init_pair(makeColorPair(FROB_YELLOW,  FROB_MAGENTA), COLOR_YELLOW,  COLOR_MAGENTA);
168
		init_pair(makeColorPair(FROB_BLUE,    FROB_MAGENTA), COLOR_BLUE,    COLOR_MAGENTA);
169
		init_pair(makeColorPair(FROB_MAGENTA, FROB_MAGENTA), COLOR_MAGENTA, COLOR_MAGENTA);
170
		init_pair(makeColorPair(FROB_CYAN,    FROB_MAGENTA), COLOR_CYAN,    COLOR_MAGENTA);
171
		init_pair(makeColorPair(FROB_WHITE,   FROB_MAGENTA), COLOR_WHITE,   COLOR_MAGENTA);
172
173
		init_pair(makeColorPair(FROB_BLACK,   FROB_CYAN), COLOR_BLACK,   COLOR_CYAN);
174
		init_pair(makeColorPair(FROB_RED,     FROB_CYAN), COLOR_RED,     COLOR_CYAN);
175
		init_pair(makeColorPair(FROB_GREEN,   FROB_CYAN), COLOR_GREEN,   COLOR_CYAN);
176
		init_pair(makeColorPair(FROB_YELLOW,  FROB_CYAN), COLOR_YELLOW,  COLOR_CYAN);
177
		init_pair(makeColorPair(FROB_BLUE,    FROB_CYAN), COLOR_BLUE,    COLOR_CYAN);
178
		init_pair(makeColorPair(FROB_MAGENTA, FROB_CYAN), COLOR_MAGENTA, COLOR_CYAN);
179
		init_pair(makeColorPair(FROB_CYAN,    FROB_CYAN), COLOR_CYAN,    COLOR_CYAN);
180
		init_pair(makeColorPair(FROB_WHITE,   FROB_CYAN), COLOR_WHITE,   COLOR_CYAN);
181
182
		init_pair(makeColorPair(FROB_BLACK,   FROB_WHITE), COLOR_BLACK,   COLOR_WHITE);
183
		init_pair(makeColorPair(FROB_RED,     FROB_WHITE), COLOR_RED,     COLOR_WHITE);
184
		init_pair(makeColorPair(FROB_GREEN,   FROB_WHITE), COLOR_GREEN,   COLOR_WHITE);
185
		init_pair(makeColorPair(FROB_YELLOW,  FROB_WHITE), COLOR_YELLOW,  COLOR_WHITE);
186
		init_pair(makeColorPair(FROB_BLUE,    FROB_WHITE), COLOR_BLUE,    COLOR_WHITE);
187
		init_pair(makeColorPair(FROB_MAGENTA, FROB_WHITE), COLOR_MAGENTA, COLOR_WHITE);
188
		init_pair(makeColorPair(FROB_CYAN,    FROB_WHITE), COLOR_CYAN,    COLOR_WHITE);
189
		init_pair(makeColorPair(FROB_WHITE,   FROB_WHITE), COLOR_WHITE,   COLOR_WHITE);
190
	}
191
}
192
193
194
FrobTadsApplicationCurses::~FrobTadsApplicationCurses()
195
{
196
	// Enable the cursor.
197
	curs_set(0);
198
199
	// Flush any pending output so we won't lose any "thanks for
200
	// playing" messages when "pausing prior to exit" is disabled.
201
	if (not this->options.exitPause) this->fGameWindow->flush();
202
203
	// Delete our windows, if we have any.
204
	if (this->fGameWindow != 0) delete this->fGameWindow;
205
206
	// Shut down curses.
207
	endwin();
208
209
	// Delete our optimization buffer.
210
	delete[] this->fDispBuf;
211
}
212
213
214
void
215
FrobTadsApplicationCurses::init()
216
{
217
	// Delete the current window, if we have one.
218
	if (this->fGameWindow != 0) delete this->fGameWindow;
219
220
	// Create the main window.  Make it fill the whole terminal.  We
221
	// initialize the dimensions to 0 in case getTermSize() is
222
	// unable to detect the actual size, since in curses/ncurses 0
223
	// for width and height actually means "fullscreen".
224
	unsigned width = 0;
225
	unsigned height = 0;
226
	bool sizeDetected = getTermSize(width, height);
227
	if (sizeDetected) {
228
		// Update the environment in case we're running inside
229
		// an old/broken terminal or are using an old/broken
230
		// version of curses.
231
#if HAVE_PUTENV
232
		// Note that the strings have to be static, since
233
		// putenv() does not always copy the string arguments
234
		// but points the environment to the application's
235
		// address space (and therefore the application would
236
		// segfault if the strings were not static).
237
		static char linesEnv[32];
238
		static char columnsEnv[32];
239
		char tmp[16];
240
		strcpy(linesEnv, "LINES=");
241
		strcpy(columnsEnv, "COLUMNS=");
242
		sprintf(tmp, "%u", height);
243
		strcat(linesEnv, tmp);
244
		sprintf(tmp, "%u", width);
245
		strcat(columnsEnv, tmp);
246
		// Normally, since the strings are part of the
247
		// environment, we would need to use putenv() only once;
248
		// changing the strings later would also change the
249
		// environment, since the strings are part of it.
250
		// However, not every system follows the standard, so we
251
		// need to putenv() the strings over and over again.
252
		putenv(linesEnv);
253
		putenv(columnsEnv);
254
		// Enforce the detected size upon curses.
255
		// TODO: Is this needed?  Or even allowed?  For now, we
256
		// assume it's neither.
257
		//COLS = width;
258
		//LINES = height;
259
#endif
260
	}
261
262
	// Reset curses, in case the terminal's size has changed.
263
	endwin();
264
	refresh();
265
	// Fixes problems with old curses versions.
266
	wclear(stdscr);
267
268
	// Create the window.
269
	this->fGameWindow = new FrobTadsWindow(height, width, 0, 0);
270
271
	// Disable scrolling.
272
	this->fGameWindow->enableScrolling(false);
273
274
	// Tell the window to recognize keypad keys (function keys,
275
	// cursor-keys, etc.) during input.
276
	this->fGameWindow->keypadMode(true);
277
278
	// Enable 8-bit characters during input.
279
	this->fGameWindow->input8bit(true);
280
281
	// Create the buffer that we use to optimize output.
282
	if (this->fDispBuf != 0) delete[] this->fDispBuf;
283
	this->fDispBuf = new chtype[this->fGameWindow->width() + 1];
284
285
	// Explicitly mark the window as touched, to work around a
286
	// curses color and screen corruption issue.
287
	this->fGameWindow->touch();
288
289
	// Fix for curses color corruption problem.  The window is
290
	// cleared to a color (other than black and white) which contrasts
291
	// with the normal background color.
292
	if (sizeDetected) {
293
		int fillcolor;
294
		if (this->options.bgColor == FROB_BLUE) {
295
			fillcolor = ossgetcolor(OSGEN_COLOR_WHITE, OSGEN_COLOR_RED, 0, 0);
296
		} else {
297
			fillcolor = ossgetcolor(OSGEN_COLOR_WHITE, OSGEN_COLOR_BLUE, 0, 0);
298
		}
299
		ossclr(0, 0, height - 1, width - 1, fillcolor);
300
		this->fGameWindow->flush();
301
	}
302
}
303
304
305
void
306
FrobTadsApplicationCurses::clear( int top, int left, int bottom, int right, int attrs )
307
{
308
	const chtype c = attrs | ' ';
309
	for (int y = top; y <= bottom; ++y) {
310
		for (int x = left; x <= right; ++x) {
311
			this->fGameWindow->printChar(y, x, c);
312
		}
313
	}
314
}
315
316
317
void
318
FrobTadsApplicationCurses::scrollRegionUp( int top, int left, int bottom, int right, int attrs )
319
{
320
	// Old curses versions might break when using overlapping
321
	// windows, so we do the scrolling by hand.
322
	for (int y = bottom; y > top; --y) {
323
		for (int x = left; x <= right; ++x) {
324
			const chtype c = this->fGameWindow->charAt(y-1, x);
325
			this->fGameWindow->printChar(y, x, c);
326
		}
327
	}
328
329
	// Clear the last line.
330
	const chtype c = attrs | ' ';
331
	for (int i = left; i <= right; ++i) this->fGameWindow->printChar(top, i, c);
332
333
	// If soft-scrolling is enabled, update the display so that we
334
	// have a visible scrolling-effect.
335
	if (this->options.softScroll) this->fGameWindow->flush();
336
}
337
338
339
void
340
FrobTadsApplicationCurses::scrollRegionDown( int top, int left, int bottom, int right, int attrs )
341
{
342
	// This works like scrollRegionUp(), just the other way around.
343
	for (int y = top; y < bottom; ++y) {
344
		for (int x = left; x <= right; ++x) {
345
			this->fGameWindow->printChar(y, x, this->fGameWindow->charAt(y+1, x));
346
		}
347
	}
348
	const chtype c = attrs | ' ';
349
	for (int i = left; i <= right; ++i) this->fGameWindow->printChar(bottom, i, c);
350
	if (this->options.softScroll) this->fGameWindow->flush();
351
}
352
353
354
int
355
FrobTadsApplicationCurses::getRawChar( bool cursorVisible, int timeout )
356
{
357
	// The character we read.
358
	int c;
359
360
	// Tell osgen to check if it should redraw the screen.
361
	osssb_redraw_if_needed();
362
363
	// Some curses versions don't automatically update the display
364
	// prior to input, so we must explicitly request it.
365
	this->fGameWindow->flush();
366
367
	// Prepare for timed input.
368
	this->fGameWindow->setTimeout(timeout);
369
	this->fRemainingTimeout = timeout;
370
371
	// Enable the cursor, if needed.  We won't enable it if the
372
	// timeout is very small, even if the caller requested it.
373
	// "Small" means under 1 second, which is what most terminals
374
	// use for a cursor blink.
375
	if (cursorVisible and (timeout > 1000 or timeout < 1)) curs_set(1);
376
377
	// On some systems, ncurses is configured to supply its own
378
	// SIGWINCH handler.  If this is the case, getch() returns
379
	// KEY_RESIZE when the terminal has been resized.  We can't rely
380
	// on this behavior to handle screen resizes, but we must still
381
	// recognize this return code; here we simply ignore it.  Note
382
	// that curses doesn't know anything about KEY_RESIZE; it's an
383
	// ncurses thing.  We only check for it if it is defined.
384
#ifdef KEY_RESIZE
385
	while ((c = this->fGameWindow->getChar()) == KEY_RESIZE)
386
		;
387
#else
388
	c = this->fGameWindow->getChar();
389
#endif
390
	this->fRemainingTimeout = 0;
391
392
	// Some terminals don't return KEY_BACKSPACE for the
393
	// backspace-key, but some opaque platform-specific code.  If
394
	// this is the case, map it to KEY_BACKSPACE.
395
	if (c == erasechar()) c = KEY_BACKSPACE;
396
397
	// Similarly for KEY_DL.
398
	if (c == killchar()) c = KEY_DL;
399
400
	curs_set(0);
401
	return c;
402
}