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::getRows() const
131 void ChatBuffer::scrollTop()
133 m_scroll
= getTopScrollPos();
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 u32
ChatBuffer::formatChatLine(const ChatLine
& line
, u32 cols
,
224 std::vector
<ChatFormattedLine
>& destination
) const
227 std::vector
<ChatFormattedFragment
> next_frags
;
228 ChatFormattedLine next_line
;
229 ChatFormattedFragment temp_frag
;
232 u32 hanging_indentation
= 0;
234 // Format the sender name and produce fragments
235 if (!line
.name
.empty()) {
236 temp_frag
.text
= L
"<";
237 temp_frag
.column
= 0;
238 //temp_frag.bold = 0;
239 next_frags
.push_back(temp_frag
);
240 temp_frag
.text
= line
.name
;
241 temp_frag
.column
= 0;
242 //temp_frag.bold = 1;
243 next_frags
.push_back(temp_frag
);
244 temp_frag
.text
= L
"> ";
245 temp_frag
.column
= 0;
246 //temp_frag.bold = 0;
247 next_frags
.push_back(temp_frag
);
250 std::wstring name_sanitized
= line
.name
.c_str();
252 // Choose an indentation level
253 if (line
.name
.empty()) {
255 hanging_indentation
= 0;
256 } else if (name_sanitized
.size() + 3 <= cols
/2) {
257 // Names shorter than about half the console width
258 hanging_indentation
= line
.name
.size() + 3;
261 hanging_indentation
= 2;
263 //EnrichedString line_text(line.text);
265 next_line
.first
= true;
266 bool text_processing
= false;
268 // Produce fragments and layout them into lines
269 while (!next_frags
.empty() || in_pos
< line
.text
.size())
271 // Layout fragments into lines
272 while (!next_frags
.empty())
274 ChatFormattedFragment
& frag
= next_frags
[0];
275 if (frag
.text
.size() <= cols
- out_column
)
277 // Fragment fits into current line
278 frag
.column
= out_column
;
279 next_line
.fragments
.push_back(frag
);
280 out_column
+= frag
.text
.size();
281 next_frags
.erase(next_frags
.begin());
285 // Fragment does not fit into current line
287 temp_frag
.text
= frag
.text
.substr(0, cols
- out_column
);
288 temp_frag
.column
= out_column
;
289 //temp_frag.bold = frag.bold;
290 next_line
.fragments
.push_back(temp_frag
);
291 frag
.text
= frag
.text
.substr(cols
- out_column
);
294 if (out_column
== cols
|| text_processing
)
296 // End the current line
297 destination
.push_back(next_line
);
299 next_line
.fragments
.clear();
300 next_line
.first
= false;
302 out_column
= text_processing
? hanging_indentation
: 0;
307 if (in_pos
< line
.text
.size())
309 u32 remaining_in_input
= line
.text
.size() - in_pos
;
310 u32 remaining_in_output
= cols
- out_column
;
312 // Determine a fragment length <= the minimum of
313 // remaining_in_{in,out}put. Try to end the fragment
314 // on a word boundary.
315 u32 frag_length
= 1, space_pos
= 0;
316 while (frag_length
< remaining_in_input
&&
317 frag_length
< remaining_in_output
)
319 if (iswspace(line
.text
.getString()[in_pos
+ frag_length
]))
320 space_pos
= frag_length
;
323 if (space_pos
!= 0 && frag_length
< remaining_in_input
)
324 frag_length
= space_pos
+ 1;
326 temp_frag
.text
= line
.text
.substr(in_pos
, frag_length
);
327 temp_frag
.column
= 0;
328 //temp_frag.bold = 0;
329 next_frags
.push_back(temp_frag
);
330 in_pos
+= frag_length
;
331 text_processing
= true;
336 if (num_added
== 0 || !next_line
.fragments
.empty())
338 destination
.push_back(next_line
);
345 s32
ChatBuffer::getTopScrollPos() const
347 s32 formatted_count
= (s32
) m_formatted
.size();
348 s32 rows
= (s32
) m_rows
;
352 if (formatted_count
<= rows
)
353 return formatted_count
- rows
;
358 s32
ChatBuffer::getBottomScrollPos() const
360 s32 formatted_count
= (s32
) m_formatted
.size();
361 s32 rows
= (s32
) m_rows
;
365 return formatted_count
- rows
;
368 void ChatBuffer::resize(u32 scrollback
)
370 m_scrollback
= scrollback
;
371 if (m_unformatted
.size() > m_scrollback
)
372 deleteOldest(m_unformatted
.size() - m_scrollback
);
376 ChatPrompt::ChatPrompt(const std::wstring
&prompt
, u32 history_limit
):
378 m_history_limit(history_limit
)
382 void ChatPrompt::input(wchar_t ch
)
384 m_line
.insert(m_cursor
, 1, ch
);
387 m_nick_completion_start
= 0;
388 m_nick_completion_end
= 0;
391 void ChatPrompt::input(const std::wstring
&str
)
393 m_line
.insert(m_cursor
, str
);
394 m_cursor
+= str
.size();
396 m_nick_completion_start
= 0;
397 m_nick_completion_end
= 0;
400 void ChatPrompt::addToHistory(const std::wstring
&line
)
403 (m_history
.size() == 0 || m_history
.back() != line
)) {
404 // Remove all duplicates
405 m_history
.erase(std::remove(m_history
.begin(), m_history
.end(),
406 line
), m_history
.end());
408 m_history
.push_back(line
);
410 if (m_history
.size() > m_history_limit
)
411 m_history
.erase(m_history
.begin());
412 m_history_index
= m_history
.size();
415 void ChatPrompt::clear()
420 m_nick_completion_start
= 0;
421 m_nick_completion_end
= 0;
424 std::wstring
ChatPrompt::replace(const std::wstring
&line
)
426 std::wstring old_line
= m_line
;
428 m_view
= m_cursor
= line
.size();
430 m_nick_completion_start
= 0;
431 m_nick_completion_end
= 0;
435 void ChatPrompt::historyPrev()
437 if (m_history_index
!= 0)
440 replace(m_history
[m_history_index
]);
444 void ChatPrompt::historyNext()
446 if (m_history_index
+ 1 >= m_history
.size())
448 m_history_index
= m_history
.size();
454 replace(m_history
[m_history_index
]);
458 void ChatPrompt::nickCompletion(const std::list
<std::string
>& names
, bool backwards
)
461 // (a) m_nick_completion_start == m_nick_completion_end == 0
462 // Then no previous nick completion is active.
463 // Get the word around the cursor and replace with any nick
464 // that has that word as a prefix.
465 // (b) else, continue a previous nick completion.
466 // m_nick_completion_start..m_nick_completion_end are the
467 // interval where the originally used prefix was. Cycle
468 // through the list of completions of that prefix.
469 u32 prefix_start
= m_nick_completion_start
;
470 u32 prefix_end
= m_nick_completion_end
;
471 bool initial
= (prefix_end
== 0);
474 // no previous nick completion is active
475 prefix_start
= prefix_end
= m_cursor
;
476 while (prefix_start
> 0 && !iswspace(m_line
[prefix_start
-1]))
478 while (prefix_end
< m_line
.size() && !iswspace(m_line
[prefix_end
]))
480 if (prefix_start
== prefix_end
)
483 std::wstring prefix
= m_line
.substr(prefix_start
, prefix_end
- prefix_start
);
485 // find all names that start with the selected prefix
486 std::vector
<std::wstring
> completions
;
487 for (const std::string
&name
: names
) {
488 if (str_starts_with(narrow_to_wide(name
), prefix
, true)) {
489 std::wstring completion
= narrow_to_wide(name
);
490 if (prefix_start
== 0)
492 completions
.push_back(completion
);
496 if (completions
.empty())
499 // find a replacement string and the word that will be replaced
500 u32 word_end
= prefix_end
;
501 u32 replacement_index
= 0;
504 while (word_end
< m_line
.size() && !iswspace(m_line
[word_end
]))
506 std::wstring word
= m_line
.substr(prefix_start
, word_end
- prefix_start
);
508 // cycle through completions
509 for (u32 i
= 0; i
< completions
.size(); ++i
)
511 if (str_equal(word
, completions
[i
], true))
514 replacement_index
= i
+ completions
.size() - 1;
516 replacement_index
= i
+ 1;
517 replacement_index
%= completions
.size();
522 std::wstring replacement
= completions
[replacement_index
];
523 if (word_end
< m_line
.size() && iswspace(m_line
[word_end
]))
526 // replace existing word with replacement word,
527 // place the cursor at the end and record the completion prefix
528 m_line
.replace(prefix_start
, word_end
- prefix_start
, replacement
);
529 m_cursor
= prefix_start
+ replacement
.size();
531 m_nick_completion_start
= prefix_start
;
532 m_nick_completion_end
= prefix_end
;
535 void ChatPrompt::reformat(u32 cols
)
537 if (cols
<= m_prompt
.size())
544 s32 length
= m_line
.size();
545 bool was_at_end
= (m_view
+ m_cols
>= length
+ 1);
546 m_cols
= cols
- m_prompt
.size();
553 std::wstring
ChatPrompt::getVisiblePortion() const
555 return m_prompt
+ m_line
.substr(m_view
, m_cols
);
558 s32
ChatPrompt::getVisibleCursorPosition() const
560 return m_cursor
- m_view
+ m_prompt
.size();
563 void ChatPrompt::cursorOperation(CursorOp op
, CursorOpDir dir
, CursorOpScope scope
)
565 s32 old_cursor
= m_cursor
;
566 s32 new_cursor
= m_cursor
;
568 s32 length
= m_line
.size();
569 s32 increment
= (dir
== CURSOROP_DIR_RIGHT
) ? 1 : -1;
572 case CURSOROP_SCOPE_CHARACTER
:
573 new_cursor
+= increment
;
575 case CURSOROP_SCOPE_WORD
:
576 if (dir
== CURSOROP_DIR_RIGHT
) {
577 // skip one word to the right
578 while (new_cursor
< length
&& iswspace(m_line
[new_cursor
]))
580 while (new_cursor
< length
&& !iswspace(m_line
[new_cursor
]))
582 while (new_cursor
< length
&& iswspace(m_line
[new_cursor
]))
585 // skip one word to the left
586 while (new_cursor
>= 1 && iswspace(m_line
[new_cursor
- 1]))
588 while (new_cursor
>= 1 && !iswspace(m_line
[new_cursor
- 1]))
592 case CURSOROP_SCOPE_LINE
:
593 new_cursor
+= increment
* length
;
595 case CURSOROP_SCOPE_SELECTION
:
599 new_cursor
= MYMAX(MYMIN(new_cursor
, length
), 0);
603 m_cursor
= new_cursor
;
606 case CURSOROP_DELETE
:
607 if (m_cursor_len
> 0) { // Delete selected text first
608 m_line
.erase(m_cursor
, m_cursor_len
);
610 m_cursor
= MYMIN(new_cursor
, old_cursor
);
611 m_line
.erase(m_cursor
, abs(new_cursor
- old_cursor
));
615 case CURSOROP_SELECT
:
616 if (scope
== CURSOROP_SCOPE_LINE
) {
618 m_cursor_len
= length
;
620 m_cursor
= MYMIN(new_cursor
, old_cursor
);
621 m_cursor_len
+= abs(new_cursor
- old_cursor
);
622 m_cursor_len
= MYMIN(m_cursor_len
, length
- m_cursor
);
629 m_nick_completion_start
= 0;
630 m_nick_completion_end
= 0;
633 void ChatPrompt::clampView()
635 s32 length
= m_line
.size();
636 if (length
+ 1 <= m_cols
)
642 m_view
= MYMIN(m_view
, length
+ 1 - m_cols
);
643 m_view
= MYMIN(m_view
, m_cursor
);
644 m_view
= MYMAX(m_view
, m_cursor
- m_cols
+ 1);
645 m_view
= MYMAX(m_view
, 0);
651 ChatBackend::ChatBackend():
652 m_console_buffer(500),
658 void ChatBackend::addMessage(const std::wstring
&name
, std::wstring text
)
660 // Note: A message may consist of multiple lines, for example the MOTD.
661 text
= translate_string(text
);
663 while (!fnd
.at_end())
665 std::wstring line
= fnd
.next(L
"\n");
666 m_console_buffer
.addLine(name
, line
);
667 m_recent_buffer
.addLine(name
, line
);
671 void ChatBackend::addUnparsedMessage(std::wstring message
)
673 // TODO: Remove the need to parse chat messages client-side, by sending
674 // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
676 if (message
.size() >= 2 && message
[0] == L
'<')
678 std::size_t closing
= message
.find_first_of(L
'>', 1);
679 if (closing
!= std::wstring::npos
&&
680 closing
+ 2 <= message
.size() &&
681 message
[closing
+1] == L
' ')
683 std::wstring name
= message
.substr(1, closing
- 1);
684 std::wstring text
= message
.substr(closing
+ 2);
685 addMessage(name
, text
);
690 // Unable to parse, probably a server message.
691 addMessage(L
"", message
);
694 ChatBuffer
& ChatBackend::getConsoleBuffer()
696 return m_console_buffer
;
699 ChatBuffer
& ChatBackend::getRecentBuffer()
701 return m_recent_buffer
;
704 EnrichedString
ChatBackend::getRecentChat() const
706 EnrichedString result
;
707 for (u32 i
= 0; i
< m_recent_buffer
.getLineCount(); ++i
) {
708 const ChatLine
& line
= m_recent_buffer
.getLine(i
);
711 if (!line
.name
.empty()) {
721 ChatPrompt
& ChatBackend::getPrompt()
726 void ChatBackend::reformat(u32 cols
, u32 rows
)
728 m_console_buffer
.reformat(cols
, rows
);
730 // no need to reformat m_recent_buffer, its formatted lines
733 m_prompt
.reformat(cols
);
736 void ChatBackend::clearRecentChat()
738 m_recent_buffer
.clear();
742 void ChatBackend::applySettings()
744 u32 recent_lines
= g_settings
->getU32("recent_chat_messages");
745 recent_lines
= rangelim(recent_lines
, 2, 20);
746 m_recent_buffer
.resize(recent_lines
);
749 void ChatBackend::step(float dtime
)
751 m_recent_buffer
.step(dtime
);
752 m_recent_buffer
.deleteByAge(60.0);
754 // no need to age messages in anything but m_recent_buffer
757 void ChatBackend::scroll(s32 rows
)
759 m_console_buffer
.scroll(rows
);
762 void ChatBackend::scrollPageDown()
764 m_console_buffer
.scroll(m_console_buffer
.getRows());
767 void ChatBackend::scrollPageUp()
769 m_console_buffer
.scroll(-(s32
)m_console_buffer
.getRows());