3 Copyright (C) 2011 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 General Public License as published by
7 the Free Software Foundation; either version 2 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 General Public License for more details.
15 You should have received a copy of the GNU 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.
27 ChatBuffer::ChatBuffer(u32 scrollback):
28 m_scrollback(scrollback),
34 m_empty_formatted_line()
36 if (m_scrollback == 0)
38 m_empty_formatted_line.first = true;
41 ChatBuffer::~ChatBuffer()
45 void ChatBuffer::addLine(std::wstring name, std::wstring text)
47 ChatLine line(name, text);
48 m_unformatted.push_back(line);
52 // m_formatted is valid and must be kept valid
53 bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
54 u32 num_added = formatChatLine(line, m_cols, m_formatted);
55 if (scrolled_at_bottom)
56 m_scroll += num_added;
59 // Limit number of lines by m_scrollback
60 if (m_unformatted.size() > m_scrollback)
62 deleteOldest(m_unformatted.size() - m_scrollback);
66 void ChatBuffer::clear()
68 m_unformatted.clear();
73 u32 ChatBuffer::getLineCount() const
75 return m_unformatted.size();
78 u32 ChatBuffer::getScrollback() const
83 const ChatLine& ChatBuffer::getLine(u32 index) const
85 assert(index < getLineCount());
86 return m_unformatted[index];
89 void ChatBuffer::step(f32 dtime)
91 for (u32 i = 0; i < m_unformatted.size(); ++i)
93 m_unformatted[i].age += dtime;
97 void ChatBuffer::deleteOldest(u32 count)
99 u32 del_unformatted = 0;
100 u32 del_formatted = 0;
102 while (count > 0 && del_unformatted < m_unformatted.size())
106 // keep m_formatted in sync
107 if (del_formatted < m_formatted.size())
109 assert(m_formatted[del_formatted].first);
111 while (del_formatted < m_formatted.size() &&
112 !m_formatted[del_formatted].first)
119 m_unformatted.erase(0, del_unformatted);
120 m_formatted.erase(0, del_formatted);
123 void ChatBuffer::deleteByAge(f32 maxAge)
126 while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
131 u32 ChatBuffer::getColumns() const
136 u32 ChatBuffer::getRows() const
141 void ChatBuffer::reformat(u32 cols, u32 rows)
143 if (cols == 0 || rows == 0)
145 // Clear formatted buffer
151 else if (cols != m_cols || rows != m_rows)
153 // TODO: Avoid reformatting ALL lines (even inivisble ones)
154 // each time the console size changes.
156 // Find out the scroll position in *unformatted* lines
157 u32 restore_scroll_unformatted = 0;
158 u32 restore_scroll_formatted = 0;
159 bool at_bottom = (m_scroll == getBottomScrollPos());
162 for (s32 i = 0; i < m_scroll; ++i)
164 if (m_formatted[i].first)
165 ++restore_scroll_unformatted;
169 // If number of columns change, reformat everything
173 for (u32 i = 0; i < m_unformatted.size(); ++i)
175 if (i == restore_scroll_unformatted)
176 restore_scroll_formatted = m_formatted.size();
177 formatChatLine(m_unformatted[i], cols, m_formatted);
181 // Update the console size
185 // Restore the scroll position
192 scrollAbsolute(restore_scroll_formatted);
197 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
199 s32 index = m_scroll + (s32) row;
200 if (index >= 0 && index < (s32) m_formatted.size())
201 return m_formatted[index];
203 return m_empty_formatted_line;
206 void ChatBuffer::scroll(s32 rows)
208 scrollAbsolute(m_scroll + rows);
211 void ChatBuffer::scrollAbsolute(s32 scroll)
213 s32 top = getTopScrollPos();
214 s32 bottom = getBottomScrollPos();
219 if (m_scroll > bottom)
223 void ChatBuffer::scrollBottom()
225 m_scroll = getBottomScrollPos();
228 void ChatBuffer::scrollTop()
230 m_scroll = getTopScrollPos();
233 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
234 core::array<ChatFormattedLine>& destination) const
237 core::array<ChatFormattedFragment> next_frags;
238 ChatFormattedLine next_line;
239 ChatFormattedFragment temp_frag;
242 u32 hanging_indentation = 0;
244 // Format the sender name and produce fragments
245 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);
261 // Choose an indentation level
262 if (line.name.empty())
265 hanging_indentation = 0;
267 else if (line.name.size() + 3 <= cols/2)
269 // Names shorter than about half the console width
270 hanging_indentation = line.name.size() + 3;
275 hanging_indentation = 2;
278 next_line.first = true;
279 bool text_processing = false;
281 // Produce fragments and layout them into lines
282 while (!next_frags.empty() || in_pos < line.text.size())
284 // Layout fragments into lines
285 while (!next_frags.empty())
287 ChatFormattedFragment& frag = next_frags[0];
288 if (frag.text.size() <= cols - out_column)
290 // Fragment fits into current line
291 frag.column = out_column;
292 next_line.fragments.push_back(frag);
293 out_column += frag.text.size();
294 next_frags.erase(0, 1);
298 // Fragment does not fit into current line
300 temp_frag.text = frag.text.substr(0, cols - out_column);
301 temp_frag.column = out_column;
302 //temp_frag.bold = frag.bold;
303 next_line.fragments.push_back(temp_frag);
304 frag.text = frag.text.substr(cols - out_column);
307 if (out_column == cols || text_processing)
309 // End the current line
310 destination.push_back(next_line);
312 next_line.fragments.clear();
313 next_line.first = false;
315 out_column = text_processing ? hanging_indentation : 0;
320 if (in_pos < line.text.size())
322 u32 remaining_in_input = line.text.size() - in_pos;
323 u32 remaining_in_output = cols - out_column;
325 // Determine a fragment length <= the minimum of
326 // remaining_in_{in,out}put. Try to end the fragment
327 // on a word boundary.
328 u32 frag_length = 1, space_pos = 0;
329 while (frag_length < remaining_in_input &&
330 frag_length < remaining_in_output)
332 if (isspace(line.text[in_pos + frag_length]))
333 space_pos = frag_length;
336 if (space_pos != 0 && frag_length < remaining_in_input)
337 frag_length = space_pos + 1;
339 temp_frag.text = line.text.substr(in_pos, frag_length);
340 temp_frag.column = 0;
341 //temp_frag.bold = 0;
342 next_frags.push_back(temp_frag);
343 in_pos += frag_length;
344 text_processing = true;
349 if (num_added == 0 || !next_line.fragments.empty())
351 destination.push_back(next_line);
358 s32 ChatBuffer::getTopScrollPos() const
360 s32 formatted_count = (s32) m_formatted.size();
361 s32 rows = (s32) m_rows;
364 else if (formatted_count <= rows)
365 return formatted_count - rows;
370 s32 ChatBuffer::getBottomScrollPos() const
372 s32 formatted_count = (s32) m_formatted.size();
373 s32 rows = (s32) m_rows;
377 return formatted_count - rows;
382 ChatPrompt::ChatPrompt(std::wstring prompt, u32 history_limit):
387 m_history_limit(history_limit),
391 m_nick_completion_start(0),
392 m_nick_completion_end(0)
396 ChatPrompt::~ChatPrompt()
400 void ChatPrompt::input(wchar_t ch)
402 m_line.insert(m_cursor, 1, ch);
405 m_nick_completion_start = 0;
406 m_nick_completion_end = 0;
409 std::wstring ChatPrompt::submit()
411 std::wstring line = m_line;
414 m_history.push_back(line);
415 if (m_history.size() > m_history_limit)
417 m_history_index = m_history.size();
420 m_nick_completion_start = 0;
421 m_nick_completion_end = 0;
425 void ChatPrompt::clear()
430 m_nick_completion_start = 0;
431 m_nick_completion_end = 0;
434 void ChatPrompt::replace(std::wstring line)
437 m_view = m_cursor = line.size();
439 m_nick_completion_start = 0;
440 m_nick_completion_end = 0;
443 void ChatPrompt::historyPrev()
445 if (m_history_index != 0)
448 replace(m_history[m_history_index]);
452 void ChatPrompt::historyNext()
454 if (m_history_index + 1 >= m_history.size())
456 m_history_index = m_history.size();
462 replace(m_history[m_history_index]);
466 void ChatPrompt::nickCompletion(const core::list<std::wstring>& names, bool backwards)
469 // (a) m_nick_completion_start == m_nick_completion_end == 0
470 // Then no previous nick completion is active.
471 // Get the word around the cursor and replace with any nick
472 // that has that word as a prefix.
473 // (b) else, continue a previous nick completion.
474 // m_nick_completion_start..m_nick_completion_end are the
475 // interval where the originally used prefix was. Cycle
476 // through the list of completions of that prefix.
477 u32 prefix_start = m_nick_completion_start;
478 u32 prefix_end = m_nick_completion_end;
479 bool initial = (prefix_end == 0);
482 // no previous nick completion is active
483 prefix_start = prefix_end = m_cursor;
484 while (prefix_start > 0 && !isspace(m_line[prefix_start-1]))
486 while (prefix_end < m_line.size() && !isspace(m_line[prefix_end]))
488 if (prefix_start == prefix_end)
491 std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start);
493 // find all names that start with the selected prefix
494 core::array<std::wstring> completions;
495 for (core::list<std::wstring>::ConstIterator
497 i != names.end(); i++)
499 if (str_starts_with(*i, prefix, true))
501 std::wstring completion = *i;
502 if (prefix_start == 0)
504 completions.push_back(completion);
507 if (completions.empty())
510 // find a replacement string and the word that will be replaced
511 u32 word_end = prefix_end;
512 u32 replacement_index = 0;
515 while (word_end < m_line.size() && !isspace(m_line[word_end]))
517 std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
519 // cycle through completions
520 for (u32 i = 0; i < completions.size(); ++i)
522 if (str_equal(word, completions[i], true))
525 replacement_index = i + completions.size() - 1;
527 replacement_index = i + 1;
528 replacement_index %= completions.size();
533 std::wstring replacement = completions[replacement_index] + L" ";
534 if (word_end < m_line.size() && isspace(word_end))
537 // replace existing word with replacement word,
538 // place the cursor at the end and record the completion prefix
539 m_line.replace(prefix_start, word_end - prefix_start, replacement);
540 m_cursor = prefix_start + replacement.size();
542 m_nick_completion_start = prefix_start;
543 m_nick_completion_end = prefix_end;
546 void ChatPrompt::reformat(u32 cols)
548 if (cols <= m_prompt.size())
555 s32 length = m_line.size();
556 bool was_at_end = (m_view + m_cols >= length + 1);
557 m_cols = cols - m_prompt.size();
564 std::wstring ChatPrompt::getVisiblePortion() const
566 return m_prompt + m_line.substr(m_view, m_cols);
569 s32 ChatPrompt::getVisibleCursorPosition() const
571 return m_cursor - m_view + m_prompt.size();
574 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
576 s32 old_cursor = m_cursor;
577 s32 new_cursor = m_cursor;
579 s32 length = m_line.size();
580 s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
582 if (scope == CURSOROP_SCOPE_CHARACTER)
584 new_cursor += increment;
586 else if (scope == CURSOROP_SCOPE_WORD)
590 // skip one word to the right
591 while (new_cursor < length && isspace(m_line[new_cursor]))
593 while (new_cursor < length && !isspace(m_line[new_cursor]))
595 while (new_cursor < length && isspace(m_line[new_cursor]))
600 // skip one word to the left
601 while (new_cursor >= 1 && isspace(m_line[new_cursor - 1]))
603 while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1]))
607 else if (scope == CURSOROP_SCOPE_LINE)
609 new_cursor += increment * length;
612 new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
614 if (op == CURSOROP_MOVE)
616 m_cursor = new_cursor;
618 else if (op == CURSOROP_DELETE)
620 if (new_cursor < old_cursor)
622 m_line.erase(new_cursor, old_cursor - new_cursor);
623 m_cursor = new_cursor;
625 else if (new_cursor > old_cursor)
627 m_line.erase(old_cursor, new_cursor - old_cursor);
628 m_cursor = old_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 ChatBackend::~ChatBackend()
667 void ChatBackend::addMessage(std::wstring name, std::wstring text)
669 // Note: A message may consist of multiple lines, for example the MOTD.
673 std::wstring line = fnd.next(L"\n");
674 m_console_buffer.addLine(name, line);
675 m_recent_buffer.addLine(name, line);
679 void ChatBackend::addUnparsedMessage(std::wstring message)
681 // TODO: Remove the need to parse chat messages client-side, by sending
682 // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
684 if (message.size() >= 2 && message[0] == L'<')
686 std::size_t closing = message.find_first_of(L'>', 1);
687 if (closing != std::wstring::npos &&
688 closing + 2 <= message.size() &&
689 message[closing+1] == L' ')
691 std::wstring name = message.substr(1, closing - 1);
692 std::wstring text = message.substr(closing + 2);
693 addMessage(name, text);
698 // Unable to parse, probably a server message.
699 addMessage(L"", message);
702 ChatBuffer& ChatBackend::getConsoleBuffer()
704 return m_console_buffer;
707 ChatBuffer& ChatBackend::getRecentBuffer()
709 return m_recent_buffer;
712 std::wstring ChatBackend::getRecentChat()
714 std::wostringstream stream;
715 for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
717 const ChatLine& line = m_recent_buffer.getLine(i);
720 if (!line.name.empty())
721 stream << L"<" << line.name << L"> ";
727 ChatPrompt& ChatBackend::getPrompt()
732 void ChatBackend::reformat(u32 cols, u32 rows)
734 m_console_buffer.reformat(cols, rows);
736 // no need to reformat m_recent_buffer, its formatted lines
739 m_prompt.reformat(cols);
742 void ChatBackend::clearRecentChat()
744 m_recent_buffer.clear();
747 void ChatBackend::step(float dtime)
749 m_recent_buffer.step(dtime);
750 m_recent_buffer.deleteByAge(60.0);
752 // no need to age messages in anything but m_recent_buffer
755 void ChatBackend::scroll(s32 rows)
757 m_console_buffer.scroll(rows);
760 void ChatBackend::scrollPageDown()
762 m_console_buffer.scroll(m_console_buffer.getRows());
765 void ChatBackend::scrollPageUp()
767 m_console_buffer.scroll(-m_console_buffer.getRows());