Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Make chat web links clickable (#11092)
If enabled in minetest.conf, provides colored, clickable (middle-mouse or ctrl-left-mouse) weblinks in chat output, to open the OS' default web browser.
  • Loading branch information
pecksin committed Jun 20, 2021
1 parent e1b297a commit 1805775
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 30 deletions.
6 changes: 6 additions & 0 deletions builtin/settingtypes.txt
Expand Up @@ -973,6 +973,12 @@ mute_sound (Mute sound) bool false

[Client]

# Clickable weblinks (middle-click or ctrl-left-click) enabled in chat console output.
clickable_chat_weblinks (Chat weblinks) bool false

# Optional override for chat weblink color.
chat_weblink_color (Weblink color) string

[*Network]

# Address to connect to.
Expand Down
8 changes: 8 additions & 0 deletions minetest.conf.example
Expand Up @@ -1155,6 +1155,14 @@
# Client
#

# If enabled, http links in chat can be middle-clicked or ctrl-left-clicked to open the link in the OS's default web browser.
# type: bool
# clickable_chat_weblinks = false

# If clickable_chat_weblinks is enabled, specify the color (as 24-bit hexadecimal) of weblinks in chat.
# type: string
# chat_weblink_color = #8888FF

## Network

# Address to connect to.
Expand Down
9 changes: 9 additions & 0 deletions po/minetest.pot
Expand Up @@ -6551,3 +6551,12 @@ msgid ""
"be queued.\n"
"This should be lower than curl_parallel_limit."
msgstr ""

#: src/gui/guiChatConsole.cpp
msgid "Opening webpage"
msgstr ""

#: src/gui/guiChatConsole.cpp
msgid "Failed to open webpage"
msgstr ""

133 changes: 105 additions & 28 deletions src/chat.cpp
Expand Up @@ -35,6 +35,17 @@ ChatBuffer::ChatBuffer(u32 scrollback):
if (m_scrollback == 0)
m_scrollback = 1;
m_empty_formatted_line.first = true;

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(const std::wstring &name, const std::wstring &text)
Expand Down Expand Up @@ -263,78 +274,144 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
//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 (iswspace(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++;
}
Expand Down
8 changes: 8 additions & 0 deletions src/chat.h
Expand Up @@ -57,6 +57,8 @@ struct ChatFormattedFragment
EnrichedString text;
// starting column
u32 column;
// web link is empty for most frags
std::string weblink;
// formatting
//u8 bold:1;
};
Expand Down Expand Up @@ -118,6 +120,7 @@ class ChatBuffer
std::vector<ChatFormattedLine>& destination) const;

void resize(u32 scrollback);

protected:
s32 getTopScrollPos() const;
s32 getBottomScrollPos() const;
Expand All @@ -138,6 +141,11 @@ class ChatBuffer
std::vector<ChatFormattedLine> m_formatted;
// Empty formatted line, for error returns
ChatFormattedLine m_empty_formatted_line;

// Enable clickable chat weblinks
bool m_cache_clickable_chat_weblinks;
// Color of clickable chat weblinks
irr::video::SColor m_cache_chat_weblink_color;
};

class ChatPrompt
Expand Down
2 changes: 2 additions & 0 deletions src/defaultsettings.cpp
Expand Up @@ -65,6 +65,8 @@ void set_default_settings()
settings->setDefault("max_out_chat_queue_size", "20");
settings->setDefault("pause_on_lost_focus", "false");
settings->setDefault("enable_register_confirmation", "true");
settings->setDefault("clickable_chat_weblinks", "false");
settings->setDefault("chat_weblink_color", "#8888FF");

// Keymap
settings->setDefault("remote_port", "30000");
Expand Down

0 comments on commit 1805775

Please sign in to comment.