1 /*
2  * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
3  * It is copyright by its individual contributors, as recorded in the
4  * project's Git history.  See COPYING.txt at the top level for license
5  * terms and a link to the Git history.
6  *
7  * --
8  *  Based on an early version of SDL_Console
9  *  Written By: Garrett Banuk <mongoose@mongeese.org>
10  *  Code Cleanup and heavily extended by: Clemens Wacha <reflex-2000@gmx.net>
11  *  Ported to use native Descent interfaces by: Bradley Bell <btb@icculus.org>
12  *
13  *  This is free, just be sure to give us credit when using it
14  *  in any of your programs.
15  * --
16  *
17  * Rewritten to use C++ utilities by Kp.  Post-Bradley work is under
18  * the standard Rebirth terms, which are less permissive than the
19  * statement above.
20  */
21 /*
22  *
23  * Command-line interface for the console
24  *
25  */
26 
27 #include <algorithm>
28 #include <cassert>
29 #include <cctype>
30 #include <deque>
31 #include <string>
32 
33 #include "gr.h"
34 #include "gamefont.h"
35 #include "console.h"
36 #include "cli.h"
37 #include "cmd.h"
38 #include "compiler-poison.h"
39 
40 // Cursor shown if we are in insert mode
41 #define CLI_INS_CURSOR          "_"
42 // Cursor shown if we are in overwrite mode
43 #define CLI_OVR_CURSOR          "|"
44 
45 namespace {
46 
47 class CLIState
48 {
49 	static const char g_prompt_mode_cmd = ']';
50 	static const char g_prompt_strings[];
51 	/* When drawing an underscore as a cursor indicator, shift it down by
52 	 * this many pixels, to make it easier to see when the underlined
53 	 * character is itself an underscore.
54 	 */
55 	static const unsigned m_cursor_underline_y_shift = 3;
56 	static const unsigned m_maximum_history_lines = 100;
57 	unsigned m_history_position, m_line_position;
58 	std::string m_line;
59 	std::deque<std::string> m_lines;
60 	CLI_insert_type m_insert_type;
61 	void history_move(unsigned position);
62 public:
63 	void init();
64 	unsigned draw(unsigned, unsigned);
65 	void execute_active_line();
66 	void insert_completion();
67 	void cursor_left();
68 	void cursor_right();
69 	void cursor_home();
70 	void cursor_end();
71 	void cursor_del();
72 	void cursor_backspace();
73 	void add_character(char c);
74 	void clear_active_line();
75 	void history_prev();
76 	void history_next();
77 	void toggle_overwrite_mode();
78 };
79 
80 const char CLIState::g_prompt_strings[] = {
81 	g_prompt_mode_cmd,
82 };
83 
84 }
85 
86 static CLIState g_cli;
87 
88 /* Initializes the cli */
cli_init()89 void cli_init()
90 {
91 	g_cli.init();
92 }
93 
94 /* Draws the command line the user is typing in to the screen */
cli_draw(unsigned y,unsigned line_spacing)95 unsigned cli_draw(unsigned y, unsigned line_spacing)
96 {
97 	return g_cli.draw(y, line_spacing);
98 }
99 
100 /* Executes the command entered */
cli_execute()101 void cli_execute()
102 {
103 	g_cli.execute_active_line();
104 }
105 
cli_autocomplete(void)106 void cli_autocomplete(void)
107 {
108 	g_cli.insert_completion();
109 }
110 
cli_cursor_left()111 void cli_cursor_left()
112 {
113 	g_cli.cursor_left();
114 }
115 
cli_cursor_right()116 void cli_cursor_right()
117 {
118 	g_cli.cursor_right();
119 }
120 
cli_cursor_home()121 void cli_cursor_home()
122 {
123 	g_cli.cursor_home();
124 }
125 
cli_cursor_end()126 void cli_cursor_end()
127 {
128 	g_cli.cursor_end();
129 }
130 
cli_cursor_del()131 void cli_cursor_del()
132 {
133 	g_cli.cursor_del();
134 }
135 
cli_cursor_backspace()136 void cli_cursor_backspace()
137 {
138 	g_cli.cursor_backspace();
139 }
140 
cli_add_character(char character)141 void cli_add_character(char character)
142 {
143 	g_cli.add_character(character);
144 }
145 
cli_clear()146 void cli_clear()
147 {
148 	g_cli.clear_active_line();
149 }
150 
cli_history_prev()151 void cli_history_prev()
152 {
153 	g_cli.history_prev();
154 }
155 
cli_history_next()156 void cli_history_next()
157 {
158 	g_cli.history_next();
159 }
160 
cli_toggle_overwrite_mode()161 void cli_toggle_overwrite_mode()
162 {
163 	g_cli.toggle_overwrite_mode();
164 }
165 
init()166 void CLIState::init()
167 {
168 	m_lines.emplace_front();
169 }
170 
draw(unsigned y,unsigned line_spacing)171 unsigned CLIState::draw(unsigned y, unsigned line_spacing)
172 {
173 	using wrap_result = std::pair<const char *, unsigned>;
174 	/* At most this many lines of wrapped input can be shown at once.
175 	 * Any excess lines will be hidden.
176 	 *
177 	 * Use a power of 2 to make the modulus optimize into a fast masking
178 	 * operation.
179 	 *
180 	 * Zero-initialize for safety, but also mark it as initially
181 	 * undefined for Valgrind.  Assuming no bugs, any element of wraps[]
182 	 * accessed by the second loop will have been initialized by the
183 	 * first loop.
184 	 */
185 	std::array<wrap_result, 8> wraps{};
186 	DXX_MAKE_VAR_UNDEFINED(wraps);
187 	const auto margin_width = FSPACX(1);
188 	const char prompt_string[2] = {g_prompt_strings[0], 0};
189 	const auto &&[prompt_width, h] = gr_get_string_size(*grd_curcanv->cv_font, prompt_string);
190 	y -= line_spacing;
191 	const auto canvas_width = grd_curcanv->cv_bitmap.bm_w;
192 	const unsigned max_pixels_per_line = canvas_width - (margin_width * 2) - prompt_width;
193 	const unsigned unknown_cursor_line = ~0u;
194 	const auto line_position = m_line_position;
195 	const auto line_begin = m_line.c_str();
196 	std::size_t last_wrap_line = 0;
197 	unsigned cursor_line = unknown_cursor_line;
198 	/* Search the text and initialize wraps[] to record where line
199 	 * breaks will appear.  If the wrapped text is more than
200 	 * wraps.size() vertical lines, only the most recent wraps.size()
201 	 * lines are saved and shown.
202 	 */
203 	for (const char *p = line_begin;; ++last_wrap_line)
204 	{
205 		auto &w = wraps[last_wrap_line % wraps.size()];
206 		w = gr_get_string_wrap(*grd_curcanv->cv_font, p, max_pixels_per_line);
207 		/* Record the vertical line on which the cursor will appear as
208 		 * `cursor_line`.
209 		 */
210 		if (cursor_line == unknown_cursor_line)
211 		{
212 			const auto unseen_position = w.first - p;
213 			if (line_position < unseen_position)
214 				cursor_line = last_wrap_line;
215 		}
216 		/* If more text exists than can be shown, then stop at
217 		 * (wraps.size() / 2) lines past the cursor line.
218 		 */
219 		else if (last_wrap_line >= wraps.size() && cursor_line + (wraps.size() / 2) < last_wrap_line)
220 			break;
221 		p = w.first;
222 		if (!*p)
223 			break;
224 	}
225 	const auto line_left = margin_width + prompt_width + 1;
226 	const auto cursor_string = (m_insert_type == CLI_insert_type::insert ? CLI_INS_CURSOR : CLI_OVR_CURSOR);
227 	const auto &&[cursor_width, cursor_height] = gr_get_string_size(*grd_curcanv->cv_font, cursor_string);
228 	if (line_position == m_line.size())
229 	{
230 		const auto &w = wraps[last_wrap_line % wraps.size()];
231 		if (cursor_width + line_left + w.second > max_pixels_per_line)
232 		{
233 			auto &w2 = wraps[++last_wrap_line % wraps.size()];
234 			w2 = {w.first, 0};
235 			assert(!*w2.first);
236 		}
237 		cursor_line = last_wrap_line;
238 	}
239 	for (unsigned i = std::min(last_wrap_line + 1, wraps.size());; --last_wrap_line)
240 	{
241 		const auto &w = wraps[last_wrap_line % wraps.size()];
242 		const auto p = w.first;
243 		if (!p)
244 		{
245 			assert(p);
246 			break;
247 		}
248 		std::string::const_pointer q;
249 		if (last_wrap_line)
250 		{
251 			q = wraps[(last_wrap_line - 1) % wraps.size()].first;
252 			if (!q)
253 			{
254 				assert(q);
255 				break;
256 			}
257 		}
258 		else
259 			q = line_begin;
260 		std::string::pointer mc;
261 		std::string::value_type c;
262 		/* If the parsing loop exited by the cursor_line test, then this
263 		 * test is true on every pass through this loop.
264 		 *
265 		 * If the parsing loop exited by !*p, then this test is false on
266 		 * the first pass through this loop and true on every other
267 		 * pass.
268 		 *
269 		 * If the input text requires only one vertical line, then the
270 		 * parsing loop will have exited through the !*p test and this
271 		 * loop will only run iteration.
272 		 */
273 		if (*p)
274 		{
275 			/* Temporarily write a null into the text string for the
276 			 * benefit of null-terminator based code in the gr_string*
277 			 * functions.  The original character is saved in `c` and
278 			 * will be restored later.
279 			 */
280 			mc = &m_line[p - q];
281 			c = *mc;
282 			*mc = 0;
283 		}
284 		else
285 		{
286 			/* No need to write to the std::string because a
287 			 * null-terminator is already present.
288 			 */
289 			mc = nullptr;
290 			c = 0;
291 		}
292 		gr_string(*grd_curcanv, *grd_curcanv->cv_font, line_left, y, q, w.second, h);
293 		if (--i == cursor_line)
294 		{
295 			unsigned cx = line_left + w.second, cy = y;
296 			if (m_insert_type == CLI_insert_type::insert)
297 				cy += m_cursor_underline_y_shift;
298 			if (line_position != p - line_begin)
299 			{
300 				const auto cw = gr_get_string_size(*grd_curcanv->cv_font, &line_begin[line_position]).width;
301 				cx -= cw;
302 			}
303 			gr_string(*grd_curcanv, *grd_curcanv->cv_font, cx, cy, cursor_string, cursor_width, cursor_height);
304 		}
305 		/* Restore the original character, if one was overwritten. */
306 		if (mc)
307 			*mc = c;
308 		if (!i)
309 			break;
310 		y -= h;
311 	}
312 	gr_string(*grd_curcanv, *grd_curcanv->cv_font, margin_width, y, prompt_string, prompt_width, h);
313 	return y;
314 }
315 
execute_active_line()316 void CLIState::execute_active_line()
317 {
318 	if (m_line.empty())
319 		return;
320 	const char *p = m_line.c_str();
321 	con_printf(CON_NORMAL, "con%c%s", g_prompt_strings[0], p);
322 	cmd_append(p);
323 	m_lines[0] = move(m_line);
324 	m_lines.emplace_front();
325 	m_history_position = 0;
326 	if (m_lines.size() > m_maximum_history_lines)
327 		m_lines.pop_back();
328 	clear_active_line();
329 }
330 
insert_completion()331 void CLIState::insert_completion()
332 {
333 	const auto suggestion = cmd_complete(m_line.c_str());
334 	if (!suggestion)
335 		return;
336 	m_line = suggestion;
337 	m_line += " ";
338 	m_line_position = m_line.size();
339 }
340 
cursor_left()341 void CLIState::cursor_left()
342 {
343 	if (m_line_position > 0)
344 		-- m_line_position;
345 }
346 
cursor_right()347 void CLIState::cursor_right()
348 {
349 	if (m_line_position < m_line.size())
350 		++ m_line_position;
351 }
352 
cursor_home()353 void CLIState::cursor_home()
354 {
355 	m_line_position = 0;
356 }
357 
cursor_end()358 void CLIState::cursor_end()
359 {
360 	m_line_position = m_line.size();
361 }
362 
cursor_del()363 void CLIState::cursor_del()
364 {
365 	const auto l = m_line_position;
366 	if (l >= m_line.size())
367 		return;
368 	m_line.erase(next(m_line.begin(), l));
369 }
370 
cursor_backspace()371 void CLIState::cursor_backspace()
372 {
373 	if (m_line_position <= 0)
374 		return;
375 	m_line.erase(next(m_line.begin(), --m_line_position));
376 }
377 
add_character(char c)378 void CLIState::add_character(char c)
379 {
380 	if (m_insert_type == CLI_insert_type::overwrite && m_line_position < m_line.size())
381 		m_line[m_line_position] = c;
382 	else
383 		m_line.insert(next(m_line.begin(), m_line_position), c);
384 	++m_line_position;
385 }
386 
clear_active_line()387 void CLIState::clear_active_line()
388 {
389 	m_line_position = 0;
390 	m_line.clear();
391 }
392 
history_move(unsigned position)393 void CLIState::history_move(unsigned position)
394 {
395 	if (position >= m_lines.size())
396 		return;
397 	m_lines[m_history_position] = move(m_line);
398 	auto &l = m_lines[m_history_position = position];
399 	m_line_position = l.size();
400 	m_line = l;
401 }
402 
history_prev()403 void CLIState::history_prev()
404 {
405 	history_move(m_history_position + 1);
406 }
407 
history_next()408 void CLIState::history_next()
409 {
410 	const auto max_lines = m_lines.size();
411 	if (m_history_position > max_lines)
412 		m_history_position = max_lines;
413 	history_move(m_history_position - 1);
414 }
415 
toggle_overwrite_mode()416 void CLIState::toggle_overwrite_mode()
417 {
418 	m_insert_type = m_insert_type == CLI_insert_type::insert
419 		? CLI_insert_type::overwrite
420 		: CLI_insert_type::insert;
421 }
422