Subversion Repositories Games.Descent

Rev

Blame | Last modification | View Log | Download | RSS feed

  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. }
  424.