Subversion Repositories Games.Descent

Rev

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
}