*/
#include "chat.h"
-#include "debug.h"
-#include "config.h"
-#include "util/strfnd.h"
+
+#include <algorithm>
#include <cctype>
#include <sstream>
+
+#include "config.h"
+#include "debug.h"
+#include "util/strfnd.h"
#include "util/string.h"
#include "util/numeric.h"
ChatBuffer::ChatBuffer(u32 scrollback):
- m_scrollback(scrollback),
- m_unformatted(),
- m_cols(0),
- m_rows(0),
- m_scroll(0),
- m_formatted(),
- m_empty_formatted_line()
+ m_scrollback(scrollback)
{
if (m_scrollback == 0)
m_scrollback = 1;
m_empty_formatted_line.first = true;
-}
-ChatBuffer::~ChatBuffer()
-{
+ m_cache_clickable_chat_weblinks = false;
+ // Curses mode cannot access g_settings here
+ if (g_settings != nullptr) {
+ m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
+ if (m_cache_clickable_chat_weblinks) {
+ std::string colorval = g_settings->get("chat_weblink_color");
+ parseColorString(colorval, m_cache_chat_weblink_color, false, 255);
+ m_cache_chat_weblink_color.setAlpha(255);
+ }
+ }
}
-void ChatBuffer::addLine(std::wstring name, std::wstring text)
+void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
{
+ m_lines_modified = true;
+
ChatLine line(name, text);
m_unformatted.push_back(line);
- if (m_rows > 0)
- {
+ if (m_rows > 0) {
// m_formatted is valid and must be kept valid
bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
u32 num_added = formatChatLine(line, m_cols, m_formatted);
}
// Limit number of lines by m_scrollback
- if (m_unformatted.size() > m_scrollback)
- {
+ if (m_unformatted.size() > m_scrollback) {
deleteOldest(m_unformatted.size() - m_scrollback);
}
}
m_unformatted.clear();
m_formatted.clear();
m_scroll = 0;
+ m_lines_modified = true;
}
u32 ChatBuffer::getLineCount() const
void ChatBuffer::step(f32 dtime)
{
- for (u32 i = 0; i < m_unformatted.size(); ++i)
- {
- m_unformatted[i].age += dtime;
+ for (ChatLine &line : m_unformatted) {
+ line.age += dtime;
}
}
u32 del_unformatted = 0;
u32 del_formatted = 0;
- while (count > 0 && del_unformatted < m_unformatted.size())
- {
+ while (count > 0 && del_unformatted < m_unformatted.size()) {
++del_unformatted;
// keep m_formatted in sync
- if (del_formatted < m_formatted.size())
- {
-
+ if (del_formatted < m_formatted.size()) {
sanity_check(m_formatted[del_formatted].first);
++del_formatted;
while (del_formatted < m_formatted.size() &&
m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
+ if (del_unformatted > 0)
+ m_lines_modified = true;
+
if (at_bottom)
m_scroll = getBottomScrollPos();
else
deleteOldest(count);
}
-u32 ChatBuffer::getColumns() const
-{
- return m_cols;
-}
-
u32 ChatBuffer::getRows() const
{
return m_rows;
s32 index = m_scroll + (s32) row;
if (index >= 0 && index < (s32) m_formatted.size())
return m_formatted[index];
- else
- return m_empty_formatted_line;
+
+ return m_empty_formatted_line;
}
void ChatBuffer::scroll(s32 rows)
m_scroll = getBottomScrollPos();
}
-void ChatBuffer::scrollTop()
-{
- m_scroll = getTopScrollPos();
-}
-
u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
std::vector<ChatFormattedLine>& destination) const
{
//EnrichedString line_text(line.text);
next_line.first = true;
- bool text_processing = false;
+ // Set/use forced newline after the last frag in each line
+ bool mark_newline = false;
// Produce fragments and layout them into lines
- while (!next_frags.empty() || in_pos < line.text.size())
- {
+ while (!next_frags.empty() || in_pos < line.text.size()) {
+ mark_newline = false; // now using this to USE line-end frag
+
// Layout fragments into lines
- while (!next_frags.empty())
- {
+ while (!next_frags.empty()) {
ChatFormattedFragment& frag = next_frags[0];
- if (frag.text.size() <= cols - out_column)
- {
+
+ // Force newline after this frag, if marked
+ if (frag.column == INT_MAX)
+ mark_newline = true;
+
+ if (frag.text.size() <= cols - out_column) {
// Fragment fits into current line
frag.column = out_column;
next_line.fragments.push_back(frag);
out_column += frag.text.size();
next_frags.erase(next_frags.begin());
- }
- else
- {
+ } else {
// Fragment does not fit into current line
// So split it up
temp_frag.text = frag.text.substr(0, cols - out_column);
temp_frag.column = out_column;
- //temp_frag.bold = frag.bold;
+ temp_frag.weblink = frag.weblink;
+
next_line.fragments.push_back(temp_frag);
frag.text = frag.text.substr(cols - out_column);
+ frag.column = 0;
out_column = cols;
}
- if (out_column == cols || text_processing)
- {
+
+ if (out_column == cols || mark_newline) {
// End the current line
destination.push_back(next_line);
num_added++;
next_line.fragments.clear();
next_line.first = false;
- out_column = text_processing ? hanging_indentation : 0;
+ out_column = hanging_indentation;
+ mark_newline = false;
}
}
- // Produce fragment
- if (in_pos < line.text.size())
- {
- u32 remaining_in_input = line.text.size() - in_pos;
- u32 remaining_in_output = cols - out_column;
+ // Produce fragment(s) for next formatted line
+ if (!(in_pos < line.text.size()))
+ continue;
+
+ const std::wstring &linestring = line.text.getString();
+ u32 remaining_in_output = cols - out_column;
+ size_t http_pos = std::wstring::npos;
+ mark_newline = false; // now using this to SET line-end frag
+ // Construct all frags for next output line
+ while (!mark_newline) {
// Determine a fragment length <= the minimum of
// remaining_in_{in,out}put. Try to end the fragment
// on a word boundary.
- u32 frag_length = 1, space_pos = 0;
+ u32 frag_length = 0, space_pos = 0;
+ u32 remaining_in_input = line.text.size() - in_pos;
+
+ if (m_cache_clickable_chat_weblinks) {
+ // Note: unsigned(-1) on fail
+ http_pos = linestring.find(L"https://", in_pos);
+ if (http_pos == std::wstring::npos)
+ http_pos = linestring.find(L"http://", in_pos);
+ if (http_pos != std::wstring::npos)
+ http_pos -= in_pos;
+ }
+
while (frag_length < remaining_in_input &&
- frag_length < remaining_in_output)
- {
- if (isspace(line.text.getString()[in_pos + frag_length]))
+ frag_length < remaining_in_output) {
+ if (iswspace(linestring[in_pos + frag_length]))
space_pos = frag_length;
++frag_length;
}
+
+ if (http_pos >= remaining_in_output) {
+ // Http not in range, grab until space or EOL, halt as normal.
+ // Note this works because (http_pos = npos) is unsigned(-1)
+
+ mark_newline = true;
+ } else if (http_pos == 0) {
+ // At http, grab ALL until FIRST whitespace or end marker. loop.
+ // If at end of string, next loop will be empty string to mark end of weblink.
+
+ frag_length = 6; // Frag is at least "http://"
+
+ // Chars to mark end of weblink
+ // TODO? replace this with a safer (slower) regex whitelist?
+ static const std::wstring delim_chars = L"\'\";,";
+ wchar_t tempchar = linestring[in_pos+frag_length];
+ while (frag_length < remaining_in_input &&
+ !iswspace(tempchar) &&
+ delim_chars.find(tempchar) == std::wstring::npos) {
+ ++frag_length;
+ tempchar = linestring[in_pos+frag_length];
+ }
+
+ space_pos = frag_length - 1;
+ // This frag may need to be force-split. That's ok, urls aren't "words"
+ if (frag_length >= remaining_in_output) {
+ mark_newline = true;
+ }
+ } else {
+ // Http in range, grab until http, loop
+
+ space_pos = http_pos - 1;
+ frag_length = http_pos;
+ }
+
+ // Include trailing space in current frag
if (space_pos != 0 && frag_length < remaining_in_input)
frag_length = space_pos + 1;
temp_frag.text = line.text.substr(in_pos, frag_length);
- temp_frag.column = 0;
- //temp_frag.bold = 0;
+ // A hack so this frag remembers mark_newline for the layout phase
+ temp_frag.column = mark_newline ? INT_MAX : 0;
+
+ if (http_pos == 0) {
+ // Discard color stuff from the source frag
+ temp_frag.text = EnrichedString(temp_frag.text.getString());
+ temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
+ // Set weblink in the frag meta
+ temp_frag.weblink = wide_to_utf8(temp_frag.text.getString());
+ } else {
+ temp_frag.weblink.clear();
+ }
next_frags.push_back(temp_frag);
in_pos += frag_length;
- text_processing = true;
+ remaining_in_output -= std::min(frag_length, remaining_in_output);
}
}
// End the last line
- if (num_added == 0 || !next_line.fragments.empty())
- {
+ if (num_added == 0 || !next_line.fragments.empty()) {
destination.push_back(next_line);
num_added++;
}
s32 rows = (s32) m_rows;
if (rows == 0)
return 0;
- else if (formatted_count <= rows)
+
+ if (formatted_count <= rows)
return formatted_count - rows;
- else
- return 0;
+
+ return 0;
}
s32 ChatBuffer::getBottomScrollPos() const
s32 rows = (s32) m_rows;
if (rows == 0)
return 0;
- else
- return formatted_count - rows;
+
+ return formatted_count - rows;
}
+void ChatBuffer::resize(u32 scrollback)
+{
+ m_scrollback = scrollback;
+ if (m_unformatted.size() > m_scrollback)
+ deleteOldest(m_unformatted.size() - m_scrollback);
+}
ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
m_prompt(prompt),
- m_line(L""),
- m_history(),
- m_history_index(0),
- m_history_limit(history_limit),
- m_cols(0),
- m_view(0),
- m_cursor(0),
- m_cursor_len(0),
- m_nick_completion_start(0),
- m_nick_completion_end(0)
-{
-}
-
-ChatPrompt::~ChatPrompt()
+ m_history_limit(history_limit)
{
}
m_nick_completion_end = 0;
}
-void ChatPrompt::addToHistory(std::wstring line)
+void ChatPrompt::addToHistory(const std::wstring &line)
{
- if (!line.empty())
+ if (!line.empty() &&
+ (m_history.size() == 0 || m_history.back() != line)) {
+ // Remove all duplicates
+ m_history.erase(std::remove(m_history.begin(), m_history.end(),
+ line), m_history.end());
+ // Push unique line
m_history.push_back(line);
+ }
if (m_history.size() > m_history_limit)
m_history.erase(m_history.begin());
m_history_index = m_history.size();
m_nick_completion_end = 0;
}
-std::wstring ChatPrompt::replace(std::wstring line)
+std::wstring ChatPrompt::replace(const std::wstring &line)
{
std::wstring old_line = m_line;
m_line = line;
{
// no previous nick completion is active
prefix_start = prefix_end = m_cursor;
- while (prefix_start > 0 && !isspace(m_line[prefix_start-1]))
+ while (prefix_start > 0 && !iswspace(m_line[prefix_start-1]))
--prefix_start;
- while (prefix_end < m_line.size() && !isspace(m_line[prefix_end]))
+ while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
++prefix_end;
if (prefix_start == prefix_end)
return;
// find all names that start with the selected prefix
std::vector<std::wstring> completions;
- for (std::list<std::string>::const_iterator
- i = names.begin();
- i != names.end(); ++i)
- {
- if (str_starts_with(narrow_to_wide(*i), prefix, true))
- {
- std::wstring completion = narrow_to_wide(*i);
+ for (const std::string &name : names) {
+ std::wstring completion = utf8_to_wide(name);
+ if (str_starts_with(completion, prefix, true)) {
if (prefix_start == 0)
completion += L": ";
completions.push_back(completion);
}
}
+
if (completions.empty())
return;
u32 replacement_index = 0;
if (!initial)
{
- while (word_end < m_line.size() && !isspace(m_line[word_end]))
+ while (word_end < m_line.size() && !iswspace(m_line[word_end]))
++word_end;
std::wstring word = m_line.substr(prefix_start, word_end - prefix_start);
}
}
std::wstring replacement = completions[replacement_index];
- if (word_end < m_line.size() && isspace(word_end))
+ if (word_end < m_line.size() && iswspace(m_line[word_end]))
++word_end;
// replace existing word with replacement word,
case CURSOROP_SCOPE_WORD:
if (dir == CURSOROP_DIR_RIGHT) {
// skip one word to the right
- while (new_cursor < length && isspace(m_line[new_cursor]))
+ while (new_cursor < length && iswspace(m_line[new_cursor]))
new_cursor++;
- while (new_cursor < length && !isspace(m_line[new_cursor]))
+ while (new_cursor < length && !iswspace(m_line[new_cursor]))
new_cursor++;
- while (new_cursor < length && isspace(m_line[new_cursor]))
+ while (new_cursor < length && iswspace(m_line[new_cursor]))
new_cursor++;
} else {
// skip one word to the left
- while (new_cursor >= 1 && isspace(m_line[new_cursor - 1]))
+ while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1]))
new_cursor--;
- while (new_cursor >= 1 && !isspace(m_line[new_cursor - 1]))
+ while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
new_cursor--;
}
break;
{
}
-ChatBackend::~ChatBackend()
-{
-}
-
-void ChatBackend::addMessage(std::wstring name, std::wstring text)
+void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
{
// Note: A message may consist of multiple lines, for example the MOTD.
+ text = translate_string(text);
WStrfnd fnd(text);
while (!fnd.at_end())
{
return m_recent_buffer;
}
-EnrichedString ChatBackend::getRecentChat()
+EnrichedString ChatBackend::getRecentChat() const
{
EnrichedString result;
- for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
- {
+ for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
const ChatLine& line = m_recent_buffer.getLine(i);
if (i != 0)
result += L"\n";
m_recent_buffer.clear();
}
+
+void ChatBackend::applySettings()
+{
+ u32 recent_lines = g_settings->getU32("recent_chat_messages");
+ recent_lines = rangelim(recent_lines, 2, 20);
+ m_recent_buffer.resize(recent_lines);
+}
+
void ChatBackend::step(float dtime)
{
m_recent_buffer.step(dtime);