3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
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.
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.
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.
28 #include "util/strfnd.h"
29 #include "util/string.h"
30 #include "util/numeric.h"
32 ChatBuffer::ChatBuffer(u32 scrollback):
33 m_scrollback(scrollback)
35 if (m_scrollback == 0)
37 m_empty_formatted_line.first = true;
40 void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
42 ChatLine line(name, text);
43 m_unformatted.push_back(line);
46 // m_formatted is valid and must be kept valid
47 bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
48 u32 num_added = formatChatLine(line, m_cols, m_formatted);
49 if (scrolled_at_bottom)
50 m_scroll += num_added;
53 // Limit number of lines by m_scrollback
54 if (m_unformatted.size() > m_scrollback) {
55 deleteOldest(m_unformatted.size() - m_scrollback);
59 void ChatBuffer::clear()
61 m_unformatted.clear();
66 u32 ChatBuffer::getLineCount() const
68 return m_unformatted.size();
71 const ChatLine& ChatBuffer::getLine(u32 index) const
73 assert(index < getLineCount()); // pre-condition
74 return m_unformatted[index];
77 void ChatBuffer::step(f32 dtime)
79 for (ChatLine &line : m_unformatted) {
84 void ChatBuffer::deleteOldest(u32 count)
86 bool at_bottom = (m_scroll == getBottomScrollPos());
88 u32 del_unformatted = 0;
89 u32 del_formatted = 0;
91 while (count > 0 && del_unformatted < m_unformatted.size())
95 // keep m_formatted in sync
96 if (del_formatted < m_formatted.size())
99 sanity_check(m_formatted[del_formatted].first);
101 while (del_formatted < m_formatted.size() &&
102 !m_formatted[del_formatted].first)
109 m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
110 m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
113 m_scroll = getBottomScrollPos();
115 scrollAbsolute(m_scroll - del_formatted);
118 void ChatBuffer::deleteByAge(f32 maxAge)
121 while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
126 u32 ChatBuffer::getColumns() const
131 u32 ChatBuffer::getRows() const
136 void ChatBuffer::reformat(u32 cols, u32 rows)
138 if (cols == 0 || rows == 0)
140 // Clear formatted buffer
146 else if (cols != m_cols || rows != m_rows)
148 // TODO: Avoid reformatting ALL lines (even invisible ones)
149 // each time the console size changes.
151 // Find out the scroll position in *unformatted* lines
152 u32 restore_scroll_unformatted = 0;
153 u32 restore_scroll_formatted = 0;
154 bool at_bottom = (m_scroll == getBottomScrollPos());
157 for (s32 i = 0; i < m_scroll; ++i)
159 if (m_formatted[i].first)
160 ++restore_scroll_unformatted;
164 // If number of columns change, reformat everything
168 for (u32 i = 0; i < m_unformatted.size(); ++i)
170 if (i == restore_scroll_unformatted)
171 restore_scroll_formatted = m_formatted.size();
172 formatChatLine(m_unformatted[i], cols, m_formatted);
176 // Update the console size
180 // Restore the scroll position
187 scrollAbsolute(restore_scroll_formatted);
192 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
194 s32 index = m_scroll + (s32) row;
195 if (index >= 0 && index < (s32) m_formatted.size())
196 return m_formatted[index];
198 return m_empty_formatted_line;
201 void ChatBuffer::scroll(s32 rows)
203 scrollAbsolute(m_scroll + rows);
206 void ChatBuffer::scrollAbsolute(s32 scroll)
208 s32 top = getTopScrollPos();
209 s32 bottom = getBottomScrollPos();
214 if (m_scroll > bottom)
218 void ChatBuffer::scrollBottom()
220 m_scroll = getBottomScrollPos();
223 void ChatBuffer::scrollTop()
225 m_scroll = getTopScrollPos();
228 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
229 std::vector<ChatFormattedLine>& destination) const
232 std::vector<ChatFormattedFragment> next_frags;
233 ChatFormattedLine next_line;
234 ChatFormattedFragment temp_frag;
237 u32 hanging_indentation = 0;
239 // Format the sender name and produce fragments
240 if (!line.name.empty()) {
241 temp_frag.text = L"<";
242 temp_frag.column = 0;
243 //temp_frag.bold = 0;
244 next_frags.push_back(temp_frag);
245 temp_frag.text = line.name;
246 temp_frag.column = 0;
247 //temp_frag.bold = 1;
248 next_frags.push_back(temp_frag);
249 temp_frag.text = L"> ";
250 temp_frag.column = 0;
251 //temp_frag.bold = 0;
252 next_frags.push_back(temp_frag);
255 std::wstring name_sanitized = line.name.c_str();
257 // Choose an indentation level
258 if (line.name.empty()) {
260 hanging_indentation = 0;
261 } else if (name_sanitized.size() + 3 <= cols/2) {
262 // Names shorter than about half the console width
263 hanging_indentation = line.name.size() + 3;
266 hanging_indentation = 2;
268 //EnrichedString line_text(line.text);
270 next_line.first = true;
271 bool text_processing = false;
273 // Produce fragments and layout them into lines
274 while (!next_frags.empty() || in_pos < line.text.size())
276 // Layout fragments into lines
277 while (!next_frags.empty())
279 ChatFormattedFragment& frag = next_frags[0];
280 if (frag.text.size() <= cols - out_column)
282 // Fragment fits into current line
283 frag.column = out_column;
284 next_line.fragments.push_back(frag);
285 out_column += frag.text.size();
286 next_frags.erase(next_frags.begin());
290 // Fragment does not fit into current line
292 temp_frag.text = frag.text.substr(0, cols - out_column);
293 temp_frag.column = out_column;
294 //temp_frag.bold = frag.bold;
295 next_line.fragments.push_back(temp_frag);
296 frag.text = frag.text.substr(cols - out_column);
299 if (out_column == cols || text_processing)
301 // End the current line
302 destination.push_back(next_line);
304 next_line.fragments.clear();
305 next_line.first = false;
307 out_column = text_processing ? hanging_indentation : 0;
312 if (in_pos < line.text.size())
314 u32 remaining_in_input = line.text.size() - in_pos;
315 u32 remaining_in_output = cols - out_column;
317 // Determine a fragment length <= the minimum of
318 // remaining_in_{in,out}put. Try to end the fragment
319 // on a word boundary.
320 u32 frag_length = 1, space_pos = 0;
321 while (frag_length < remaining_in_input &&
322 frag_length < remaining_in_output)
324 if (iswspace(line.text.getString()[in_pos + frag_length]))
325 space_pos = frag_length;
328 if (space_pos != 0 && frag_length < remaining_in_input)
329 frag_length = space_pos + 1;
331 temp_frag.text = line.text.substr(in_pos, frag_length);
332 temp_frag.column = 0;
333 //temp_frag.bold = 0;
334 next_frags.push_back(temp_frag);
335 in_pos += frag_length;
336 text_processing = true;
341 if (num_added == 0 || !next_line.fragments.empty())
343 destination.push_back(next_line);
350 s32 ChatBuffer::getTopScrollPos() const
352 s32 formatted_count = (s32) m_formatted.size();
353 s32 rows = (s32) m_rows;
357 if (formatted_count <= rows)
358 return formatted_count - rows;
363 s32 ChatBuffer::getBottomScrollPos() const
365 s32 formatted_count = (s32) m_formatted.size();
366 s32 rows = (s32) m_rows;
370 return formatted_count - rows;
373 void ChatBuffer::resize(u32 scrollback)
375 m_scrollback = scrollback;
376 if (m_unformatted.size() > m_scrollback)
377 deleteOldest(m_unformatted.size() - m_scrollback);
381 ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
383 m_history_limit(history_limit)
387 void ChatPrompt::input(wchar_t ch)
389 m_line.insert(m_cursor, 1, ch);
392 m_nick_completion_start = 0;
393 m_nick_completion_end = 0;
396 void ChatPrompt::input(const std::wstring &str)
398 m_line.insert(m_cursor, str);
399 m_cursor += str.size();
401 m_nick_completion_start = 0;
402 m_nick_completion_end = 0;
405 void ChatPrompt::addToHistory(const std::wstring &line)
408 (m_history.size() == 0 || m_history.back() != line)) {
409 // Remove all duplicates
410 m_history.erase(std::remove(m_history.begin(), m_history.end(),
411 line), m_history.end());
413 m_history.push_back(line);
415 if (m_history.size() > m_history_limit)
416 m_history.erase(m_history.begin());
417 m_history_index = m_history.size();
420 void ChatPrompt::clear()
425 m_nick_completion_start = 0;
426 m_nick_completion_end = 0;
429 std::wstring ChatPrompt::replace(const std::wstring &line)
431 std::wstring old_line = m_line;
433 m_view = m_cursor = line.size();
435 m_nick_completion_start = 0;
436 m_nick_completion_end = 0;
440 void ChatPrompt::historyPrev()
442 if (m_history_index != 0)
445 replace(m_history[m_history_index]);
449 void ChatPrompt::historyNext()
451 if (m_history_index + 1 >= m_history.size())
453 m_history_index = m_history.size();
459 replace(m_history[m_history_index]);
463 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
466 // (a) m_nick_completion_start == m_nick_completion_end == 0
467 // Then no previous nick completion is active.
468 // Get the word around the cursor and replace with any nick
469 // that has that word as a prefix.
470 // (b) else, continue a previous nick completion.
471 // m_nick_completion_start..m_nick_completion_end are the
472 // interval where the originally used prefix was. Cycle
473 // through the list of completions of that prefix.
474 u32 prefix_start = m_nick_completion_start;
475 u32 prefix_end = m_nick_completion_end;
476 bool initial = (prefix_end == 0);
479 // no previous nick completion is active
480 prefix_start = prefix_end = m_cursor;
481 while (prefix_start > 0 && !iswspace(m_line[prefix_start-1]))
483 while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
485 if (prefix_start == prefix_end)
488 std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
490 // find all names that start with the selected prefix
491 std::vector<std::wstring> completions;
492 for (const std::string &name : names) {
493 if (str_starts_with(narrow_to_wide(name), prefix, true)) {
494 std::wstring completion = narrow_to_wide(name);
495 if (prefix_start == 0)
497 completions.push_back(completion);
501 if (completions.empty())
504 // find a replacement string and the word that will be replaced
505 u32 word_end = prefix_end;
506 u32 replacement_index = 0;
509 while (word_end < m_line.size() && !iswspace(m_line[word_end]))
511 std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
513 // cycle through completions
514 for (u32 i = 0; i < completions.size(); ++i)
516 if (str_equal(word, completions[i], true))
519 replacement_index = i + completions.size() - 1;
521 replacement_index = i + 1;
522 replacement_index %= completions.size();
527 std::wstring replacement = completions[replacement_index];
528 if (word_end < m_line.size() && iswspace(m_line[word_end]))
531 // replace existing word with replacement word,
532 // place the cursor at the end and record the completion prefix
533 m_line.replace(prefix_start, word_end - prefix_start, replacement);
534 m_cursor = prefix_start + replacement.size();
536 m_nick_completion_start = prefix_start;
537 m_nick_completion_end = prefix_end;
540 void ChatPrompt::reformat(u32 cols)
542 if (cols <= m_prompt.size())
549 s32 length = m_line.size();
550 bool was_at_end = (m_view + m_cols >= length + 1);
551 m_cols = cols - m_prompt.size();
558 std::wstring ChatPrompt::getVisiblePortion() const
560 return m_prompt + m_line.substr(m_view, m_cols);
563 s32 ChatPrompt::getVisibleCursorPosition() const
565 return m_cursor - m_view + m_prompt.size();
568 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
570 s32 old_cursor = m_cursor;
571 s32 new_cursor = m_cursor;
573 s32 length = m_line.size();
574 s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
577 case CURSOROP_SCOPE_CHARACTER:
578 new_cursor += increment;
580 case CURSOROP_SCOPE_WORD:
581 if (dir == CURSOROP_DIR_RIGHT) {
582 // skip one word to the right
583 while (new_cursor < length && iswspace(m_line[new_cursor]))
585 while (new_cursor < length && !iswspace(m_line[new_cursor]))
587 while (new_cursor < length && iswspace(m_line[new_cursor]))
590 // skip one word to the left
591 while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1]))
593 while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
597 case CURSOROP_SCOPE_LINE:
598 new_cursor += increment * length;
600 case CURSOROP_SCOPE_SELECTION:
604 new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
608 m_cursor = new_cursor;
611 case CURSOROP_DELETE:
612 if (m_cursor_len > 0) { // Delete selected text first
613 m_line.erase(m_cursor, m_cursor_len);
615 m_cursor = MYMIN(new_cursor, old_cursor);
616 m_line.erase(m_cursor, abs(new_cursor - old_cursor));
620 case CURSOROP_SELECT:
621 if (scope == CURSOROP_SCOPE_LINE) {
623 m_cursor_len = length;
625 m_cursor = MYMIN(new_cursor, old_cursor);
626 m_cursor_len += abs(new_cursor - old_cursor);
627 m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
634 m_nick_completion_start = 0;
635 m_nick_completion_end = 0;
638 void ChatPrompt::clampView()
640 s32 length = m_line.size();
641 if (length + 1 <= m_cols)
647 m_view = MYMIN(m_view, length + 1 - m_cols);
648 m_view = MYMIN(m_view, m_cursor);
649 m_view = MYMAX(m_view, m_cursor - m_cols + 1);
650 m_view = MYMAX(m_view, 0);
656 ChatBackend::ChatBackend():
657 m_console_buffer(500),
663 void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
665 // Note: A message may consist of multiple lines, for example the MOTD.
666 text = translate_string(text);
668 while (!fnd.at_end())
670 std::wstring line = fnd.next(L"\n");
671 m_console_buffer.addLine(name, line);
672 m_recent_buffer.addLine(name, line);
676 void ChatBackend::addUnparsedMessage(std::wstring message)
678 // TODO: Remove the need to parse chat messages client-side, by sending
679 // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
681 if (message.size() >= 2 && message[0] == L'<')
683 std::size_t closing = message.find_first_of(L'>', 1);
684 if (closing != std::wstring::npos &&
685 closing + 2 <= message.size() &&
686 message[closing+1] == L' ')
688 std::wstring name = message.substr(1, closing - 1);
689 std::wstring text = message.substr(closing + 2);
690 addMessage(name, text);
695 // Unable to parse, probably a server message.
696 addMessage(L"", message);
699 ChatBuffer& ChatBackend::getConsoleBuffer()
701 return m_console_buffer;
704 ChatBuffer& ChatBackend::getRecentBuffer()
706 return m_recent_buffer;
709 EnrichedString ChatBackend::getRecentChat() const
711 EnrichedString result;
712 for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
713 const ChatLine& line = m_recent_buffer.getLine(i);
716 if (!line.name.empty()) {
726 ChatPrompt& ChatBackend::getPrompt()
731 void ChatBackend::reformat(u32 cols, u32 rows)
733 m_console_buffer.reformat(cols, rows);
735 // no need to reformat m_recent_buffer, its formatted lines
738 m_prompt.reformat(cols);
741 void ChatBackend::clearRecentChat()
743 m_recent_buffer.clear();
747 void ChatBackend::applySettings()
749 u32 recent_lines = g_settings->getU32("recent_chat_messages");
750 recent_lines = rangelim(recent_lines, 2, 20);
751 m_recent_buffer.resize(recent_lines);
754 void ChatBackend::step(float dtime)
756 m_recent_buffer.step(dtime);
757 m_recent_buffer.deleteByAge(60.0);
759 // no need to age messages in anything but m_recent_buffer
762 void ChatBackend::scroll(s32 rows)
764 m_console_buffer.scroll(rows);
767 void ChatBackend::scrollPageDown()
769 m_console_buffer.scroll(m_console_buffer.getRows());
772 void ChatBackend::scrollPageUp()
774 m_console_buffer.scroll(-(s32)m_console_buffer.getRows());