dxx-rebirth/common/main/cli.cpp
2023-04-06 16:42:49 -07:00

422 lines
9.9 KiB
C++

/*
* This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
* It is copyright by its individual contributors, as recorded in the
* project's Git history. See COPYING.txt at the top level for license
* terms and a link to the Git history.
*
* --
* Based on an early version of SDL_Console
* Written By: Garrett Banuk <mongoose@mongeese.org>
* Code Cleanup and heavily extended by: Clemens Wacha <reflex-2000@gmx.net>
* Ported to use native Descent interfaces by: Bradley Bell <btb@icculus.org>
*
* This is free, just be sure to give us credit when using it
* in any of your programs.
* --
*
* Rewritten to use C++ utilities by Kp. Post-Bradley work is under
* the standard Rebirth terms, which are less permissive than the
* statement above.
*/
/*
*
* Command-line interface for the console
*
*/
#include <algorithm>
#include <cassert>
#include <cctype>
#include <deque>
#include <string>
#include "gr.h"
#include "gamefont.h"
#include "console.h"
#include "cli.h"
#include "cmd.h"
#include "compiler-poison.h"
// Cursor shown if we are in insert mode
#define CLI_INS_CURSOR "_"
// Cursor shown if we are in overwrite mode
#define CLI_OVR_CURSOR "|"
namespace {
class CLIState
{
static const char g_prompt_mode_cmd = ']';
static const char g_prompt_strings[];
/* When drawing an underscore as a cursor indicator, shift it down by
* this many pixels, to make it easier to see when the underlined
* character is itself an underscore.
*/
static const unsigned m_cursor_underline_y_shift = 3;
static const unsigned m_maximum_history_lines = 100;
unsigned m_history_position, m_line_position;
std::string m_line;
std::deque<std::string> m_lines;
CLI_insert_type m_insert_type;
void history_move(unsigned position);
public:
void init();
unsigned draw(unsigned, unsigned);
void execute_active_line();
void insert_completion();
void cursor_left();
void cursor_right();
void cursor_home();
void cursor_end();
void cursor_del();
void cursor_backspace();
void add_character(char c);
void clear_active_line();
void history_prev();
void history_next();
void toggle_overwrite_mode();
};
const char CLIState::g_prompt_strings[] = {
g_prompt_mode_cmd,
};
static CLIState g_cli;
}
/* Initializes the cli */
void cli_init()
{
g_cli.init();
}
/* Draws the command line the user is typing in to the screen */
unsigned cli_draw(unsigned y, unsigned line_spacing)
{
return g_cli.draw(y, line_spacing);
}
/* Executes the command entered */
void cli_execute()
{
g_cli.execute_active_line();
}
void cli_autocomplete(void)
{
g_cli.insert_completion();
}
void cli_cursor_left()
{
g_cli.cursor_left();
}
void cli_cursor_right()
{
g_cli.cursor_right();
}
void cli_cursor_home()
{
g_cli.cursor_home();
}
void cli_cursor_end()
{
g_cli.cursor_end();
}
void cli_cursor_del()
{
g_cli.cursor_del();
}
void cli_cursor_backspace()
{
g_cli.cursor_backspace();
}
void cli_add_character(char character)
{
g_cli.add_character(character);
}
void cli_clear()
{
g_cli.clear_active_line();
}
void cli_history_prev()
{
g_cli.history_prev();
}
void cli_history_next()
{
g_cli.history_next();
}
void cli_toggle_overwrite_mode()
{
g_cli.toggle_overwrite_mode();
}
void CLIState::init()
{
m_lines.emplace_front();
}
unsigned CLIState::draw(unsigned y, unsigned line_spacing)
{
using wrap_result = std::pair<const char *, unsigned>;
/* At most this many lines of wrapped input can be shown at once.
* Any excess lines will be hidden.
*
* Use a power of 2 to make the modulus optimize into a fast masking
* operation.
*
* Zero-initialize for safety, but also mark it as initially
* undefined for Valgrind. Assuming no bugs, any element of wraps[]
* accessed by the second loop will have been initialized by the
* first loop.
*/
std::array<wrap_result, 8> wraps{};
DXX_MAKE_VAR_UNDEFINED(wraps);
const auto margin_width = FSPACX(1);
const char prompt_string[2] = {g_prompt_strings[0], 0};
const auto &&[prompt_width, h] = gr_get_string_size(*grd_curcanv->cv_font, prompt_string);
y -= line_spacing;
const auto canvas_width = grd_curcanv->cv_bitmap.bm_w;
const unsigned max_pixels_per_line = canvas_width - (margin_width * 2) - prompt_width;
const unsigned unknown_cursor_line = ~0u;
const auto line_position = m_line_position;
const auto line_begin = m_line.c_str();
std::size_t last_wrap_line = 0;
unsigned cursor_line = unknown_cursor_line;
/* Search the text and initialize wraps[] to record where line
* breaks will appear. If the wrapped text is more than
* wraps.size() vertical lines, only the most recent wraps.size()
* lines are saved and shown.
*/
for (const char *p = line_begin;; ++last_wrap_line)
{
auto &w = wraps[last_wrap_line % wraps.size()];
w = gr_get_string_wrap(*grd_curcanv->cv_font, p, max_pixels_per_line);
/* Record the vertical line on which the cursor will appear as
* `cursor_line`.
*/
if (cursor_line == unknown_cursor_line)
{
const auto unseen_position = w.first - p;
if (line_position < unseen_position)
cursor_line = last_wrap_line;
}
/* If more text exists than can be shown, then stop at
* (wraps.size() / 2) lines past the cursor line.
*/
else if (last_wrap_line >= wraps.size() && cursor_line + (wraps.size() / 2) < last_wrap_line)
break;
p = w.first;
if (!*p)
break;
}
const auto line_left = margin_width + prompt_width + 1;
const auto cursor_string = (m_insert_type == CLI_insert_type::insert ? CLI_INS_CURSOR : CLI_OVR_CURSOR);
const auto &&[cursor_width, cursor_height] = gr_get_string_size(*grd_curcanv->cv_font, cursor_string);
if (line_position == m_line.size())
{
const auto &w = wraps[last_wrap_line % wraps.size()];
if (cursor_width + line_left + w.second > max_pixels_per_line)
{
auto &w2 = wraps[++last_wrap_line % wraps.size()];
w2 = {w.first, 0};
assert(!*w2.first);
}
cursor_line = last_wrap_line;
}
for (unsigned i = std::min(last_wrap_line + 1, wraps.size());; --last_wrap_line)
{
const auto &w = wraps[last_wrap_line % wraps.size()];
const auto p = w.first;
if (!p)
{
assert(p);
break;
}
std::string::const_pointer q;
if (last_wrap_line)
{
q = wraps[(last_wrap_line - 1) % wraps.size()].first;
if (!q)
{
assert(q);
break;
}
}
else
q = line_begin;
std::string::pointer mc;
std::string::value_type c;
/* If the parsing loop exited by the cursor_line test, then this
* test is true on every pass through this loop.
*
* If the parsing loop exited by !*p, then this test is false on
* the first pass through this loop and true on every other
* pass.
*
* If the input text requires only one vertical line, then the
* parsing loop will have exited through the !*p test and this
* loop will only run iteration.
*/
if (*p)
{
/* Temporarily write a null into the text string for the
* benefit of null-terminator based code in the gr_string*
* functions. The original character is saved in `c` and
* will be restored later.
*/
mc = &m_line[p - q];
c = *mc;
*mc = 0;
}
else
{
/* No need to write to the std::string because a
* null-terminator is already present.
*/
mc = nullptr;
c = 0;
}
gr_string(*grd_curcanv, *grd_curcanv->cv_font, line_left, y, q, w.second, h);
if (--i == cursor_line)
{
unsigned cx = line_left + w.second, cy = y;
if (m_insert_type == CLI_insert_type::insert)
cy += m_cursor_underline_y_shift;
if (line_position != p - line_begin)
{
const auto cw = gr_get_string_size(*grd_curcanv->cv_font, &line_begin[line_position]).width;
cx -= cw;
}
gr_string(*grd_curcanv, *grd_curcanv->cv_font, cx, cy, cursor_string, cursor_width, cursor_height);
}
/* Restore the original character, if one was overwritten. */
if (mc)
*mc = c;
if (!i)
break;
y -= h;
}
gr_string(*grd_curcanv, *grd_curcanv->cv_font, margin_width, y, prompt_string, prompt_width, h);
return y;
}
void CLIState::execute_active_line()
{
if (m_line.empty())
return;
const char *p = m_line.c_str();
con_printf(CON_NORMAL, "con%c%s", g_prompt_strings[0], p);
cmd_append(p);
m_lines[0] = std::move(m_line);
m_lines.emplace_front();
m_history_position = 0;
if (m_lines.size() > m_maximum_history_lines)
m_lines.pop_back();
clear_active_line();
}
void CLIState::insert_completion()
{
const auto suggestion = cmd_complete(m_line.c_str());
if (!suggestion)
return;
m_line = suggestion;
m_line += " ";
m_line_position = m_line.size();
}
void CLIState::cursor_left()
{
if (m_line_position > 0)
-- m_line_position;
}
void CLIState::cursor_right()
{
if (m_line_position < m_line.size())
++ m_line_position;
}
void CLIState::cursor_home()
{
m_line_position = 0;
}
void CLIState::cursor_end()
{
m_line_position = m_line.size();
}
void CLIState::cursor_del()
{
const auto l = m_line_position;
if (l >= m_line.size())
return;
m_line.erase(next(m_line.begin(), l));
}
void CLIState::cursor_backspace()
{
if (m_line_position <= 0)
return;
m_line.erase(next(m_line.begin(), --m_line_position));
}
void CLIState::add_character(char c)
{
if (m_insert_type == CLI_insert_type::overwrite && m_line_position < m_line.size())
m_line[m_line_position] = c;
else
m_line.insert(next(m_line.begin(), m_line_position), c);
++m_line_position;
}
void CLIState::clear_active_line()
{
m_line_position = 0;
m_line.clear();
}
void CLIState::history_move(unsigned position)
{
if (position >= m_lines.size())
return;
m_lines[m_history_position] = std::move(m_line);
auto &l = m_lines[m_history_position = position];
m_line_position = l.size();
m_line = l;
}
void CLIState::history_prev()
{
history_move(m_history_position + 1);
}
void CLIState::history_next()
{
const auto max_lines = m_lines.size();
if (m_history_position > max_lines)
m_history_position = max_lines;
history_move(m_history_position - 1);
}
void CLIState::toggle_overwrite_mode()
{
m_insert_type = m_insert_type == CLI_insert_type::insert
? CLI_insert_type::overwrite
: CLI_insert_type::insert;
}