]> git.lizzy.rs Git - dragonfireclient.git/blobdiff - src/chat.cpp
Fix BSD iconv declaration
[dragonfireclient.git] / src / chat.cpp
index 1fb872b8527badb4d5fa4ae1696601b84c7d5970..92df038e83ab0f69d42ab07c699d526fccfe271a 100644 (file)
@@ -18,38 +18,44 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 */
 
 #include "chat.h"
-#include "debug.h"
-#include "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);
@@ -58,8 +64,7 @@ void ChatBuffer::addLine(std::wstring name, std::wstring text)
        }
 
        // 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);
        }
 }
@@ -69,6 +74,7 @@ void ChatBuffer::clear()
        m_unformatted.clear();
        m_formatted.clear();
        m_scroll = 0;
+       m_lines_modified = true;
 }
 
 u32 ChatBuffer::getLineCount() const
@@ -76,38 +82,32 @@ u32 ChatBuffer::getLineCount() const
        return m_unformatted.size();
 }
 
-u32 ChatBuffer::getScrollback() const
-{
-       return m_scrollback;
-}
-
 const ChatLine& ChatBuffer::getLine(u32 index) const
 {
-       assert(index < getLineCount());
+       assert(index < getLineCount()); // pre-condition
        return m_unformatted[index];
 }
 
 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;
        }
 }
 
 void ChatBuffer::deleteOldest(u32 count)
 {
+       bool at_bottom = (m_scroll == getBottomScrollPos());
+
        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())
-               {
-                       assert(m_formatted[del_formatted].first);
+               if (del_formatted < m_formatted.size()) {
+                       sanity_check(m_formatted[del_formatted].first);
                        ++del_formatted;
                        while (del_formatted < m_formatted.size() &&
                                        !m_formatted[del_formatted].first)
@@ -119,6 +119,14 @@ void ChatBuffer::deleteOldest(u32 count)
 
        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
+               scrollAbsolute(m_scroll - del_formatted);
 }
 
 void ChatBuffer::deleteByAge(f32 maxAge)
@@ -129,11 +137,6 @@ void ChatBuffer::deleteByAge(f32 maxAge)
        deleteOldest(count);
 }
 
-u32 ChatBuffer::getColumns() const
-{
-       return m_cols;
-}
-
 u32 ChatBuffer::getRows() const
 {
        return m_rows;
@@ -200,8 +203,8 @@ const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
        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)
@@ -226,11 +229,6 @@ void ChatBuffer::scrollBottom()
        m_scroll = getBottomScrollPos();
 }
 
-void ChatBuffer::scrollTop()
-{
-       m_scroll = getTopScrollPos();
-}
-
 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
                std::vector<ChatFormattedLine>& destination) const
 {
@@ -243,8 +241,7 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
        u32 hanging_indentation = 0;
 
        // Format the sender name and produce fragments
-       if (!line.name.empty())
-       {
+       if (!line.name.empty()) {
                temp_frag.text = L"<";
                temp_frag.column = 0;
                //temp_frag.bold = 0;
@@ -259,96 +256,160 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
                next_frags.push_back(temp_frag);
        }
 
+       std::wstring name_sanitized = line.name.c_str();
+
        // Choose an indentation level
-       if (line.name.empty())
-       {
+       if (line.name.empty()) {
                // Server messages
                hanging_indentation = 0;
-       }
-       else if (line.name.size() + 3 <= cols/2)
-       {
+       } else if (name_sanitized.size() + 3 <= cols/2) {
                // Names shorter than about half the console width
                hanging_indentation = line.name.size() + 3;
-       }
-       else
-       {
+       } else {
                // Very long names
                hanging_indentation = 2;
        }
+       //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[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++;
        }
@@ -362,10 +423,11 @@ s32 ChatBuffer::getTopScrollPos() const
        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
@@ -374,27 +436,21 @@ s32 ChatBuffer::getBottomScrollPos() const
        s32 rows = (s32) m_rows;
        if (rows == 0)
                return 0;
-       else
-               return formatted_count - rows;
-}
-
 
+       return formatted_count - rows;
+}
 
-ChatPrompt::ChatPrompt(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_nick_completion_start(0),
-       m_nick_completion_end(0)
+void ChatBuffer::resize(u32 scrollback)
 {
+       m_scrollback = scrollback;
+       if (m_unformatted.size() > m_scrollback)
+               deleteOldest(m_unformatted.size() - m_scrollback);
 }
 
-ChatPrompt::~ChatPrompt()
+
+ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
+       m_prompt(prompt),
+       m_history_limit(history_limit)
 {
 }
 
@@ -416,20 +472,19 @@ void ChatPrompt::input(const std::wstring &str)
        m_nick_completion_end = 0;
 }
 
-std::wstring ChatPrompt::submit()
+void ChatPrompt::addToHistory(const std::wstring &line)
 {
-       std::wstring line = m_line;
-       m_line.clear();
-       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_view = 0;
-       m_cursor = 0;
-       m_nick_completion_start = 0;
-       m_nick_completion_end = 0;
-       return line;
 }
 
 void ChatPrompt::clear()
@@ -441,13 +496,15 @@ void ChatPrompt::clear()
        m_nick_completion_end = 0;
 }
 
-void ChatPrompt::replace(std::wstring line)
+std::wstring ChatPrompt::replace(const std::wstring &line)
 {
+       std::wstring old_line = m_line;
        m_line =  line;
        m_view = m_cursor = line.size();
        clampView();
        m_nick_completion_start = 0;
        m_nick_completion_end = 0;
+       return old_line;
 }
 
 void ChatPrompt::historyPrev()
@@ -491,9 +548,9 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
        {
                // 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;
@@ -502,18 +559,15 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
 
        // 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":";
+                               completion += L": ";
                        completions.push_back(completion);
                }
        }
+
        if (completions.empty())
                return;
 
@@ -522,7 +576,7 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
        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);
 
@@ -540,8 +594,8 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
                        }
                }
        }
-       std::wstring replacement = completions[replacement_index] + L" ";
-       if (word_end < m_line.size() && isspace(word_end))
+       std::wstring replacement = completions[replacement_index];
+       if (word_end < m_line.size() && iswspace(m_line[word_end]))
                ++word_end;
 
        // replace existing word with replacement word,
@@ -589,54 +643,60 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
        s32 length = m_line.size();
        s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
 
-       if (scope == CURSOROP_SCOPE_CHARACTER)
-       {
+       switch (scope) {
+       case CURSOROP_SCOPE_CHARACTER:
                new_cursor += increment;
-       }
-       else if (scope == CURSOROP_SCOPE_WORD)
-       {
-               if (increment > 0)
-               {
+               break;
+       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
-               {
+               } 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--;
                }
-       }
-       else if (scope == CURSOROP_SCOPE_LINE)
-       {
+               break;
+       case CURSOROP_SCOPE_LINE:
                new_cursor += increment * length;
+               break;
+       case CURSOROP_SCOPE_SELECTION:
+               break;
        }
 
        new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
 
-       if (op == CURSOROP_MOVE)
-       {
+       switch (op) {
+       case CURSOROP_MOVE:
                m_cursor = new_cursor;
-       }
-       else if (op == CURSOROP_DELETE)
-       {
-               if (new_cursor < old_cursor)
-               {
-                       m_line.erase(new_cursor, old_cursor - new_cursor);
-                       m_cursor = new_cursor;
+               m_cursor_len = 0;
+               break;
+       case CURSOROP_DELETE:
+               if (m_cursor_len > 0) { // Delete selected text first
+                       m_line.erase(m_cursor, m_cursor_len);
+               } else {
+                       m_cursor = MYMIN(new_cursor, old_cursor);
+                       m_line.erase(m_cursor, abs(new_cursor - old_cursor));
                }
-               else if (new_cursor > old_cursor)
-               {
-                       m_line.erase(old_cursor, new_cursor - old_cursor);
-                       m_cursor = old_cursor;
+               m_cursor_len = 0;
+               break;
+       case CURSOROP_SELECT:
+               if (scope == CURSOROP_SCOPE_LINE) {
+                       m_cursor = 0;
+                       m_cursor_len = length;
+               } else {
+                       m_cursor = MYMIN(new_cursor, old_cursor);
+                       m_cursor_len += abs(new_cursor - old_cursor);
+                       m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
                }
+               break;
        }
 
        clampView();
@@ -670,15 +730,12 @@ ChatBackend::ChatBackend():
 {
 }
 
-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.atend())
+       while (!fnd.at_end())
        {
                std::wstring line = fnd.next(L"\n");
                m_console_buffer.addLine(name, line);
@@ -719,19 +776,21 @@ ChatBuffer& ChatBackend::getRecentBuffer()
        return m_recent_buffer;
 }
 
-std::wstring ChatBackend::getRecentChat()
+EnrichedString ChatBackend::getRecentChat() const
 {
-       std::wostringstream stream;
-       for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i)
-       {
+       EnrichedString result;
+       for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
                const ChatLine& line = m_recent_buffer.getLine(i);
                if (i != 0)
-                       stream << L"\n";
-               if (!line.name.empty())
-                       stream << L"<" << line.name << L"> ";
-               stream << line.text;
+                       result += L"\n";
+               if (!line.name.empty()) {
+                       result += L"<";
+                       result += line.name;
+                       result += L"> ";
+               }
+               result += line.text;
        }
-       return stream.str();
+       return result;
 }
 
 ChatPrompt& ChatBackend::getPrompt()
@@ -754,6 +813,14 @@ void ChatBackend::clearRecentChat()
        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);