]> git.lizzy.rs Git - dragonfireclient.git/blob - src/chat.cpp
Fix client-side performance of chat UI (#11612)
[dragonfireclient.git] / src / chat.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "chat.h"
21
22 #include <algorithm>
23 #include <cctype>
24 #include <sstream>
25
26 #include "config.h"
27 #include "debug.h"
28 #include "util/strfnd.h"
29 #include "util/string.h"
30 #include "util/numeric.h"
31
32 ChatBuffer::ChatBuffer(u32 scrollback):
33         m_scrollback(scrollback)
34 {
35         if (m_scrollback == 0)
36                 m_scrollback = 1;
37         m_empty_formatted_line.first = true;
38
39         m_cache_clickable_chat_weblinks = false;
40         // Curses mode cannot access g_settings here
41         if (g_settings != nullptr) {
42                 m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
43                 if (m_cache_clickable_chat_weblinks) {
44                         std::string colorval = g_settings->get("chat_weblink_color");
45                         parseColorString(colorval, m_cache_chat_weblink_color, false, 255);
46                         m_cache_chat_weblink_color.setAlpha(255);
47                 }
48         }
49 }
50
51 void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
52 {
53         m_lines_modified = true;
54
55         ChatLine line(name, text);
56         m_unformatted.push_back(line);
57
58         if (m_rows > 0) {
59                 // m_formatted is valid and must be kept valid
60                 bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
61                 u32 num_added = formatChatLine(line, m_cols, m_formatted);
62                 if (scrolled_at_bottom)
63                         m_scroll += num_added;
64         }
65
66         // Limit number of lines by m_scrollback
67         if (m_unformatted.size() > m_scrollback) {
68                 deleteOldest(m_unformatted.size() - m_scrollback);
69         }
70 }
71
72 void ChatBuffer::clear()
73 {
74         m_unformatted.clear();
75         m_formatted.clear();
76         m_scroll = 0;
77         m_lines_modified = true;
78 }
79
80 u32 ChatBuffer::getLineCount() const
81 {
82         return m_unformatted.size();
83 }
84
85 const ChatLine& ChatBuffer::getLine(u32 index) const
86 {
87         assert(index < getLineCount()); // pre-condition
88         return m_unformatted[index];
89 }
90
91 void ChatBuffer::step(f32 dtime)
92 {
93         for (ChatLine &line : m_unformatted) {
94                 line.age += dtime;
95         }
96 }
97
98 void ChatBuffer::deleteOldest(u32 count)
99 {
100         bool at_bottom = (m_scroll == getBottomScrollPos());
101
102         u32 del_unformatted = 0;
103         u32 del_formatted = 0;
104
105         while (count > 0 && del_unformatted < m_unformatted.size()) {
106                 ++del_unformatted;
107
108                 // keep m_formatted in sync
109                 if (del_formatted < m_formatted.size()) {
110                         sanity_check(m_formatted[del_formatted].first);
111                         ++del_formatted;
112                         while (del_formatted < m_formatted.size() &&
113                                         !m_formatted[del_formatted].first)
114                                 ++del_formatted;
115                 }
116
117                 --count;
118         }
119
120         m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
121         m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
122
123         if (del_unformatted > 0)
124                 m_lines_modified = true;
125
126         if (at_bottom)
127                 m_scroll = getBottomScrollPos();
128         else
129                 scrollAbsolute(m_scroll - del_formatted);
130 }
131
132 void ChatBuffer::deleteByAge(f32 maxAge)
133 {
134         u32 count = 0;
135         while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
136                 ++count;
137         deleteOldest(count);
138 }
139
140 u32 ChatBuffer::getRows() const
141 {
142         return m_rows;
143 }
144
145 void ChatBuffer::scrollTop()
146 {
147         m_scroll = getTopScrollPos();
148 }
149
150 void ChatBuffer::reformat(u32 cols, u32 rows)
151 {
152         if (cols == 0 || rows == 0)
153         {
154                 // Clear formatted buffer
155                 m_cols = 0;
156                 m_rows = 0;
157                 m_scroll = 0;
158                 m_formatted.clear();
159         }
160         else if (cols != m_cols || rows != m_rows)
161         {
162                 // TODO: Avoid reformatting ALL lines (even invisible ones)
163                 // each time the console size changes.
164
165                 // Find out the scroll position in *unformatted* lines
166                 u32 restore_scroll_unformatted = 0;
167                 u32 restore_scroll_formatted = 0;
168                 bool at_bottom = (m_scroll == getBottomScrollPos());
169                 if (!at_bottom)
170                 {
171                         for (s32 i = 0; i < m_scroll; ++i)
172                         {
173                                 if (m_formatted[i].first)
174                                         ++restore_scroll_unformatted;
175                         }
176                 }
177
178                 // If number of columns change, reformat everything
179                 if (cols != m_cols)
180                 {
181                         m_formatted.clear();
182                         for (u32 i = 0; i < m_unformatted.size(); ++i)
183                         {
184                                 if (i == restore_scroll_unformatted)
185                                         restore_scroll_formatted = m_formatted.size();
186                                 formatChatLine(m_unformatted[i], cols, m_formatted);
187                         }
188                 }
189
190                 // Update the console size
191                 m_cols = cols;
192                 m_rows = rows;
193
194                 // Restore the scroll position
195                 if (at_bottom)
196                 {
197                         scrollBottom();
198                 }
199                 else
200                 {
201                         scrollAbsolute(restore_scroll_formatted);
202                 }
203         }
204 }
205
206 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
207 {
208         s32 index = m_scroll + (s32) row;
209         if (index >= 0 && index < (s32) m_formatted.size())
210                 return m_formatted[index];
211
212         return m_empty_formatted_line;
213 }
214
215 void ChatBuffer::scroll(s32 rows)
216 {
217         scrollAbsolute(m_scroll + rows);
218 }
219
220 void ChatBuffer::scrollAbsolute(s32 scroll)
221 {
222         s32 top = getTopScrollPos();
223         s32 bottom = getBottomScrollPos();
224
225         m_scroll = scroll;
226         if (m_scroll < top)
227                 m_scroll = top;
228         if (m_scroll > bottom)
229                 m_scroll = bottom;
230 }
231
232 void ChatBuffer::scrollBottom()
233 {
234         m_scroll = getBottomScrollPos();
235 }
236
237 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
238                 std::vector<ChatFormattedLine>& destination) const
239 {
240         u32 num_added = 0;
241         std::vector<ChatFormattedFragment> next_frags;
242         ChatFormattedLine next_line;
243         ChatFormattedFragment temp_frag;
244         u32 out_column = 0;
245         u32 in_pos = 0;
246         u32 hanging_indentation = 0;
247
248         // Format the sender name and produce fragments
249         if (!line.name.empty()) {
250                 temp_frag.text = L"<";
251                 temp_frag.column = 0;
252                 //temp_frag.bold = 0;
253                 next_frags.push_back(temp_frag);
254                 temp_frag.text = line.name;
255                 temp_frag.column = 0;
256                 //temp_frag.bold = 1;
257                 next_frags.push_back(temp_frag);
258                 temp_frag.text = L"> ";
259                 temp_frag.column = 0;
260                 //temp_frag.bold = 0;
261                 next_frags.push_back(temp_frag);
262         }
263
264         std::wstring name_sanitized = line.name.c_str();
265
266         // Choose an indentation level
267         if (line.name.empty()) {
268                 // Server messages
269                 hanging_indentation = 0;
270         } else if (name_sanitized.size() + 3 <= cols/2) {
271                 // Names shorter than about half the console width
272                 hanging_indentation = line.name.size() + 3;
273         } else {
274                 // Very long names
275                 hanging_indentation = 2;
276         }
277         //EnrichedString line_text(line.text);
278
279         next_line.first = true;
280         // Set/use forced newline after the last frag in each line
281         bool mark_newline = false;
282
283         // Produce fragments and layout them into lines
284         while (!next_frags.empty() || in_pos < line.text.size()) {
285                 mark_newline = false; // now using this to USE line-end frag
286
287                 // Layout fragments into lines
288                 while (!next_frags.empty()) {
289                         ChatFormattedFragment& frag = next_frags[0];
290
291                         // Force newline after this frag, if marked
292                         if (frag.column == INT_MAX)
293                                 mark_newline = true;
294
295                         if (frag.text.size() <= cols - out_column) {
296                                 // Fragment fits into current line
297                                 frag.column = out_column;
298                                 next_line.fragments.push_back(frag);
299                                 out_column += frag.text.size();
300                                 next_frags.erase(next_frags.begin());
301                         } else {
302                                 // Fragment does not fit into current line
303                                 // So split it up
304                                 temp_frag.text = frag.text.substr(0, cols - out_column);
305                                 temp_frag.column = out_column;
306                                 temp_frag.weblink = frag.weblink;
307
308                                 next_line.fragments.push_back(temp_frag);
309                                 frag.text = frag.text.substr(cols - out_column);
310                                 frag.column = 0;
311                                 out_column = cols;
312                         }
313
314                         if (out_column == cols || mark_newline) {
315                                 // End the current line
316                                 destination.push_back(next_line);
317                                 num_added++;
318                                 next_line.fragments.clear();
319                                 next_line.first = false;
320
321                                 out_column = hanging_indentation;
322                                 mark_newline = false;
323                         }
324                 }
325
326                 // Produce fragment(s) for next formatted line
327                 if (!(in_pos < line.text.size()))
328                         continue;
329
330                 const std::wstring &linestring = line.text.getString();
331                 u32 remaining_in_output = cols - out_column;
332                 size_t http_pos = std::wstring::npos;
333                 mark_newline = false;  // now using this to SET line-end frag
334
335                 // Construct all frags for next output line
336                 while (!mark_newline) {
337                         // Determine a fragment length <= the minimum of
338                         // remaining_in_{in,out}put. Try to end the fragment
339                         // on a word boundary.
340                         u32 frag_length = 0, space_pos = 0;
341                         u32 remaining_in_input = line.text.size() - in_pos;
342
343                         if (m_cache_clickable_chat_weblinks) {
344                                 // Note: unsigned(-1) on fail
345                                 http_pos = linestring.find(L"https://", in_pos);
346                                 if (http_pos == std::wstring::npos)
347                                         http_pos = linestring.find(L"http://", in_pos);
348                                 if (http_pos != std::wstring::npos)
349                                         http_pos -= in_pos;
350                         }
351
352                         while (frag_length < remaining_in_input &&
353                                         frag_length < remaining_in_output) {
354                                 if (iswspace(linestring[in_pos + frag_length]))
355                                         space_pos = frag_length;
356                                 ++frag_length;
357                         }
358
359                         if (http_pos >= remaining_in_output) {
360                                 // Http not in range, grab until space or EOL, halt as normal.
361                                 // Note this works because (http_pos = npos) is unsigned(-1)
362
363                                 mark_newline = true;
364                         } else if (http_pos == 0) {
365                                 // At http, grab ALL until FIRST whitespace or end marker. loop.
366                                 // If at end of string, next loop will be empty string to mark end of weblink.
367
368                                 frag_length = 6;  // Frag is at least "http://"
369
370                                 // Chars to mark end of weblink
371                                 // TODO? replace this with a safer (slower) regex whitelist?
372                                 static const std::wstring delim_chars = L"\'\";,";
373                                 wchar_t tempchar = linestring[in_pos+frag_length];
374                                 while (frag_length < remaining_in_input &&
375                                                 !iswspace(tempchar) &&
376                                                 delim_chars.find(tempchar) == std::wstring::npos) {
377                                         ++frag_length;
378                                         tempchar = linestring[in_pos+frag_length];
379                                 }
380
381                                 space_pos = frag_length - 1;
382                                 // This frag may need to be force-split. That's ok, urls aren't "words"
383                                 if (frag_length >= remaining_in_output) {
384                                         mark_newline = true;
385                                 }
386                         } else {
387                                 // Http in range, grab until http, loop
388
389                                 space_pos = http_pos - 1;
390                                 frag_length = http_pos;
391                         }
392
393                         // Include trailing space in current frag
394                         if (space_pos != 0 && frag_length < remaining_in_input)
395                                 frag_length = space_pos + 1;
396
397                         temp_frag.text = line.text.substr(in_pos, frag_length);
398                         // A hack so this frag remembers mark_newline for the layout phase
399                         temp_frag.column = mark_newline ? INT_MAX : 0;
400
401                         if (http_pos == 0) {
402                                 // Discard color stuff from the source frag
403                                 temp_frag.text = EnrichedString(temp_frag.text.getString());
404                                 temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
405                                 // Set weblink in the frag meta
406                                 temp_frag.weblink = wide_to_utf8(temp_frag.text.getString());
407                         } else {
408                                 temp_frag.weblink.clear();
409                         }
410                         next_frags.push_back(temp_frag);
411                         in_pos += frag_length;
412                         remaining_in_output -= std::min(frag_length, remaining_in_output);
413                 }
414         }
415
416         // End the last line
417         if (num_added == 0 || !next_line.fragments.empty()) {
418                 destination.push_back(next_line);
419                 num_added++;
420         }
421
422         return num_added;
423 }
424
425 s32 ChatBuffer::getTopScrollPos() const
426 {
427         s32 formatted_count = (s32) m_formatted.size();
428         s32 rows = (s32) m_rows;
429         if (rows == 0)
430                 return 0;
431
432         if (formatted_count <= rows)
433                 return formatted_count - rows;
434
435         return 0;
436 }
437
438 s32 ChatBuffer::getBottomScrollPos() const
439 {
440         s32 formatted_count = (s32) m_formatted.size();
441         s32 rows = (s32) m_rows;
442         if (rows == 0)
443                 return 0;
444
445         return formatted_count - rows;
446 }
447
448 void ChatBuffer::resize(u32 scrollback)
449 {
450         m_scrollback = scrollback;
451         if (m_unformatted.size() > m_scrollback)
452                 deleteOldest(m_unformatted.size() - m_scrollback);
453 }
454
455
456 ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
457         m_prompt(prompt),
458         m_history_limit(history_limit)
459 {
460 }
461
462 void ChatPrompt::input(wchar_t ch)
463 {
464         m_line.insert(m_cursor, 1, ch);
465         m_cursor++;
466         clampView();
467         m_nick_completion_start = 0;
468         m_nick_completion_end = 0;
469 }
470
471 void ChatPrompt::input(const std::wstring &str)
472 {
473         m_line.insert(m_cursor, str);
474         m_cursor += str.size();
475         clampView();
476         m_nick_completion_start = 0;
477         m_nick_completion_end = 0;
478 }
479
480 void ChatPrompt::addToHistory(const std::wstring &line)
481 {
482         if (!line.empty() &&
483                         (m_history.size() == 0 || m_history.back() != line)) {
484                 // Remove all duplicates
485                 m_history.erase(std::remove(m_history.begin(), m_history.end(),
486                         line), m_history.end());
487                 // Push unique line
488                 m_history.push_back(line);
489         }
490         if (m_history.size() > m_history_limit)
491                 m_history.erase(m_history.begin());
492         m_history_index = m_history.size();
493 }
494
495 void ChatPrompt::clear()
496 {
497         m_line.clear();
498         m_view = 0;
499         m_cursor = 0;
500         m_nick_completion_start = 0;
501         m_nick_completion_end = 0;
502 }
503
504 std::wstring ChatPrompt::replace(const std::wstring &line)
505 {
506         std::wstring old_line = m_line;
507         m_line =  line;
508         m_view = m_cursor = line.size();
509         clampView();
510         m_nick_completion_start = 0;
511         m_nick_completion_end = 0;
512         return old_line;
513 }
514
515 void ChatPrompt::historyPrev()
516 {
517         if (m_history_index != 0)
518         {
519                 --m_history_index;
520                 replace(m_history[m_history_index]);
521         }
522 }
523
524 void ChatPrompt::historyNext()
525 {
526         if (m_history_index + 1 >= m_history.size())
527         {
528                 m_history_index = m_history.size();
529                 replace(L"");
530         }
531         else
532         {
533                 ++m_history_index;
534                 replace(m_history[m_history_index]);
535         }
536 }
537
538 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
539 {
540         // Two cases:
541         // (a) m_nick_completion_start == m_nick_completion_end == 0
542         //     Then no previous nick completion is active.
543         //     Get the word around the cursor and replace with any nick
544         //     that has that word as a prefix.
545         // (b) else, continue a previous nick completion.
546         //     m_nick_completion_start..m_nick_completion_end are the
547         //     interval where the originally used prefix was. Cycle
548         //     through the list of completions of that prefix.
549         u32 prefix_start = m_nick_completion_start;
550         u32 prefix_end = m_nick_completion_end;
551         bool initial = (prefix_end == 0);
552         if (initial)
553         {
554                 // no previous nick completion is active
555                 prefix_start = prefix_end = m_cursor;
556                 while (prefix_start > 0 && !iswspace(m_line[prefix_start-1]))
557                         --prefix_start;
558                 while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
559                         ++prefix_end;
560                 if (prefix_start == prefix_end)
561                         return;
562         }
563         std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
564
565         // find all names that start with the selected prefix
566         std::vector<std::wstring> completions;
567         for (const std::string &name : names) {
568                 std::wstring completion = utf8_to_wide(name);
569                 if (str_starts_with(completion, prefix, true)) {
570                         if (prefix_start == 0)
571                                 completion += L": ";
572                         completions.push_back(completion);
573                 }
574         }
575
576         if (completions.empty())
577                 return;
578
579         // find a replacement string and the word that will be replaced
580         u32 word_end = prefix_end;
581         u32 replacement_index = 0;
582         if (!initial)
583         {
584                 while (word_end < m_line.size() && !iswspace(m_line[word_end]))
585                         ++word_end;
586                 std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
587
588                 // cycle through completions
589                 for (u32 i = 0; i < completions.size(); ++i)
590                 {
591                         if (str_equal(word, completions[i], true))
592                         {
593                                 if (backwards)
594                                         replacement_index = i + completions.size() - 1;
595                                 else
596                                         replacement_index = i + 1;
597                                 replacement_index %= completions.size();
598                                 break;
599                         }
600                 }
601         }
602         std::wstring replacement = completions[replacement_index];
603         if (word_end < m_line.size() && iswspace(m_line[word_end]))
604                 ++word_end;
605
606         // replace existing word with replacement word,
607         // place the cursor at the end and record the completion prefix
608         m_line.replace(prefix_start, word_end - prefix_start, replacement);
609         m_cursor = prefix_start + replacement.size();
610         clampView();
611         m_nick_completion_start = prefix_start;
612         m_nick_completion_end = prefix_end;
613 }
614
615 void ChatPrompt::reformat(u32 cols)
616 {
617         if (cols <= m_prompt.size())
618         {
619                 m_cols = 0;
620                 m_view = m_cursor;
621         }
622         else
623         {
624                 s32 length = m_line.size();
625                 bool was_at_end = (m_view + m_cols >= length + 1);
626                 m_cols = cols - m_prompt.size();
627                 if (was_at_end)
628                         m_view = length;
629                 clampView();
630         }
631 }
632
633 std::wstring ChatPrompt::getVisiblePortion() const
634 {
635         return m_prompt + m_line.substr(m_view, m_cols);
636 }
637
638 s32 ChatPrompt::getVisibleCursorPosition() const
639 {
640         return m_cursor - m_view + m_prompt.size();
641 }
642
643 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
644 {
645         s32 old_cursor = m_cursor;
646         s32 new_cursor = m_cursor;
647
648         s32 length = m_line.size();
649         s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
650
651         switch (scope) {
652         case CURSOROP_SCOPE_CHARACTER:
653                 new_cursor += increment;
654                 break;
655         case CURSOROP_SCOPE_WORD:
656                 if (dir == CURSOROP_DIR_RIGHT) {
657                         // skip one word to the right
658                         while (new_cursor < length && iswspace(m_line[new_cursor]))
659                                 new_cursor++;
660                         while (new_cursor < length && !iswspace(m_line[new_cursor]))
661                                 new_cursor++;
662                         while (new_cursor < length && iswspace(m_line[new_cursor]))
663                                 new_cursor++;
664                 } else {
665                         // skip one word to the left
666                         while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1]))
667                                 new_cursor--;
668                         while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
669                                 new_cursor--;
670                 }
671                 break;
672         case CURSOROP_SCOPE_LINE:
673                 new_cursor += increment * length;
674                 break;
675         case CURSOROP_SCOPE_SELECTION:
676                 break;
677         }
678
679         new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
680
681         switch (op) {
682         case CURSOROP_MOVE:
683                 m_cursor = new_cursor;
684                 m_cursor_len = 0;
685                 break;
686         case CURSOROP_DELETE:
687                 if (m_cursor_len > 0) { // Delete selected text first
688                         m_line.erase(m_cursor, m_cursor_len);
689                 } else {
690                         m_cursor = MYMIN(new_cursor, old_cursor);
691                         m_line.erase(m_cursor, abs(new_cursor - old_cursor));
692                 }
693                 m_cursor_len = 0;
694                 break;
695         case CURSOROP_SELECT:
696                 if (scope == CURSOROP_SCOPE_LINE) {
697                         m_cursor = 0;
698                         m_cursor_len = length;
699                 } else {
700                         m_cursor = MYMIN(new_cursor, old_cursor);
701                         m_cursor_len += abs(new_cursor - old_cursor);
702                         m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
703                 }
704                 break;
705         }
706
707         clampView();
708
709         m_nick_completion_start = 0;
710         m_nick_completion_end = 0;
711 }
712
713 void ChatPrompt::clampView()
714 {
715         s32 length = m_line.size();
716         if (length + 1 <= m_cols)
717         {
718                 m_view = 0;
719         }
720         else
721         {
722                 m_view = MYMIN(m_view, length + 1 - m_cols);
723                 m_view = MYMIN(m_view, m_cursor);
724                 m_view = MYMAX(m_view, m_cursor - m_cols + 1);
725                 m_view = MYMAX(m_view, 0);
726         }
727 }
728
729
730
731 ChatBackend::ChatBackend():
732         m_console_buffer(500),
733         m_recent_buffer(6),
734         m_prompt(L"]", 500)
735 {
736 }
737
738 void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
739 {
740         // Note: A message may consist of multiple lines, for example the MOTD.
741         text = translate_string(text);
742         WStrfnd fnd(text);
743         while (!fnd.at_end())
744         {
745                 std::wstring line = fnd.next(L"\n");
746                 m_console_buffer.addLine(name, line);
747                 m_recent_buffer.addLine(name, line);
748         }
749 }
750
751 void ChatBackend::addUnparsedMessage(std::wstring message)
752 {
753         // TODO: Remove the need to parse chat messages client-side, by sending
754         // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
755
756         if (message.size() >= 2 && message[0] == L'<')
757         {
758                 std::size_t closing = message.find_first_of(L'>', 1);
759                 if (closing != std::wstring::npos &&
760                                 closing + 2 <= message.size() &&
761                                 message[closing+1] == L' ')
762                 {
763                         std::wstring name = message.substr(1, closing - 1);
764                         std::wstring text = message.substr(closing + 2);
765                         addMessage(name, text);
766                         return;
767                 }
768         }
769
770         // Unable to parse, probably a server message.
771         addMessage(L"", message);
772 }
773
774 ChatBuffer& ChatBackend::getConsoleBuffer()
775 {
776         return m_console_buffer;
777 }
778
779 ChatBuffer& ChatBackend::getRecentBuffer()
780 {
781         return m_recent_buffer;
782 }
783
784 EnrichedString ChatBackend::getRecentChat() const
785 {
786         EnrichedString result;
787         for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
788                 const ChatLine& line = m_recent_buffer.getLine(i);
789                 if (i != 0)
790                         result += L"\n";
791                 if (!line.name.empty()) {
792                         result += L"<";
793                         result += line.name;
794                         result += L"> ";
795                 }
796                 result += line.text;
797         }
798         return result;
799 }
800
801 ChatPrompt& ChatBackend::getPrompt()
802 {
803         return m_prompt;
804 }
805
806 void ChatBackend::reformat(u32 cols, u32 rows)
807 {
808         m_console_buffer.reformat(cols, rows);
809
810         // no need to reformat m_recent_buffer, its formatted lines
811         // are not used
812
813         m_prompt.reformat(cols);
814 }
815
816 void ChatBackend::clearRecentChat()
817 {
818         m_recent_buffer.clear();
819 }
820
821
822 void ChatBackend::applySettings()
823 {
824         u32 recent_lines = g_settings->getU32("recent_chat_messages");
825         recent_lines = rangelim(recent_lines, 2, 20);
826         m_recent_buffer.resize(recent_lines);
827 }
828
829 void ChatBackend::step(float dtime)
830 {
831         m_recent_buffer.step(dtime);
832         m_recent_buffer.deleteByAge(60.0);
833
834         // no need to age messages in anything but m_recent_buffer
835 }
836
837 void ChatBackend::scroll(s32 rows)
838 {
839         m_console_buffer.scroll(rows);
840 }
841
842 void ChatBackend::scrollPageDown()
843 {
844         m_console_buffer.scroll(m_console_buffer.getRows());
845 }
846
847 void ChatBackend::scrollPageUp()
848 {
849         m_console_buffer.scroll(-(s32)m_console_buffer.getRows());
850 }