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