Skip to content

Commit 1805775

Browse files
authored
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.
1 parent e1b297a commit 1805775

File tree

8 files changed

+242
-30
lines changed

8 files changed

+242
-30
lines changed

builtin/settingtypes.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,12 @@ mute_sound (Mute sound) bool false
973973

974974
[Client]
975975

976+
# Clickable weblinks (middle-click or ctrl-left-click) enabled in chat console output.
977+
clickable_chat_weblinks (Chat weblinks) bool false
978+
979+
# Optional override for chat weblink color.
980+
chat_weblink_color (Weblink color) string
981+
976982
[*Network]
977983

978984
# Address to connect to.

minetest.conf.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,14 @@
11551155
# Client
11561156
#
11571157

1158+
# 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.
1159+
# type: bool
1160+
# clickable_chat_weblinks = false
1161+
1162+
# If clickable_chat_weblinks is enabled, specify the color (as 24-bit hexadecimal) of weblinks in chat.
1163+
# type: string
1164+
# chat_weblink_color = #8888FF
1165+
11581166
## Network
11591167

11601168
# Address to connect to.

po/minetest.pot

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6551,3 +6551,12 @@ msgid ""
65516551
"be queued.\n"
65526552
"This should be lower than curl_parallel_limit."
65536553
msgstr ""
6554+
6555+
#: src/gui/guiChatConsole.cpp
6556+
msgid "Opening webpage"
6557+
msgstr ""
6558+
6559+
#: src/gui/guiChatConsole.cpp
6560+
msgid "Failed to open webpage"
6561+
msgstr ""
6562+

src/chat.cpp

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ ChatBuffer::ChatBuffer(u32 scrollback):
3535
if (m_scrollback == 0)
3636
m_scrollback = 1;
3737
m_empty_formatted_line.first = true;
38+
39+
m_cache_clickable_chat_weblinks = false;
40+
// Curses mode cannot access g_settings here
41+
if (g_settings != nullptr) {
42+
m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
43+
if (m_cache_clickable_chat_weblinks) {
44+
std::string colorval = g_settings->get("chat_weblink_color");
45+
parseColorString(colorval, m_cache_chat_weblink_color, false, 255);
46+
m_cache_chat_weblink_color.setAlpha(255);
47+
}
48+
}
3849
}
3950

4051
void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
@@ -263,78 +274,144 @@ u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
263274
//EnrichedString line_text(line.text);
264275

265276
next_line.first = true;
266-
bool text_processing = false;
277+
// Set/use forced newline after the last frag in each line
278+
bool mark_newline = false;
267279

268280
// Produce fragments and layout them into lines
269-
while (!next_frags.empty() || in_pos < line.text.size())
270-
{
281+
while (!next_frags.empty() || in_pos < line.text.size()) {
282+
mark_newline = false; // now using this to USE line-end frag
283+
271284
// Layout fragments into lines
272-
while (!next_frags.empty())
273-
{
285+
while (!next_frags.empty()) {
274286
ChatFormattedFragment& frag = next_frags[0];
275-
if (frag.text.size() <= cols - out_column)
276-
{
287+
288+
// Force newline after this frag, if marked
289+
if (frag.column == INT_MAX)
290+
mark_newline = true;
291+
292+
if (frag.text.size() <= cols - out_column) {
277293
// Fragment fits into current line
278294
frag.column = out_column;
279295
next_line.fragments.push_back(frag);
280296
out_column += frag.text.size();
281297
next_frags.erase(next_frags.begin());
282-
}
283-
else
284-
{
298+
} else {
285299
// Fragment does not fit into current line
286300
// So split it up
287301
temp_frag.text = frag.text.substr(0, cols - out_column);
288302
temp_frag.column = out_column;
289-
//temp_frag.bold = frag.bold;
303+
temp_frag.weblink = frag.weblink;
304+
290305
next_line.fragments.push_back(temp_frag);
291306
frag.text = frag.text.substr(cols - out_column);
307+
frag.column = 0;
292308
out_column = cols;
293309
}
294-
if (out_column == cols || text_processing)
295-
{
310+
311+
if (out_column == cols || mark_newline) {
296312
// End the current line
297313
destination.push_back(next_line);
298314
num_added++;
299315
next_line.fragments.clear();
300316
next_line.first = false;
301317

302-
out_column = text_processing ? hanging_indentation : 0;
318+
out_column = hanging_indentation;
319+
mark_newline = false;
303320
}
304321
}
305322

306-
// Produce fragment
307-
if (in_pos < line.text.size())
308-
{
309-
u32 remaining_in_input = line.text.size() - in_pos;
310-
u32 remaining_in_output = cols - out_column;
323+
// Produce fragment(s) for next formatted line
324+
if (!(in_pos < line.text.size()))
325+
continue;
311326

327+
const std::wstring &linestring = line.text.getString();
328+
u32 remaining_in_output = cols - out_column;
329+
size_t http_pos = std::wstring::npos;
330+
mark_newline = false; // now using this to SET line-end frag
331+
332+
// Construct all frags for next output line
333+
while (!mark_newline) {
312334
// Determine a fragment length <= the minimum of
313335
// remaining_in_{in,out}put. Try to end the fragment
314336
// on a word boundary.
315-
u32 frag_length = 1, space_pos = 0;
337+
u32 frag_length = 0, space_pos = 0;
338+
u32 remaining_in_input = line.text.size() - in_pos;
339+
340+
if (m_cache_clickable_chat_weblinks) {
341+
// Note: unsigned(-1) on fail
342+
http_pos = linestring.find(L"https://", in_pos);
343+
if (http_pos == std::wstring::npos)
344+
http_pos = linestring.find(L"http://", in_pos);
345+
if (http_pos != std::wstring::npos)
346+
http_pos -= in_pos;
347+
}
348+
316349
while (frag_length < remaining_in_input &&
317-
frag_length < remaining_in_output)
318-
{
319-
if (iswspace(line.text.getString()[in_pos + frag_length]))
350+
frag_length < remaining_in_output) {
351+
if (iswspace(linestring[in_pos + frag_length]))
320352
space_pos = frag_length;
321353
++frag_length;
322354
}
355+
356+
if (http_pos >= remaining_in_output) {
357+
// Http not in range, grab until space or EOL, halt as normal.
358+
// Note this works because (http_pos = npos) is unsigned(-1)
359+
360+
mark_newline = true;
361+
} else if (http_pos == 0) {
362+
// At http, grab ALL until FIRST whitespace or end marker. loop.
363+
// If at end of string, next loop will be empty string to mark end of weblink.
364+
365+
frag_length = 6; // Frag is at least "http://"
366+
367+
// Chars to mark end of weblink
368+
// TODO? replace this with a safer (slower) regex whitelist?
369+
static const std::wstring delim_chars = L"\'\");,";
370+
wchar_t tempchar = linestring[in_pos+frag_length];
371+
while (frag_length < remaining_in_input &&
372+
!iswspace(tempchar) &&
373+
delim_chars.find(tempchar) == std::wstring::npos) {
374+
++frag_length;
375+
tempchar = linestring[in_pos+frag_length];
376+
}
377+
378+
space_pos = frag_length - 1;
379+
// This frag may need to be force-split. That's ok, urls aren't "words"
380+
if (frag_length >= remaining_in_output) {
381+
mark_newline = true;
382+
}
383+
} else {
384+
// Http in range, grab until http, loop
385+
386+
space_pos = http_pos - 1;
387+
frag_length = http_pos;
388+
}
389+
390+
// Include trailing space in current frag
323391
if (space_pos != 0 && frag_length < remaining_in_input)
324392
frag_length = space_pos + 1;
325393

326394
temp_frag.text = line.text.substr(in_pos, frag_length);
327-
temp_frag.column = 0;
328-
//temp_frag.bold = 0;
395+
// A hack so this frag remembers mark_newline for the layout phase
396+
temp_frag.column = mark_newline ? INT_MAX : 0;
397+
398+
if (http_pos == 0) {
399+
// Discard color stuff from the source frag
400+
temp_frag.text = EnrichedString(temp_frag.text.getString());
401+
temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
402+
// Set weblink in the frag meta
403+
temp_frag.weblink = wide_to_utf8(temp_frag.text.getString());
404+
} else {
405+
temp_frag.weblink.clear();
406+
}
329407
next_frags.push_back(temp_frag);
330408
in_pos += frag_length;
331-
text_processing = true;
409+
remaining_in_output -= std::min(frag_length, remaining_in_output);
332410
}
333411
}
334412

335413
// End the last line
336-
if (num_added == 0 || !next_line.fragments.empty())
337-
{
414+
if (num_added == 0 || !next_line.fragments.empty()) {
338415
destination.push_back(next_line);
339416
num_added++;
340417
}

src/chat.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ struct ChatFormattedFragment
5757
EnrichedString text;
5858
// starting column
5959
u32 column;
60+
// web link is empty for most frags
61+
std::string weblink;
6062
// formatting
6163
//u8 bold:1;
6264
};
@@ -118,6 +120,7 @@ class ChatBuffer
118120
std::vector<ChatFormattedLine>& destination) const;
119121

120122
void resize(u32 scrollback);
123+
121124
protected:
122125
s32 getTopScrollPos() const;
123126
s32 getBottomScrollPos() const;
@@ -138,6 +141,11 @@ class ChatBuffer
138141
std::vector<ChatFormattedLine> m_formatted;
139142
// Empty formatted line, for error returns
140143
ChatFormattedLine m_empty_formatted_line;
144+
145+
// Enable clickable chat weblinks
146+
bool m_cache_clickable_chat_weblinks;
147+
// Color of clickable chat weblinks
148+
irr::video::SColor m_cache_chat_weblink_color;
141149
};
142150

143151
class ChatPrompt

src/defaultsettings.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ void set_default_settings()
6565
settings->setDefault("max_out_chat_queue_size", "20");
6666
settings->setDefault("pause_on_lost_focus", "false");
6767
settings->setDefault("enable_register_confirmation", "true");
68+
settings->setDefault("clickable_chat_weblinks", "false");
69+
settings->setDefault("chat_weblink_color", "#8888FF");
6870

6971
// Keymap
7072
settings->setDefault("remote_port", "30000");

0 commit comments

Comments
 (0)