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