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.
25 #include "util/string.h"
26 #include "util/numeric.h"
28 ChatBuffer::ChatBuffer(u32 scrollback):
29 m_scrollback(scrollback),
35 m_empty_formatted_line()
37 if (m_scrollback == 0)
39 m_empty_formatted_line.first = true;
42 ChatBuffer::~ChatBuffer()
46 void ChatBuffer::addLine(std::wstring name, std::wstring text)
48 ChatLine line(name, text);
49 m_unformatted.push_back(line);
53 // m_formatted is valid and must be kept valid
54 bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
55 u32 num_added = formatChatLine(line, m_cols, m_formatted);
56 if (scrolled_at_bottom)
57 m_scroll += num_added;
60 // Limit number of lines by m_scrollback
61 if (m_unformatted.size() > m_scrollback)
63 deleteOldest(m_unformatted.size() - m_scrollback);
67 void ChatBuffer::clear()
69 m_unformatted.clear();
74 u32 ChatBuffer::getLineCount() const
76 return m_unformatted.size();
79 u32 ChatBuffer::getScrollback() const
84 const ChatLine& ChatBuffer::getLine(u32 index) const
86 assert(index < getLineCount()); // pre-condition
87 return m_unformatted[index];
90 void ChatBuffer::step(f32 dtime)
92 for (u32 i = 0; i < m_unformatted.size(); ++i)
94 m_unformatted[i].age += dtime;
98 void ChatBuffer::deleteOldest(u32 count)
100 u32 del_unformatted = 0;
101 u32 del_formatted = 0;
103 while (count > 0 && del_unformatted < m_unformatted.size())
107 // keep m_formatted in sync
108 if (del_formatted < m_formatted.size())
111 sanity_check(m_formatted[del_formatted].first);
113 while (del_formatted < m_formatted.size() &&
114 !m_formatted[del_formatted].first)
121 m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
122 m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
125 void ChatBuffer::deleteByAge(f32 maxAge)
128 while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
133 u32 ChatBuffer::getColumns() const
138 u32 ChatBuffer::getRows() const
143 void ChatBuffer::reformat(u32 cols, u32 rows)
145 if (cols == 0 || rows == 0)
147 // Clear formatted buffer
153 else if (cols != m_cols || rows != m_rows)
155 // TODO: Avoid reformatting ALL lines (even invisible ones)
156 // each time the console size changes.
158 // Find out the scroll position in *unformatted* lines
159 u32 restore_scroll_unformatted = 0;
160 u32 restore_scroll_formatted = 0;
161 bool at_bottom = (m_scroll == getBottomScrollPos());
164 for (s32 i = 0; i < m_scroll; ++i)
166 if (m_formatted[i].first)
167 ++restore_scroll_unformatted;
171 // If number of columns change, reformat everything
175 for (u32 i = 0; i < m_unformatted.size(); ++i)
177 if (i == restore_scroll_unformatted)
178 restore_scroll_formatted = m_formatted.size();
179 formatChatLine(m_unformatted[i], cols, m_formatted);
183 // Update the console size
187 // Restore the scroll position
194 scrollAbsolute(restore_scroll_formatted);
199 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
201 s32 index = m_scroll + (s32) row;
202 if (index >= 0 && index < (s32) m_formatted.size())
203 return m_formatted[index];
205 return m_empty_formatted_line;
208 void ChatBuffer::scroll(s32 rows)
210 scrollAbsolute(m_scroll + rows);
213 void ChatBuffer::scrollAbsolute(s32 scroll)
215 s32 top = getTopScrollPos();
216 s32 bottom = getBottomScrollPos();
221 if (m_scroll > bottom)
225 void ChatBuffer::scrollBottom()
227 m_scroll = getBottomScrollPos();
230 void ChatBuffer::scrollTop()
232 m_scroll = getTopScrollPos();
235 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
236 std::vector<ChatFormattedLine>& destination) const
239 std::vector<ChatFormattedFragment> next_frags;
240 ChatFormattedLine next_line;
241 ChatFormattedFragment temp_frag;
244 u32 hanging_indentation = 0;
246 // Format the sender name and produce fragments
247 if (!line.name.empty())
249 temp_frag.text = L"<";
250 temp_frag.column = 0;
251 //temp_frag.bold = 0;
252 next_frags.push_back(temp_frag);
253 temp_frag.text = line.name;
254 temp_frag.column = 0;
255 //temp_frag.bold = 1;
256 next_frags.push_back(temp_frag);
257 temp_frag.text = L"> ";
258 temp_frag.column = 0;
259 //temp_frag.bold = 0;
260 next_frags.push_back(temp_frag);
263 // Choose an indentation level
264 if (line.name.empty())
267 hanging_indentation = 0;
269 else if (line.name.size() + 3 <= cols/2)
271 // Names shorter than about half the console width
272 hanging_indentation = line.name.size() + 3;
277 hanging_indentation = 2;
280 next_line.first = true;
281 bool text_processing = false;
283 // Produce fragments and layout them into lines
284 while (!next_frags.empty() || in_pos < line.text.size())
286 // Layout fragments into lines
287 while (!next_frags.empty())
289 ChatFormattedFragment& frag = next_frags[0];
290 if (frag.text.size() <= cols - out_column)
292 // Fragment fits into current line
293 frag.column = out_column;
294 next_line.fragments.push_back(frag);
295 out_column += frag.text.size();
296 next_frags.erase(next_frags.begin());
300 // Fragment does not fit into current line
302 temp_frag.text = frag.text.substr(0, cols - out_column);
303 temp_frag.column = out_column;
304 //temp_frag.bold = frag.bold;
305 next_line.fragments.push_back(temp_frag);
306 frag.text = frag.text.substr(cols - out_column);
309 if (out_column == cols || text_processing)
311 // End the current line
312 destination.push_back(next_line);
314 next_line.fragments.clear();
315 next_line.first = false;
317 out_column = text_processing ? hanging_indentation : 0;
322 if (in_pos < line.text.size())
324 u32 remaining_in_input = line.text.size() - in_pos;
325 u32 remaining_in_output = cols - out_column;
327 // Determine a fragment length <= the minimum of
328 // remaining_in_{in,out}put. Try to end the fragment
329 // on a word boundary.
330 u32 frag_length = 1, space_pos = 0;
331 while (frag_length < remaining_in_input &&
332 frag_length < remaining_in_output)
334 if (isspace(line.text[in_pos + frag_length]))
335 space_pos = frag_length;
338 if (space_pos != 0 && frag_length < remaining_in_input)
339 frag_length = space_pos + 1;
341 temp_frag.text = line.text.substr(in_pos, frag_length);
342 temp_frag.column = 0;
343 //temp_frag.bold = 0;
344 next_frags.push_back(temp_frag);
345 in_pos += frag_length;
346 text_processing = true;
351 if (num_added == 0 || !next_line.fragments.empty())
353 destination.push_back(next_line);
360 s32 ChatBuffer::getTopScrollPos() const
362 s32 formatted_count = (s32) m_formatted.size();
363 s32 rows = (s32) m_rows;
366 else if (formatted_count <= rows)
367 return formatted_count - rows;
372 s32 ChatBuffer::getBottomScrollPos() const
374 s32 formatted_count = (s32) m_formatted.size();
375 s32 rows = (s32) m_rows;
379 return formatted_count - rows;
384 ChatPrompt::ChatPrompt(std::wstring prompt, u32 history_limit):
389 m_history_limit(history_limit),
393 m_nick_completion_start(0),
394 m_nick_completion_end(0)
398 ChatPrompt::~ChatPrompt()
402 void ChatPrompt::input(wchar_t ch)
404 m_line.insert(m_cursor, 1, ch);
407 m_nick_completion_start = 0;
408 m_nick_completion_end = 0;
411 void ChatPrompt::input(const std::wstring &str)
413 m_line.insert(m_cursor, str);
414 m_cursor += str.size();
416 m_nick_completion_start = 0;
417 m_nick_completion_end = 0;
420 std::wstring ChatPrompt::submit()
422 std::wstring line = m_line;
425 m_history.push_back(line);
426 if (m_history.size() > m_history_limit)
427 m_history.erase(m_history.begin());
428 m_history_index = m_history.size();
431 m_nick_completion_start = 0;
432 m_nick_completion_end = 0;
436 void ChatPrompt::clear()
441 m_nick_completion_start = 0;
442 m_nick_completion_end = 0;
445 void ChatPrompt::replace(std::wstring line)
448 m_view = m_cursor = line.size();
450 m_nick_completion_start = 0;
451 m_nick_completion_end = 0;
454 void ChatPrompt::historyPrev()
456 if (m_history_index != 0)
459 replace(m_history[m_history_index]);
463 void ChatPrompt::historyNext()
465 if (m_history_index + 1 >= m_history.size())
467 m_history_index = m_history.size();
473 replace(m_history[m_history_index]);
477 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
480 // (a) m_nick_completion_start == m_nick_completion_end == 0
481 // Then no previous nick completion is active.
482 // Get the word around the cursor and replace with any nick
483 // that has that word as a prefix.
484 // (b) else, continue a previous nick completion.
485 // m_nick_completion_start..m_nick_completion_end are the
486 // interval where the originally used prefix was. Cycle
487 // through the list of completions of that prefix.
488 u32 prefix_start = m_nick_completion_start;
489 u32 prefix_end = m_nick_completion_end;
490 bool initial = (prefix_end == 0);
493 // no previous nick completion is active
494 prefix_start = prefix_end = m_cursor;
495 while (prefix_start > 0 && !isspace(m_line[prefix_start-1]))
497 while (prefix_end < m_line.size() && !isspace(m_line[prefix_end]))
499 if (prefix_start == prefix_end)
502 std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
504 // find all names that start with the selected prefix
505 std::vector<std::wstring> completions;
506 for (std::list<std::string>::const_iterator
508 i != names.end(); ++i)
510 if (str_starts_with(narrow_to_wide(*i), prefix, true))
512 std::wstring completion = narrow_to_wide(*i);
513 if (prefix_start == 0)
515 completions.push_back(completion);
518 if (completions.empty())
521 // find a replacement string and the word that will be replaced
522 u32 word_end = prefix_end;
523 u32 replacement_index = 0;
526 while (word_end < m_line.size() && !isspace(m_line[word_end]))
528 std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
530 // cycle through completions
531 for (u32 i = 0; i < completions.size(); ++i)
533 if (str_equal(word, completions[i], true))
536 replacement_index = i + completions.size() - 1;
538 replacement_index = i + 1;
539 replacement_index %= completions.size();
544 std::wstring replacement = completions[replacement_index];
545 if (word_end < m_line.size() && isspace(word_end))
548 // replace existing word with replacement word,
549 // place the cursor at the end and record the completion prefix
550 m_line.replace(prefix_start, word_end - prefix_start, replacement);
551 m_cursor = prefix_start + replacement.size();
553 m_nick_completion_start = prefix_start;
554 m_nick_completion_end = prefix_end;
557 void ChatPrompt::reformat(u32 cols)
559 if (cols <= m_prompt.size())
566 s32 length = m_line.size();
567 bool was_at_end = (m_view + m_cols >= length + 1);
568 m_cols = cols - m_prompt.size();
575 std::wstring ChatPrompt::getVisiblePortion() const
577 return m_prompt + m_line.substr(m_view, m_cols);
580 s32 ChatPrompt::getVisibleCursorPosition() const
582 return m_cursor - m_view + m_prompt.size();
585 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
587 s32 old_cursor = m_cursor;
588 s32 new_cursor = m_cursor;
590 s32 length = m_line.size();
591 s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
593 if (scope == CURSOROP_SCOPE_CHARACTER)
595 new_cursor += increment;
597 else if (scope == CURSOROP_SCOPE_WORD)
601 // skip one word to the right
602 while (new_cursor < length && isspace(m_line[new_cursor]))
604 while (new_cursor < length && !isspace(m_line[new_cursor]))
606 while (new_cursor < length && isspace(m_line[new_cursor]))
611 // skip one word to the left
612 while (new_cursor >= 1 && isspace(m_line[new_cursor - 1]))
614 while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1]))
618 else if (scope == CURSOROP_SCOPE_LINE)
620 new_cursor += increment * length;
623 new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
625 if (op == CURSOROP_MOVE)
627 m_cursor = new_cursor;
629 else if (op == CURSOROP_DELETE)
631 if (new_cursor < old_cursor)
633 m_line.erase(new_cursor, old_cursor - new_cursor);
634 m_cursor = new_cursor;
636 else if (new_cursor > old_cursor)
638 m_line.erase(old_cursor, new_cursor - old_cursor);
639 m_cursor = old_cursor;
645 m_nick_completion_start = 0;
646 m_nick_completion_end = 0;
649 void ChatPrompt::clampView()
651 s32 length = m_line.size();
652 if (length + 1 <= m_cols)
658 m_view = MYMIN(m_view, length + 1 - m_cols);
659 m_view = MYMIN(m_view, m_cursor);
660 m_view = MYMAX(m_view, m_cursor - m_cols + 1);
661 m_view = MYMAX(m_view, 0);
667 ChatBackend::ChatBackend():
668 m_console_buffer(500),
674 ChatBackend::~ChatBackend()
678 void ChatBackend::addMessage(std::wstring name, std::wstring text)
680 // Note: A message may consist of multiple lines, for example the MOTD.
684 std::wstring line = fnd.next(L"\n");
685 m_console_buffer.addLine(name, line);
686 m_recent_buffer.addLine(name, line);
690 void ChatBackend::addUnparsedMessage(std::wstring message)
692 // TODO: Remove the need to parse chat messages client-side, by sending
693 // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
695 if (message.size() >= 2 && message[0] == L'<')
697 std::size_t closing = message.find_first_of(L'>', 1);
698 if (closing != std::wstring::npos &&
699 closing + 2 <= message.size() &&
700 message[closing+1] == L' ')
702 std::wstring name = message.substr(1, closing - 1);
703 std::wstring text = message.substr(closing + 2);
704 addMessage(name, text);
709 // Unable to parse, probably a server message.
710 addMessage(L"", message);
713 ChatBuffer& ChatBackend::getConsoleBuffer()
715 return m_console_buffer;
718 ChatBuffer& ChatBackend::getRecentBuffer()
720 return m_recent_buffer;
723 std::wstring ChatBackend::getRecentChat()
725 std::wostringstream stream;
726 for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
728 const ChatLine& line = m_recent_buffer.getLine(i);
731 if (!line.name.empty())
732 stream << L"<" << line.name << L"> ";
738 ChatPrompt& ChatBackend::getPrompt()
743 void ChatBackend::reformat(u32 cols, u32 rows)
745 m_console_buffer.reformat(cols, rows);
747 // no need to reformat m_recent_buffer, its formatted lines
750 m_prompt.reformat(cols);
753 void ChatBackend::clearRecentChat()
755 m_recent_buffer.clear();
758 void ChatBackend::step(float dtime)
760 m_recent_buffer.step(dtime);
761 m_recent_buffer.deleteByAge(60.0);
763 // no need to age messages in anything but m_recent_buffer
766 void ChatBackend::scroll(s32 rows)
768 m_console_buffer.scroll(rows);
771 void ChatBackend::scrollPageDown()
773 m_console_buffer.scroll(m_console_buffer.getRows());
776 void ChatBackend::scrollPageUp()
778 m_console_buffer.scroll(-(s32)m_console_buffer.getRows());