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