chat.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. /*
  2. Minetest
  3. Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
  4. This program is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU Lesser General Public License as published by
  6. the Free Software Foundation; either version 2.1 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU Lesser General Public License for more details.
  12. You should have received a copy of the GNU Lesser General Public License along
  13. with this program; if not, write to the Free Software Foundation, Inc.,
  14. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  15. */
  16. #include "chat.h"
  17. #include <algorithm>
  18. #include <cctype>
  19. #include <sstream>
  20. #include "config.h"
  21. #include "debug.h"
  22. #include "util/strfnd.h"
  23. #include "util/string.h"
  24. #include "util/numeric.h"
  25. ChatBuffer::ChatBuffer(u32 scrollback):
  26. m_scrollback(scrollback)
  27. {
  28. if (m_scrollback == 0)
  29. m_scrollback = 1;
  30. m_empty_formatted_line.first = true;
  31. m_cache_clickable_chat_weblinks = false;
  32. // Curses mode cannot access g_settings here
  33. if (g_settings != nullptr) {
  34. m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
  35. if (m_cache_clickable_chat_weblinks) {
  36. std::string colorval = g_settings->get("chat_weblink_color");
  37. parseColorString(colorval, m_cache_chat_weblink_color, false, 255);
  38. m_cache_chat_weblink_color.setAlpha(255);
  39. }
  40. }
  41. }
  42. void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
  43. {
  44. m_lines_modified = true;
  45. ChatLine line(name, text);
  46. m_unformatted.push_back(line);
  47. if (m_rows > 0) {
  48. // m_formatted is valid and must be kept valid
  49. bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
  50. u32 num_added = formatChatLine(line, m_cols, m_formatted);
  51. if (scrolled_at_bottom)
  52. m_scroll += num_added;
  53. }
  54. // Limit number of lines by m_scrollback
  55. if (m_unformatted.size() > m_scrollback) {
  56. deleteOldest(m_unformatted.size() - m_scrollback);
  57. }
  58. }
  59. void ChatBuffer::clear()
  60. {
  61. m_unformatted.clear();
  62. m_formatted.clear();
  63. m_scroll = 0;
  64. m_lines_modified = true;
  65. }
  66. u32 ChatBuffer::getLineCount() const
  67. {
  68. return m_unformatted.size();
  69. }
  70. const ChatLine& ChatBuffer::getLine(u32 index) const
  71. {
  72. assert(index < getLineCount()); // pre-condition
  73. return m_unformatted[index];
  74. }
  75. void ChatBuffer::step(f32 dtime)
  76. {
  77. for (ChatLine &line : m_unformatted) {
  78. line.age += dtime;
  79. }
  80. }
  81. void ChatBuffer::deleteOldest(u32 count)
  82. {
  83. bool at_bottom = (m_scroll == getBottomScrollPos());
  84. u32 del_unformatted = 0;
  85. u32 del_formatted = 0;
  86. while (count > 0 && del_unformatted < m_unformatted.size()) {
  87. ++del_unformatted;
  88. // keep m_formatted in sync
  89. if (del_formatted < m_formatted.size()) {
  90. sanity_check(m_formatted[del_formatted].first);
  91. ++del_formatted;
  92. while (del_formatted < m_formatted.size() &&
  93. !m_formatted[del_formatted].first)
  94. ++del_formatted;
  95. }
  96. --count;
  97. }
  98. m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
  99. m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
  100. if (del_unformatted > 0)
  101. m_lines_modified = true;
  102. if (at_bottom)
  103. m_scroll = getBottomScrollPos();
  104. else
  105. scrollAbsolute(m_scroll - del_formatted);
  106. }
  107. void ChatBuffer::deleteByAge(f32 maxAge)
  108. {
  109. u32 count = 0;
  110. while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
  111. ++count;
  112. deleteOldest(count);
  113. }
  114. u32 ChatBuffer::getRows() const
  115. {
  116. return m_rows;
  117. }
  118. void ChatBuffer::reformat(u32 cols, u32 rows)
  119. {
  120. if (cols == 0 || rows == 0)
  121. {
  122. // Clear formatted buffer
  123. m_cols = 0;
  124. m_rows = 0;
  125. m_scroll = 0;
  126. m_formatted.clear();
  127. }
  128. else if (cols != m_cols || rows != m_rows)
  129. {
  130. // TODO: Avoid reformatting ALL lines (even invisible ones)
  131. // each time the console size changes.
  132. // Find out the scroll position in *unformatted* lines
  133. u32 restore_scroll_unformatted = 0;
  134. u32 restore_scroll_formatted = 0;
  135. bool at_bottom = (m_scroll == getBottomScrollPos());
  136. if (!at_bottom)
  137. {
  138. for (s32 i = 0; i < m_scroll; ++i)
  139. {
  140. if (m_formatted[i].first)
  141. ++restore_scroll_unformatted;
  142. }
  143. }
  144. // If number of columns change, reformat everything
  145. if (cols != m_cols)
  146. {
  147. m_formatted.clear();
  148. for (u32 i = 0; i < m_unformatted.size(); ++i)
  149. {
  150. if (i == restore_scroll_unformatted)
  151. restore_scroll_formatted = m_formatted.size();
  152. formatChatLine(m_unformatted[i], cols, m_formatted);
  153. }
  154. }
  155. // Update the console size
  156. m_cols = cols;
  157. m_rows = rows;
  158. // Restore the scroll position
  159. if (at_bottom)
  160. {
  161. scrollBottom();
  162. }
  163. else
  164. {
  165. scrollAbsolute(restore_scroll_formatted);
  166. }
  167. }
  168. }
  169. const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
  170. {
  171. s32 index = m_scroll + (s32) row;
  172. if (index >= 0 && index < (s32) m_formatted.size())
  173. return m_formatted[index];
  174. return m_empty_formatted_line;
  175. }
  176. void ChatBuffer::scroll(s32 rows)
  177. {
  178. scrollAbsolute(m_scroll + rows);
  179. }
  180. void ChatBuffer::scrollAbsolute(s32 scroll)
  181. {
  182. s32 top = getTopScrollPos();
  183. s32 bottom = getBottomScrollPos();
  184. m_scroll = scroll;
  185. if (m_scroll < top)
  186. m_scroll = top;
  187. if (m_scroll > bottom)
  188. m_scroll = bottom;
  189. }
  190. void ChatBuffer::scrollBottom()
  191. {
  192. m_scroll = getBottomScrollPos();
  193. }
  194. u32 ChatBuffer::formatChatLine(const ChatLine &line, u32 cols,
  195. std::vector<ChatFormattedLine> &destination) const
  196. {
  197. u32 num_added = 0;
  198. std::vector<ChatFormattedFragment> next_frags;
  199. ChatFormattedLine next_line;
  200. ChatFormattedFragment temp_frag;
  201. u32 out_column = 0;
  202. u32 in_pos = 0;
  203. u32 hanging_indentation = 0;
  204. // Format the sender name and produce fragments
  205. if (!line.name.empty()) {
  206. temp_frag.text = L"<";
  207. temp_frag.column = 0;
  208. //temp_frag.bold = 0;
  209. next_frags.push_back(temp_frag);
  210. temp_frag.text = line.name;
  211. temp_frag.column = 0;
  212. //temp_frag.bold = 1;
  213. next_frags.push_back(temp_frag);
  214. temp_frag.text = L"> ";
  215. temp_frag.column = 0;
  216. //temp_frag.bold = 0;
  217. next_frags.push_back(temp_frag);
  218. }
  219. std::wstring name_sanitized = line.name.c_str();
  220. // Choose an indentation level
  221. if (line.name.empty()) {
  222. // Server messages
  223. hanging_indentation = 0;
  224. } else if (name_sanitized.size() + 3 <= cols/2) {
  225. // Names shorter than about half the console width
  226. hanging_indentation = line.name.size() + 3;
  227. } else {
  228. // Very long names
  229. hanging_indentation = 2;
  230. }
  231. // If there are no columns remaining after the indentation (window is very
  232. // narrow), we can't write anything
  233. if (hanging_indentation >= cols)
  234. return 0;
  235. next_line.first = true;
  236. // Set/use forced newline after the last frag in each line
  237. bool mark_newline = false;
  238. // Produce fragments and layout them into lines
  239. while (!next_frags.empty() || in_pos < line.text.size()) {
  240. mark_newline = false; // now using this to USE line-end frag
  241. // Layout fragments into lines
  242. while (!next_frags.empty()) {
  243. ChatFormattedFragment& frag = next_frags[0];
  244. // Force newline after this frag, if marked
  245. if (frag.column == INT_MAX)
  246. mark_newline = true;
  247. if (frag.text.size() <= cols - out_column) {
  248. // Fragment fits into current line
  249. frag.column = out_column;
  250. next_line.fragments.push_back(frag);
  251. out_column += frag.text.size();
  252. next_frags.erase(next_frags.begin());
  253. } else {
  254. // Fragment does not fit into current line
  255. // So split it up
  256. temp_frag.text = frag.text.substr(0, cols - out_column);
  257. temp_frag.column = out_column;
  258. temp_frag.weblink = frag.weblink;
  259. next_line.fragments.push_back(temp_frag);
  260. frag.text = frag.text.substr(cols - out_column);
  261. frag.column = 0;
  262. out_column = cols;
  263. }
  264. if (out_column == cols || mark_newline) {
  265. // End the current line
  266. destination.push_back(next_line);
  267. num_added++;
  268. next_line.fragments.clear();
  269. next_line.first = false;
  270. out_column = hanging_indentation;
  271. mark_newline = false;
  272. }
  273. }
  274. // Produce fragment(s) for next formatted line
  275. if (!(in_pos < line.text.size()))
  276. continue;
  277. const std::wstring &linestring = line.text.getString();
  278. u32 remaining_in_output = cols - out_column;
  279. size_t http_pos = std::wstring::npos;
  280. mark_newline = false; // now using this to SET line-end frag
  281. // Construct all frags for next output line
  282. while (!mark_newline) {
  283. // Determine a fragment length <= the minimum of
  284. // remaining_in_{in,out}put. Try to end the fragment
  285. // on a word boundary.
  286. u32 frag_length = 0, space_pos = 0;
  287. u32 remaining_in_input = line.text.size() - in_pos;
  288. if (m_cache_clickable_chat_weblinks) {
  289. // Note: unsigned(-1) on fail
  290. http_pos = linestring.find(L"https://", in_pos);
  291. if (http_pos == std::wstring::npos)
  292. http_pos = linestring.find(L"http://", in_pos);
  293. if (http_pos != std::wstring::npos)
  294. http_pos -= in_pos;
  295. }
  296. while (frag_length < remaining_in_input &&
  297. frag_length < remaining_in_output) {
  298. if (iswspace(linestring[in_pos + frag_length]))
  299. space_pos = frag_length;
  300. ++frag_length;
  301. }
  302. if (http_pos >= remaining_in_output) {
  303. // Http not in range, grab until space or EOL, halt as normal.
  304. // Note this works because (http_pos = npos) is unsigned(-1)
  305. mark_newline = true;
  306. } else if (http_pos == 0) {
  307. // At http, grab ALL until FIRST whitespace or end marker. loop.
  308. // If at end of string, next loop will be empty string to mark end of weblink.
  309. frag_length = 6; // Frag is at least "http://"
  310. // Chars to mark end of weblink
  311. // TODO? replace this with a safer (slower) regex whitelist?
  312. static const std::wstring delim_chars = L"\'\";";
  313. wchar_t tempchar = linestring[in_pos+frag_length];
  314. while (frag_length < remaining_in_input &&
  315. !iswspace(tempchar) &&
  316. delim_chars.find(tempchar) == std::wstring::npos) {
  317. ++frag_length;
  318. tempchar = linestring[in_pos+frag_length];
  319. }
  320. space_pos = frag_length - 1;
  321. // This frag may need to be force-split. That's ok, urls aren't "words"
  322. if (frag_length >= remaining_in_output) {
  323. mark_newline = true;
  324. }
  325. } else {
  326. // Http in range, grab until http, loop
  327. space_pos = http_pos - 1;
  328. frag_length = http_pos;
  329. }
  330. // Include trailing space in current frag
  331. if (space_pos != 0 && frag_length < remaining_in_input)
  332. frag_length = space_pos + 1;
  333. temp_frag.text = line.text.substr(in_pos, frag_length);
  334. // A hack so this frag remembers mark_newline for the layout phase
  335. temp_frag.column = mark_newline ? INT_MAX : 0;
  336. if (http_pos == 0) {
  337. // Discard color stuff from the source frag
  338. temp_frag.text = EnrichedString(temp_frag.text.getString());
  339. temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
  340. // Set weblink in the frag meta
  341. temp_frag.weblink = wide_to_utf8(temp_frag.text.getString());
  342. } else {
  343. temp_frag.weblink.clear();
  344. }
  345. next_frags.push_back(temp_frag);
  346. in_pos += frag_length;
  347. remaining_in_output -= std::min(frag_length, remaining_in_output);
  348. }
  349. }
  350. // End the last line
  351. if (num_added == 0 || !next_line.fragments.empty()) {
  352. destination.push_back(next_line);
  353. num_added++;
  354. }
  355. return num_added;
  356. }
  357. s32 ChatBuffer::getTopScrollPos() const
  358. {
  359. s32 formatted_count = (s32) m_formatted.size();
  360. s32 rows = (s32) m_rows;
  361. if (rows == 0)
  362. return 0;
  363. if (formatted_count <= rows)
  364. return formatted_count - rows;
  365. return 0;
  366. }
  367. s32 ChatBuffer::getBottomScrollPos() const
  368. {
  369. s32 formatted_count = (s32) m_formatted.size();
  370. s32 rows = (s32) m_rows;
  371. if (rows == 0)
  372. return 0;
  373. return formatted_count - rows;
  374. }
  375. void ChatBuffer::resize(u32 scrollback)
  376. {
  377. m_scrollback = scrollback;
  378. if (m_unformatted.size() > m_scrollback)
  379. deleteOldest(m_unformatted.size() - m_scrollback);
  380. }
  381. ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
  382. m_prompt(prompt),
  383. m_history_limit(history_limit)
  384. {
  385. }
  386. const std::wstring &ChatPrompt::getLineRef() const
  387. {
  388. return m_history_index >= m_history.size() ? m_line : m_history[m_history_index].line;
  389. }
  390. std::wstring &ChatPrompt::makeLineRef()
  391. {
  392. if (m_history_index >= m_history.size()) {
  393. return m_line;
  394. } else {
  395. if (!m_history[m_history_index].saved)
  396. m_history[m_history_index].saved = m_history[m_history_index].line;
  397. return m_history[m_history_index].line;
  398. }
  399. }
  400. bool ChatPrompt::HistoryEntry::operator==(const ChatPrompt::HistoryEntry &other)
  401. {
  402. if (line != other.line)
  403. return false;
  404. if (saved == other.saved)
  405. return true;
  406. if ((!saved || saved == line) && (!other.saved || other.saved == other.line))
  407. return true;
  408. return false;
  409. }
  410. void ChatPrompt::input(wchar_t ch)
  411. {
  412. makeLineRef().insert(m_cursor, 1, ch);
  413. m_cursor++;
  414. clampView();
  415. m_nick_completion_start = 0;
  416. m_nick_completion_end = 0;
  417. }
  418. void ChatPrompt::input(const std::wstring &str)
  419. {
  420. makeLineRef().insert(m_cursor, str);
  421. m_cursor += str.size();
  422. clampView();
  423. m_nick_completion_start = 0;
  424. m_nick_completion_end = 0;
  425. }
  426. void ChatPrompt::addToHistory(const std::wstring &line)
  427. {
  428. std::wstring old_line = getLine();
  429. if (m_history_index < m_history.size()) {
  430. auto entry = m_history.begin() + m_history_index;
  431. if (entry->saved && entry->line == line) {
  432. entry->line = *entry->saved;
  433. entry->saved = std::nullopt;
  434. // Remove potential duplicates
  435. auto dup_before = std::find(m_history.begin(), entry, *entry);
  436. if (dup_before != entry)
  437. m_history.erase(dup_before);
  438. else if (std::find(entry + 1, m_history.end(), *entry) != m_history.end())
  439. m_history.erase(entry);
  440. }
  441. }
  442. if (!line.empty() &&
  443. (m_history.size() == 0 || m_history.back().line != line)) {
  444. HistoryEntry entry(line);
  445. // Remove all duplicates
  446. m_history.erase(std::remove(m_history.begin(), m_history.end(), entry),
  447. m_history.end());
  448. // Push unique line
  449. m_history.push_back(std::move(entry));
  450. }
  451. if (m_history.size() > m_history_limit)
  452. m_history.erase(m_history.begin());
  453. m_history_index = m_history.size();
  454. m_line = std::move(old_line);
  455. }
  456. void ChatPrompt::clear()
  457. {
  458. makeLineRef().clear();
  459. m_view = 0;
  460. m_cursor = 0;
  461. m_nick_completion_start = 0;
  462. m_nick_completion_end = 0;
  463. }
  464. std::wstring ChatPrompt::replace(const std::wstring &line)
  465. {
  466. std::wstring old_line = getLine();
  467. makeLineRef() = line;
  468. m_view = m_cursor = line.size();
  469. clampView();
  470. m_nick_completion_start = 0;
  471. m_nick_completion_end = 0;
  472. return old_line;
  473. }
  474. void ChatPrompt::historyPrev()
  475. {
  476. if (m_history_index != 0) {
  477. --m_history_index;
  478. m_view = m_cursor = getLineRef().size();
  479. clampView();
  480. m_nick_completion_start = 0;
  481. m_nick_completion_end = 0;
  482. }
  483. }
  484. void ChatPrompt::historyNext()
  485. {
  486. if (m_history_index < m_history.size()) {
  487. m_history_index++;
  488. m_view = m_cursor = getLineRef().size();
  489. clampView();
  490. m_nick_completion_start = 0;
  491. m_nick_completion_end = 0;
  492. }
  493. }
  494. void ChatPrompt::nickCompletion(const std::set<std::string> &names, bool backwards)
  495. {
  496. const std::wstring_view line(getLineRef());
  497. // Two cases:
  498. // (a) m_nick_completion_start == m_nick_completion_end == 0
  499. // Then no previous nick completion is active.
  500. // Get the word around the cursor and replace with any nick
  501. // that has that word as a prefix.
  502. // (b) else, continue a previous nick completion.
  503. // m_nick_completion_start..m_nick_completion_end are the
  504. // interval where the originally used prefix was. Cycle
  505. // through the list of completions of that prefix.
  506. u32 prefix_start = m_nick_completion_start;
  507. u32 prefix_end = m_nick_completion_end;
  508. bool initial = (prefix_end == 0);
  509. if (initial)
  510. {
  511. // no previous nick completion is active
  512. prefix_start = prefix_end = m_cursor;
  513. while (prefix_start > 0 && !iswspace(line[prefix_start-1]))
  514. --prefix_start;
  515. while (prefix_end < line.size() && !iswspace(line[prefix_end]))
  516. ++prefix_end;
  517. if (prefix_start == prefix_end)
  518. return;
  519. }
  520. auto prefix = line.substr(prefix_start, prefix_end - prefix_start);
  521. // find all names that start with the selected prefix
  522. std::vector<std::wstring> completions;
  523. for (const std::string &name : names) {
  524. std::wstring completion = utf8_to_wide(name);
  525. if (str_starts_with(completion, prefix, true)) {
  526. if (prefix_start == 0)
  527. completion += L": ";
  528. completions.push_back(completion);
  529. }
  530. }
  531. if (completions.empty())
  532. return;
  533. // find a replacement string and the word that will be replaced
  534. u32 word_end = prefix_end;
  535. u32 replacement_index = 0;
  536. if (!initial)
  537. {
  538. while (word_end < line.size() && !iswspace(line[word_end]))
  539. ++word_end;
  540. auto word = line.substr(prefix_start, word_end - prefix_start);
  541. // cycle through completions
  542. for (u32 i = 0; i < completions.size(); ++i)
  543. {
  544. if (str_equal(word, completions[i], true))
  545. {
  546. if (backwards)
  547. replacement_index = i + completions.size() - 1;
  548. else
  549. replacement_index = i + 1;
  550. replacement_index %= completions.size();
  551. break;
  552. }
  553. }
  554. }
  555. const auto &replacement = completions[replacement_index];
  556. if (word_end < line.size() && iswspace(line[word_end]))
  557. ++word_end;
  558. // replace existing word with replacement word,
  559. // place the cursor at the end and record the completion prefix
  560. makeLineRef().replace(prefix_start, word_end - prefix_start, replacement);
  561. m_cursor = prefix_start + replacement.size();
  562. clampView();
  563. m_nick_completion_start = prefix_start;
  564. m_nick_completion_end = prefix_end;
  565. }
  566. void ChatPrompt::reformat(u32 cols)
  567. {
  568. if (cols <= m_prompt.size())
  569. {
  570. m_cols = 0;
  571. m_view = m_cursor;
  572. }
  573. else
  574. {
  575. s32 length = getLineRef().size();
  576. bool was_at_end = (m_view + m_cols >= length + 1);
  577. m_cols = cols - m_prompt.size();
  578. if (was_at_end)
  579. m_view = length;
  580. clampView();
  581. }
  582. }
  583. std::wstring ChatPrompt::getVisiblePortion() const
  584. {
  585. const std::wstring &line_ref = getLineRef();
  586. if ((size_t)m_view >= line_ref.size())
  587. return m_prompt;
  588. else
  589. return m_prompt + line_ref.substr(m_view, m_cols);
  590. }
  591. s32 ChatPrompt::getVisibleCursorPosition() const
  592. {
  593. return m_cursor - m_view + m_prompt.size();
  594. }
  595. void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
  596. {
  597. s32 old_cursor = m_cursor;
  598. s32 new_cursor = m_cursor;
  599. const std::wstring &line = getLineRef();
  600. s32 length = line.size();
  601. s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
  602. switch (scope) {
  603. case CURSOROP_SCOPE_CHARACTER:
  604. new_cursor += increment;
  605. break;
  606. case CURSOROP_SCOPE_WORD:
  607. if (dir == CURSOROP_DIR_RIGHT) {
  608. // skip one word to the right
  609. while (new_cursor < length && iswspace(line[new_cursor]))
  610. new_cursor++;
  611. while (new_cursor < length && !iswspace(line[new_cursor]))
  612. new_cursor++;
  613. while (new_cursor < length && iswspace(line[new_cursor]))
  614. new_cursor++;
  615. } else {
  616. // skip one word to the left
  617. while (new_cursor >= 1 && iswspace(line[new_cursor - 1]))
  618. new_cursor--;
  619. while (new_cursor >= 1 && !iswspace(line[new_cursor - 1]))
  620. new_cursor--;
  621. }
  622. break;
  623. case CURSOROP_SCOPE_LINE:
  624. new_cursor += increment * length;
  625. break;
  626. case CURSOROP_SCOPE_SELECTION:
  627. break;
  628. }
  629. new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
  630. switch (op) {
  631. case CURSOROP_MOVE:
  632. m_cursor = new_cursor;
  633. m_cursor_len = 0;
  634. break;
  635. case CURSOROP_DELETE:
  636. if (m_cursor_len > 0) { // Delete selected text first
  637. makeLineRef().erase(m_cursor, m_cursor_len);
  638. } else {
  639. m_cursor = MYMIN(new_cursor, old_cursor);
  640. makeLineRef().erase(m_cursor, abs(new_cursor - old_cursor));
  641. }
  642. m_cursor_len = 0;
  643. break;
  644. case CURSOROP_SELECT:
  645. if (scope == CURSOROP_SCOPE_LINE) {
  646. m_cursor = 0;
  647. m_cursor_len = length;
  648. } else {
  649. m_cursor = MYMIN(new_cursor, old_cursor);
  650. m_cursor_len += abs(new_cursor - old_cursor);
  651. m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
  652. }
  653. break;
  654. }
  655. clampView();
  656. m_nick_completion_start = 0;
  657. m_nick_completion_end = 0;
  658. }
  659. void ChatPrompt::clampView()
  660. {
  661. s32 length = getLineRef().size();
  662. if (length + 1 <= m_cols)
  663. {
  664. m_view = 0;
  665. }
  666. else
  667. {
  668. m_view = MYMIN(m_view, length + 1 - m_cols);
  669. m_view = MYMIN(m_view, m_cursor);
  670. m_view = MYMAX(m_view, m_cursor - m_cols + 1);
  671. m_view = MYMAX(m_view, 0);
  672. }
  673. }
  674. ChatBackend::ChatBackend():
  675. m_console_buffer(1500),
  676. m_recent_buffer(6),
  677. m_prompt(L"]", 1500)
  678. {
  679. }
  680. void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
  681. {
  682. // Note: A message may consist of multiple lines, for example the MOTD.
  683. text = translate_string(text);
  684. WStrfnd fnd(text);
  685. while (!fnd.at_end())
  686. {
  687. std::wstring line = fnd.next(L"\n");
  688. m_console_buffer.addLine(name, line);
  689. m_recent_buffer.addLine(name, line);
  690. }
  691. }
  692. void ChatBackend::addUnparsedMessage(std::wstring message)
  693. {
  694. // TODO: Remove the need to parse chat messages client-side, by sending
  695. // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
  696. if (message.size() >= 2 && message[0] == L'<')
  697. {
  698. std::size_t closing = message.find_first_of(L'>', 1);
  699. if (closing != std::wstring::npos &&
  700. closing + 2 <= message.size() &&
  701. message[closing+1] == L' ')
  702. {
  703. std::wstring name = message.substr(1, closing - 1);
  704. std::wstring text = message.substr(closing + 2);
  705. addMessage(name, text);
  706. return;
  707. }
  708. }
  709. // Unable to parse, probably a server message.
  710. addMessage(L"", message);
  711. }
  712. ChatBuffer& ChatBackend::getConsoleBuffer()
  713. {
  714. return m_console_buffer;
  715. }
  716. ChatBuffer& ChatBackend::getRecentBuffer()
  717. {
  718. return m_recent_buffer;
  719. }
  720. EnrichedString ChatBackend::getRecentChat() const
  721. {
  722. EnrichedString result;
  723. for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
  724. const ChatLine& line = m_recent_buffer.getLine(i);
  725. if (i != 0)
  726. result += L"\n";
  727. if (!line.name.empty()) {
  728. result += L"<";
  729. result += line.name;
  730. result += L"> ";
  731. }
  732. result += line.text;
  733. }
  734. return result;
  735. }
  736. ChatPrompt& ChatBackend::getPrompt()
  737. {
  738. return m_prompt;
  739. }
  740. void ChatBackend::reformat(u32 cols, u32 rows)
  741. {
  742. m_console_buffer.reformat(cols, rows);
  743. // no need to reformat m_recent_buffer, its formatted lines
  744. // are not used
  745. m_prompt.reformat(cols);
  746. }
  747. void ChatBackend::clearRecentChat()
  748. {
  749. m_recent_buffer.clear();
  750. }
  751. void ChatBackend::applySettings()
  752. {
  753. u32 recent_lines = g_settings->getU32("recent_chat_messages");
  754. recent_lines = rangelim(recent_lines, 2, 20);
  755. m_recent_buffer.resize(recent_lines);
  756. }
  757. void ChatBackend::step(float dtime)
  758. {
  759. m_recent_buffer.step(dtime);
  760. m_recent_buffer.deleteByAge(60.0);
  761. // no need to age messages in anything but m_recent_buffer
  762. }
  763. void ChatBackend::scroll(s32 rows)
  764. {
  765. m_console_buffer.scroll(rows);
  766. }
  767. void ChatBackend::scrollPageDown()
  768. {
  769. m_console_buffer.scroll(m_console_buffer.getRows());
  770. }
  771. void ChatBackend::scrollPageUp()
  772. {
  773. m_console_buffer.scroll(-(s32)m_console_buffer.getRows());
  774. }