]> git.lizzy.rs Git - dragonfireclient.git/blob - src/terminal_chat_console.cpp
Fix some debug info showing despite being disabled in the UI (#12205)
[dragonfireclient.git] / src / terminal_chat_console.cpp
1 /*
2 Minetest
3 Copyright (C) 2015 est31 <MTest31@outlook.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include <inttypes.h>
21 #include "config.h"
22 #if USE_CURSES
23 #include "version.h"
24 #include "terminal_chat_console.h"
25 #include "porting.h"
26 #include "settings.h"
27 #include "util/numeric.h"
28 #include "util/string.h"
29 #include "chat_interface.h"
30
31 TerminalChatConsole g_term_console;
32
33 // include this last to avoid any conflicts
34 // (likes to set macros to common names, conflicting various stuff)
35 #if CURSES_HAVE_NCURSESW_NCURSES_H
36 #include <ncursesw/ncurses.h>
37 #elif CURSES_HAVE_NCURSESW_CURSES_H
38 #include <ncursesw/curses.h>
39 #elif CURSES_HAVE_CURSES_H
40 #include <curses.h>
41 #elif CURSES_HAVE_NCURSES_H
42 #include <ncurses.h>
43 #elif CURSES_HAVE_NCURSES_NCURSES_H
44 #include <ncurses/ncurses.h>
45 #elif CURSES_HAVE_NCURSES_CURSES_H
46 #include <ncurses/curses.h>
47 #endif
48
49 // Some functions to make drawing etc position independent
50 static bool reformat_backend(ChatBackend *backend, int rows, int cols)
51 {
52         if (rows < 2)
53                 return false;
54         backend->reformat(cols, rows - 2);
55         return true;
56 }
57
58 static void move_for_backend(int row, int col)
59 {
60         move(row + 1, col);
61 }
62
63 void TerminalChatConsole::initOfCurses()
64 {
65         initscr();
66         cbreak(); //raw();
67         noecho();
68         keypad(stdscr, TRUE);
69         nodelay(stdscr, TRUE);
70         timeout(100);
71
72         // To make esc not delay up to one second. According to the internet,
73         // this is the value vim uses, too.
74         set_escdelay(25);
75
76         getmaxyx(stdscr, m_rows, m_cols);
77         m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
78 }
79
80 void TerminalChatConsole::deInitOfCurses()
81 {
82         endwin();
83 }
84
85 void *TerminalChatConsole::run()
86 {
87         BEGIN_DEBUG_EXCEPTION_HANDLER
88
89         std::cout << "========================" << std::endl;
90         std::cout << "Begin log output over terminal"
91                 << " (no stdout/stderr backlog during that)" << std::endl;
92         // Make the loggers to stdout/stderr shut up.
93         // Go over our own loggers instead.
94         LogLevelMask err_mask = g_logger.removeOutput(&stderr_output);
95         LogLevelMask out_mask = g_logger.removeOutput(&stdout_output);
96
97         g_logger.addOutput(&m_log_output);
98
99         // Inform the server of our nick
100         m_chat_interface->command_queue.push_back(
101                 new ChatEventNick(CET_NICK_ADD, m_nick));
102
103         {
104                 // Ensures that curses is deinitialized even on an exception being thrown
105                 CursesInitHelper helper(this);
106
107                 while (!stopRequested()) {
108
109                         int ch = getch();
110                         if (stopRequested())
111                                 break;
112
113                         step(ch);
114                 }
115         }
116
117         if (m_kill_requested)
118                 *m_kill_requested = true;
119
120         g_logger.removeOutput(&m_log_output);
121         g_logger.addOutputMasked(&stderr_output, err_mask);
122         g_logger.addOutputMasked(&stdout_output, out_mask);
123
124         std::cout << "End log output over terminal"
125                 << " (no stdout/stderr backlog during that)" << std::endl;
126         std::cout << "========================" << std::endl;
127
128         END_DEBUG_EXCEPTION_HANDLER
129
130         return NULL;
131 }
132
133 void TerminalChatConsole::typeChatMessage(const std::wstring &msg)
134 {
135         // Discard empty line
136         if (msg.empty())
137                 return;
138
139         // Send to server
140         m_chat_interface->command_queue.push_back(
141                 new ChatEventChat(m_nick, msg));
142
143         // Print if its a command (gets eaten by server otherwise)
144         if (msg[0] == L'/') {
145                 m_chat_backend.addMessage(L"", (std::wstring)L"Issued command: " + msg);
146         }
147 }
148
149 void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed)
150 {
151         ChatPrompt &prompt = m_chat_backend.getPrompt();
152         // Helpful if you want to collect key codes that aren't documented
153         /*if (ch != ERR) {
154                 m_chat_backend.addMessage(L"",
155                         (std::wstring)L"Pressed key " + utf8_to_wide(
156                         std::string(keyname(ch)) + " (code " + itos(ch) + ")"));
157                 complete_redraw_needed = true;
158         }//*/
159
160         // All the key codes below are compatible to xterm
161         // Only add new ones if you have tried them there,
162         // to ensure compatibility with not just xterm but the wide
163         // range of terminals that are compatible to xterm.
164
165         switch (ch) {
166                 case ERR: // no input
167                         break;
168                 case 27: // ESC
169                         // Toggle ESC mode
170                         m_esc_mode = !m_esc_mode;
171                         break;
172                 case KEY_PPAGE:
173                         m_chat_backend.scrollPageUp();
174                         complete_redraw_needed = true;
175                         break;
176                 case KEY_NPAGE:
177                         m_chat_backend.scrollPageDown();
178                         complete_redraw_needed = true;
179                         break;
180                 case KEY_ENTER:
181                 case '\r':
182                 case '\n': {
183                         prompt.addToHistory(prompt.getLine());
184                         typeChatMessage(prompt.replace(L""));
185                         break;
186                 }
187                 case KEY_UP:
188                         prompt.historyPrev();
189                         break;
190                 case KEY_DOWN:
191                         prompt.historyNext();
192                         break;
193                 case KEY_LEFT:
194                         // Left pressed
195                         // move character to the left
196                         prompt.cursorOperation(
197                                 ChatPrompt::CURSOROP_MOVE,
198                                 ChatPrompt::CURSOROP_DIR_LEFT,
199                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
200                         break;
201                 case 545:
202                         // Ctrl-Left pressed
203                         // move word to the left
204                         prompt.cursorOperation(
205                                 ChatPrompt::CURSOROP_MOVE,
206                                 ChatPrompt::CURSOROP_DIR_LEFT,
207                                 ChatPrompt::CURSOROP_SCOPE_WORD);
208                         break;
209                 case KEY_RIGHT:
210                         // Right pressed
211                         // move character to the right
212                         prompt.cursorOperation(
213                                 ChatPrompt::CURSOROP_MOVE,
214                                 ChatPrompt::CURSOROP_DIR_RIGHT,
215                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
216                         break;
217                 case 560:
218                         // Ctrl-Right pressed
219                         // move word to the right
220                         prompt.cursorOperation(
221                                 ChatPrompt::CURSOROP_MOVE,
222                                 ChatPrompt::CURSOROP_DIR_RIGHT,
223                                 ChatPrompt::CURSOROP_SCOPE_WORD);
224                         break;
225                 case KEY_HOME:
226                         // Home pressed
227                         // move to beginning of line
228                         prompt.cursorOperation(
229                                 ChatPrompt::CURSOROP_MOVE,
230                                 ChatPrompt::CURSOROP_DIR_LEFT,
231                                 ChatPrompt::CURSOROP_SCOPE_LINE);
232                         break;
233                 case KEY_END:
234                         // End pressed
235                         // move to end of line
236                         prompt.cursorOperation(
237                                 ChatPrompt::CURSOROP_MOVE,
238                                 ChatPrompt::CURSOROP_DIR_RIGHT,
239                                 ChatPrompt::CURSOROP_SCOPE_LINE);
240                         break;
241                 case KEY_BACKSPACE:
242                 case '\b':
243                 case 127:
244                         // Backspace pressed
245                         // delete character to the left
246                         prompt.cursorOperation(
247                                 ChatPrompt::CURSOROP_DELETE,
248                                 ChatPrompt::CURSOROP_DIR_LEFT,
249                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
250                         break;
251                 case KEY_DC:
252                         // Delete pressed
253                         // delete character to the right
254                         prompt.cursorOperation(
255                                 ChatPrompt::CURSOROP_DELETE,
256                                 ChatPrompt::CURSOROP_DIR_RIGHT,
257                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
258                         break;
259                 case 519:
260                         // Ctrl-Delete pressed
261                         // delete word to the right
262                         prompt.cursorOperation(
263                                 ChatPrompt::CURSOROP_DELETE,
264                                 ChatPrompt::CURSOROP_DIR_RIGHT,
265                                 ChatPrompt::CURSOROP_SCOPE_WORD);
266                         break;
267                 case 21:
268                         // Ctrl-U pressed
269                         // kill line to left end
270                         prompt.cursorOperation(
271                                 ChatPrompt::CURSOROP_DELETE,
272                                 ChatPrompt::CURSOROP_DIR_LEFT,
273                                 ChatPrompt::CURSOROP_SCOPE_LINE);
274                         break;
275                 case 11:
276                         // Ctrl-K pressed
277                         // kill line to right end
278                         prompt.cursorOperation(
279                                 ChatPrompt::CURSOROP_DELETE,
280                                 ChatPrompt::CURSOROP_DIR_RIGHT,
281                                 ChatPrompt::CURSOROP_SCOPE_LINE);
282                         break;
283                 case KEY_TAB:
284                         // Tab pressed
285                         // Nick completion
286                         prompt.nickCompletion(m_nicks, false);
287                         break;
288                 default:
289                         // Add character to the prompt,
290                         // assuming UTF-8.
291                         if (IS_UTF8_MULTB_START(ch)) {
292                                 m_pending_utf8_bytes.append(1, (char)ch);
293                                 m_utf8_bytes_to_wait += UTF8_MULTB_START_LEN(ch) - 1;
294                         } else if (m_utf8_bytes_to_wait != 0) {
295                                 m_pending_utf8_bytes.append(1, (char)ch);
296                                 m_utf8_bytes_to_wait--;
297                                 if (m_utf8_bytes_to_wait == 0) {
298                                         std::wstring w = utf8_to_wide(m_pending_utf8_bytes);
299                                         m_pending_utf8_bytes = "";
300                                         // hopefully only one char in the wstring...
301                                         for (size_t i = 0; i < w.size(); i++) {
302                                                 prompt.input(w.c_str()[i]);
303                                         }
304                                 }
305                         } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
306                                 prompt.input(ch);
307                         } else {
308                                 // Silently ignore characters we don't handle
309
310                                 //warningstream << "Pressed invalid character '"
311                                 //      << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
312                         }
313                         break;
314         }
315 }
316
317 void TerminalChatConsole::step(int ch)
318 {
319         bool complete_redraw_needed = false;
320
321         // empty queues
322         while (!m_chat_interface->outgoing_queue.empty()) {
323                 ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
324                 switch (evt->type) {
325                         case CET_NICK_REMOVE:
326                                 m_nicks.remove(((ChatEventNick *)evt)->nick);
327                                 break;
328                         case CET_NICK_ADD:
329                                 m_nicks.push_back(((ChatEventNick *)evt)->nick);
330                                 break;
331                         case CET_CHAT:
332                                 complete_redraw_needed = true;
333                                 // This is only used for direct replies from commands
334                                 // or for lua's print() functionality
335                                 m_chat_backend.addMessage(L"", ((ChatEventChat *)evt)->evt_msg);
336                                 break;
337                         case CET_TIME_INFO:
338                                 ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
339                                 m_game_time = tevt->game_time;
340                                 m_time_of_day = tevt->time;
341                 };
342                 delete evt;
343         }
344         while (!m_log_output.queue.empty()) {
345                 complete_redraw_needed = true;
346                 std::pair<LogLevel, std::string> p = m_log_output.queue.pop_frontNoEx();
347                 if (p.first > m_log_level)
348                         continue;
349
350                 std::wstring error_message = utf8_to_wide(Logger::getLevelLabel(p.first));
351                 if (!g_settings->getBool("disable_escape_sequences")) {
352                         error_message = std::wstring(L"\x1b(c@red)").append(error_message)
353                                 .append(L"\x1b(c@white)");
354                 }
355                 m_chat_backend.addMessage(error_message, utf8_to_wide(p.second));
356         }
357
358         // handle input
359         if (!m_esc_mode) {
360                 handleInput(ch, complete_redraw_needed);
361         } else {
362                 switch (ch) {
363                         case ERR: // no input
364                                 break;
365                         case 27: // ESC
366                                 // Toggle ESC mode
367                                 m_esc_mode = !m_esc_mode;
368                                 break;
369                         case 'L':
370                                 m_log_level--;
371                                 m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
372                                 break;
373                         case 'l':
374                                 m_log_level++;
375                                 m_log_level = MYMIN(m_log_level, LL_MAX - 1);
376                                 break;
377                 }
378         }
379
380         // was there a resize?
381         int xn, yn;
382         getmaxyx(stdscr, yn, xn);
383         if (xn != m_cols || yn != m_rows) {
384                 m_cols = xn;
385                 m_rows = yn;
386                 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
387                 complete_redraw_needed = true;
388         }
389
390         // draw title
391         move(0, 0);
392         clrtoeol();
393         addstr(PROJECT_NAME_C);
394         addstr(" ");
395         addstr(g_version_hash);
396
397         u32 minutes = m_time_of_day % 1000;
398         u32 hours = m_time_of_day / 1000;
399         minutes = (float)minutes / 1000 * 60;
400
401         if (m_game_time)
402                 printw(" | Game %" PRIu64 " Time of day %02d:%02d ",
403                         m_game_time, hours, minutes);
404
405         // draw text
406         if (complete_redraw_needed && m_can_draw_text)
407                 draw_text();
408
409         // draw prompt
410         if (!m_esc_mode) {
411                 // normal prompt
412                 ChatPrompt& prompt = m_chat_backend.getPrompt();
413                 std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
414                 move(m_rows - 1, 0);
415                 clrtoeol();
416                 addstr(prompt_text.c_str());
417                 // Draw cursor
418                 s32 cursor_pos = prompt.getVisibleCursorPosition();
419                 if (cursor_pos >= 0) {
420                         move(m_rows - 1, cursor_pos);
421                 }
422         } else {
423                 // esc prompt
424                 move(m_rows - 1, 0);
425                 clrtoeol();
426                 printw("[ESC] Toggle ESC mode |"
427                         " [CTRL+C] Shut down |"
428                         " (L) in-, (l) decrease loglevel %s",
429                         Logger::getLevelLabel((LogLevel) m_log_level).c_str());
430         }
431
432         refresh();
433 }
434
435 void TerminalChatConsole::draw_text()
436 {
437         ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
438         for (u32 row = 0; row < buf.getRows(); row++) {
439                 move_for_backend(row, 0);
440                 clrtoeol();
441                 const ChatFormattedLine& line = buf.getFormattedLine(row);
442                 if (line.fragments.empty())
443                         continue;
444                 for (const ChatFormattedFragment &fragment : line.fragments) {
445                         addstr(wide_to_utf8(fragment.text.getString()).c_str());
446                 }
447         }
448 }
449
450 void TerminalChatConsole::stopAndWaitforThread()
451 {
452         clearKillStatus();
453         stop();
454         wait();
455 }
456
457 #endif