/* * This file is part of the DXX-Rebirth project . * 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 * Code Cleanup and heavily extended by: Clemens Wacha * Ported to use native Descent interfaces by: Bradley Bell * * 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 #include #include #include #include #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 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; /* 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 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] = 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] = 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; }