Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | pmbaty | 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 "compiler-poison.h" |
||
| 38 | |||
| 39 | // Cursor shown if we are in insert mode |
||
| 40 | #define CLI_INS_CURSOR "_" |
||
| 41 | // Cursor shown if we are in overwrite mode |
||
| 42 | #define CLI_OVR_CURSOR "|" |
||
| 43 | |||
| 44 | namespace { |
||
| 45 | |||
| 46 | class CLIState |
||
| 47 | { |
||
| 48 | static const char g_prompt_mode_cmd = ']'; |
||
| 49 | static const char g_prompt_strings[]; |
||
| 50 | /* When drawing an underscore as a cursor indicator, shift it down by |
||
| 51 | * this many pixels, to make it easier to see when the underlined |
||
| 52 | * character is itself an underscore. |
||
| 53 | */ |
||
| 54 | static const unsigned m_cursor_underline_y_shift = 3; |
||
| 55 | static const unsigned m_maximum_history_lines = 100; |
||
| 56 | unsigned m_history_position, m_line_position; |
||
| 57 | std::string m_line; |
||
| 58 | std::deque<std::string> m_lines; |
||
| 59 | CLI_insert_type m_insert_type; |
||
| 60 | void history_move(unsigned position); |
||
| 61 | public: |
||
| 62 | void init(); |
||
| 63 | unsigned draw(unsigned, unsigned); |
||
| 64 | void execute_active_line(); |
||
| 65 | void insert_completion(); |
||
| 66 | void cursor_left(); |
||
| 67 | void cursor_right(); |
||
| 68 | void cursor_home(); |
||
| 69 | void cursor_end(); |
||
| 70 | void cursor_del(); |
||
| 71 | void cursor_backspace(); |
||
| 72 | void add_character(char c); |
||
| 73 | void clear_active_line(); |
||
| 74 | void history_prev(); |
||
| 75 | void history_next(); |
||
| 76 | void toggle_overwrite_mode(); |
||
| 77 | }; |
||
| 78 | |||
| 79 | const char CLIState::g_prompt_strings[] = { |
||
| 80 | g_prompt_mode_cmd, |
||
| 81 | }; |
||
| 82 | |||
| 83 | } |
||
| 84 | |||
| 85 | static CLIState g_cli; |
||
| 86 | |||
| 87 | /* Initializes the cli */ |
||
| 88 | void cli_init() |
||
| 89 | { |
||
| 90 | g_cli.init(); |
||
| 91 | } |
||
| 92 | |||
| 93 | /* Draws the command line the user is typing in to the screen */ |
||
| 94 | unsigned cli_draw(unsigned y, unsigned line_spacing) |
||
| 95 | { |
||
| 96 | return g_cli.draw(y, line_spacing); |
||
| 97 | } |
||
| 98 | |||
| 99 | /* Executes the command entered */ |
||
| 100 | void cli_execute() |
||
| 101 | { |
||
| 102 | g_cli.execute_active_line(); |
||
| 103 | } |
||
| 104 | |||
| 105 | void cli_autocomplete(void) |
||
| 106 | { |
||
| 107 | g_cli.insert_completion(); |
||
| 108 | } |
||
| 109 | |||
| 110 | void cli_cursor_left() |
||
| 111 | { |
||
| 112 | g_cli.cursor_left(); |
||
| 113 | } |
||
| 114 | |||
| 115 | void cli_cursor_right() |
||
| 116 | { |
||
| 117 | g_cli.cursor_right(); |
||
| 118 | } |
||
| 119 | |||
| 120 | void cli_cursor_home() |
||
| 121 | { |
||
| 122 | g_cli.cursor_home(); |
||
| 123 | } |
||
| 124 | |||
| 125 | void cli_cursor_end() |
||
| 126 | { |
||
| 127 | g_cli.cursor_end(); |
||
| 128 | } |
||
| 129 | |||
| 130 | void cli_cursor_del() |
||
| 131 | { |
||
| 132 | g_cli.cursor_del(); |
||
| 133 | } |
||
| 134 | |||
| 135 | void cli_cursor_backspace() |
||
| 136 | { |
||
| 137 | g_cli.cursor_backspace(); |
||
| 138 | } |
||
| 139 | |||
| 140 | void cli_add_character(char character) |
||
| 141 | { |
||
| 142 | g_cli.add_character(character); |
||
| 143 | } |
||
| 144 | |||
| 145 | void cli_clear() |
||
| 146 | { |
||
| 147 | g_cli.clear_active_line(); |
||
| 148 | } |
||
| 149 | |||
| 150 | void cli_history_prev() |
||
| 151 | { |
||
| 152 | g_cli.history_prev(); |
||
| 153 | } |
||
| 154 | |||
| 155 | void cli_history_next() |
||
| 156 | { |
||
| 157 | g_cli.history_next(); |
||
| 158 | } |
||
| 159 | |||
| 160 | void cli_toggle_overwrite_mode() |
||
| 161 | { |
||
| 162 | g_cli.toggle_overwrite_mode(); |
||
| 163 | } |
||
| 164 | |||
| 165 | void CLIState::init() |
||
| 166 | { |
||
| 167 | m_lines.emplace_front(); |
||
| 168 | } |
||
| 169 | |||
| 170 | unsigned CLIState::draw(unsigned y, unsigned line_spacing) |
||
| 171 | { |
||
| 172 | using wrap_result = std::pair<const char *, unsigned>; |
||
| 173 | /* At most this many lines of wrapped input can be shown at once. |
||
| 174 | * Any excess lines will be hidden. |
||
| 175 | * |
||
| 176 | * Use a power of 2 to make the modulus optimize into a fast masking |
||
| 177 | * operation. |
||
| 178 | * |
||
| 179 | * Zero-initialize for safety, but also mark it as initially |
||
| 180 | * undefined for Valgrind. Assuming no bugs, any element of wraps[] |
||
| 181 | * accessed by the second loop will have been initialized by the |
||
| 182 | * first loop. |
||
| 183 | */ |
||
| 184 | std::array<wrap_result, 8> wraps{}; |
||
| 185 | DXX_MAKE_VAR_UNDEFINED(wraps); |
||
| 186 | const auto margin_width = FSPACX(1); |
||
| 187 | const char prompt_string[2] = {g_prompt_strings[0], 0}; |
||
| 188 | int prompt_width, h; |
||
| 189 | gr_get_string_size(*grd_curcanv->cv_font, prompt_string, &prompt_width, &h, nullptr); |
||
| 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 | int cursor_width, cursor_height; |
||
| 228 | gr_get_string_size(*grd_curcanv->cv_font, cursor_string, &cursor_width, &cursor_height, nullptr); |
||
| 229 | if (line_position == m_line.size()) |
||
| 230 | { |
||
| 231 | const auto &w = wraps[last_wrap_line % wraps.size()]; |
||
| 232 | if (cursor_width + line_left + w.second > max_pixels_per_line) |
||
| 233 | { |
||
| 234 | auto &w2 = wraps[++last_wrap_line % wraps.size()]; |
||
| 235 | w2 = {w.first, 0}; |
||
| 236 | assert(!*w2.first); |
||
| 237 | } |
||
| 238 | cursor_line = last_wrap_line; |
||
| 239 | } |
||
| 240 | for (unsigned i = std::min(last_wrap_line + 1, wraps.size());; --last_wrap_line) |
||
| 241 | { |
||
| 242 | const auto &w = wraps[last_wrap_line % wraps.size()]; |
||
| 243 | const auto p = w.first; |
||
| 244 | if (!p) |
||
| 245 | { |
||
| 246 | assert(p); |
||
| 247 | break; |
||
| 248 | } |
||
| 249 | std::string::const_pointer q; |
||
| 250 | if (last_wrap_line) |
||
| 251 | { |
||
| 252 | q = wraps[(last_wrap_line - 1) % wraps.size()].first; |
||
| 253 | if (!q) |
||
| 254 | { |
||
| 255 | assert(q); |
||
| 256 | break; |
||
| 257 | } |
||
| 258 | } |
||
| 259 | else |
||
| 260 | q = line_begin; |
||
| 261 | std::string::pointer mc; |
||
| 262 | std::string::value_type c; |
||
| 263 | /* If the parsing loop exited by the cursor_line test, then this |
||
| 264 | * test is true on every pass through this loop. |
||
| 265 | * |
||
| 266 | * If the parsing loop exited by !*p, then this test is false on |
||
| 267 | * the first pass through this loop and true on every other |
||
| 268 | * pass. |
||
| 269 | * |
||
| 270 | * If the input text requires only one vertical line, then the |
||
| 271 | * parsing loop will have exited through the !*p test and this |
||
| 272 | * loop will only run iteration. |
||
| 273 | */ |
||
| 274 | if (*p) |
||
| 275 | { |
||
| 276 | /* Temporarily write a null into the text string for the |
||
| 277 | * benefit of null-terminator based code in the gr_string* |
||
| 278 | * functions. The original character is saved in `c` and |
||
| 279 | * will be restored later. |
||
| 280 | */ |
||
| 281 | mc = &m_line[p - q]; |
||
| 282 | c = *mc; |
||
| 283 | *mc = 0; |
||
| 284 | } |
||
| 285 | else |
||
| 286 | { |
||
| 287 | /* No need to write to the std::string because a |
||
| 288 | * null-terminator is already present. |
||
| 289 | */ |
||
| 290 | mc = nullptr; |
||
| 291 | c = 0; |
||
| 292 | } |
||
| 293 | gr_string(*grd_curcanv, *grd_curcanv->cv_font, line_left, y, q, w.second, h); |
||
| 294 | if (--i == cursor_line) |
||
| 295 | { |
||
| 296 | unsigned cx = line_left + w.second, cy = y; |
||
| 297 | if (m_insert_type == CLI_insert_type::insert) |
||
| 298 | cy += m_cursor_underline_y_shift; |
||
| 299 | if (line_position != p - line_begin) |
||
| 300 | { |
||
| 301 | int cw; |
||
| 302 | gr_get_string_size(*grd_curcanv->cv_font, &line_begin[line_position], &cw, nullptr, nullptr); |
||
| 303 | cx -= cw; |
||
| 304 | } |
||
| 305 | gr_string(*grd_curcanv, *grd_curcanv->cv_font, cx, cy, cursor_string, cursor_width, cursor_height); |
||
| 306 | } |
||
| 307 | /* Restore the original character, if one was overwritten. */ |
||
| 308 | if (mc) |
||
| 309 | *mc = c; |
||
| 310 | if (!i) |
||
| 311 | break; |
||
| 312 | y -= h; |
||
| 313 | } |
||
| 314 | gr_string(*grd_curcanv, *grd_curcanv->cv_font, margin_width, y, prompt_string, prompt_width, h); |
||
| 315 | return y; |
||
| 316 | } |
||
| 317 | |||
| 318 | void CLIState::execute_active_line() |
||
| 319 | { |
||
| 320 | if (m_line.empty()) |
||
| 321 | return; |
||
| 322 | const char *p = m_line.c_str(); |
||
| 323 | con_printf(CON_NORMAL, "con%c%s", g_prompt_strings[0], p); |
||
| 324 | cmd_append(p); |
||
| 325 | m_lines[0] = move(m_line); |
||
| 326 | m_lines.emplace_front(); |
||
| 327 | m_history_position = 0; |
||
| 328 | if (m_lines.size() > m_maximum_history_lines) |
||
| 329 | m_lines.pop_back(); |
||
| 330 | clear_active_line(); |
||
| 331 | } |
||
| 332 | |||
| 333 | void CLIState::insert_completion() |
||
| 334 | { |
||
| 335 | const auto suggestion = cmd_complete(m_line.c_str()); |
||
| 336 | if (!suggestion) |
||
| 337 | return; |
||
| 338 | m_line = suggestion; |
||
| 339 | m_line += " "; |
||
| 340 | m_line_position = m_line.size(); |
||
| 341 | } |
||
| 342 | |||
| 343 | void CLIState::cursor_left() |
||
| 344 | { |
||
| 345 | if (m_line_position > 0) |
||
| 346 | -- m_line_position; |
||
| 347 | } |
||
| 348 | |||
| 349 | void CLIState::cursor_right() |
||
| 350 | { |
||
| 351 | if (m_line_position < m_line.size()) |
||
| 352 | ++ m_line_position; |
||
| 353 | } |
||
| 354 | |||
| 355 | void CLIState::cursor_home() |
||
| 356 | { |
||
| 357 | m_line_position = 0; |
||
| 358 | } |
||
| 359 | |||
| 360 | void CLIState::cursor_end() |
||
| 361 | { |
||
| 362 | m_line_position = m_line.size(); |
||
| 363 | } |
||
| 364 | |||
| 365 | void CLIState::cursor_del() |
||
| 366 | { |
||
| 367 | const auto l = m_line_position; |
||
| 368 | if (l >= m_line.size()) |
||
| 369 | return; |
||
| 370 | m_line.erase(next(m_line.begin(), l)); |
||
| 371 | } |
||
| 372 | |||
| 373 | void CLIState::cursor_backspace() |
||
| 374 | { |
||
| 375 | if (m_line_position <= 0) |
||
| 376 | return; |
||
| 377 | m_line.erase(next(m_line.begin(), --m_line_position)); |
||
| 378 | } |
||
| 379 | |||
| 380 | void CLIState::add_character(char c) |
||
| 381 | { |
||
| 382 | if (m_insert_type == CLI_insert_type::overwrite && m_line_position < m_line.size()) |
||
| 383 | m_line[m_line_position] = c; |
||
| 384 | else |
||
| 385 | m_line.insert(next(m_line.begin(), m_line_position), c); |
||
| 386 | ++m_line_position; |
||
| 387 | } |
||
| 388 | |||
| 389 | void CLIState::clear_active_line() |
||
| 390 | { |
||
| 391 | m_line_position = 0; |
||
| 392 | m_line.clear(); |
||
| 393 | } |
||
| 394 | |||
| 395 | void CLIState::history_move(unsigned position) |
||
| 396 | { |
||
| 397 | if (position >= m_lines.size()) |
||
| 398 | return; |
||
| 399 | m_lines[m_history_position] = move(m_line); |
||
| 400 | auto &l = m_lines[m_history_position = position]; |
||
| 401 | m_line_position = l.size(); |
||
| 402 | m_line = l; |
||
| 403 | } |
||
| 404 | |||
| 405 | void CLIState::history_prev() |
||
| 406 | { |
||
| 407 | history_move(m_history_position + 1); |
||
| 408 | } |
||
| 409 | |||
| 410 | void CLIState::history_next() |
||
| 411 | { |
||
| 412 | const auto max_lines = m_lines.size(); |
||
| 413 | if (m_history_position > max_lines) |
||
| 414 | m_history_position = max_lines; |
||
| 415 | history_move(m_history_position - 1); |
||
| 416 | } |
||
| 417 | |||
| 418 | void CLIState::toggle_overwrite_mode() |
||
| 419 | { |
||
| 420 | m_insert_type = m_insert_type == CLI_insert_type::insert |
||
| 421 | ? CLI_insert_type::overwrite |
||
| 422 | : CLI_insert_type::insert; |
||
| 423 | } |