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