]> git.lizzy.rs Git - minetest.git/blob - src/terminal_chat_console.cpp
Shaders: Remove special handling for liquids. (#4670)
[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         ChatPrompt &prompt = m_chat_backend.getPrompt();
150         // Helpful if you want to collect key codes that aren't documented
151         /*if (ch != ERR) {
152                 m_chat_backend.addMessage(L"",
153                         (std::wstring)L"Pressed key " + utf8_to_wide(
154                         std::string(keyname(ch)) + " (code " + itos(ch) + ")"));
155                 complete_redraw_needed = true;
156         }//*/
157
158         // All the key codes below are compatible to xterm
159         // Only add new ones if you have tried them there,
160         // to ensure compatibility with not just xterm but the wide
161         // range of terminals that are compatible to xterm.
162
163         switch (ch) {
164                 case ERR: // no input
165                         break;
166                 case 27: // ESC
167                         // Toggle ESC mode
168                         m_esc_mode = !m_esc_mode;
169                         break;
170                 case KEY_PPAGE:
171                         m_chat_backend.scrollPageUp();
172                         complete_redraw_needed = true;
173                         break;
174                 case KEY_NPAGE:
175                         m_chat_backend.scrollPageDown();
176                         complete_redraw_needed = true;
177                         break;
178                 case KEY_ENTER:
179                 case '\r':
180                 case '\n': {
181                         prompt.addToHistory(prompt.getLine());
182                         typeChatMessage(prompt.replace(L""));
183                         break;
184                 }
185                 case KEY_UP:
186                         prompt.historyPrev();
187                         break;
188                 case KEY_DOWN:
189                         prompt.historyNext();
190                         break;
191                 case KEY_LEFT:
192                         // Left pressed
193                         // move character to the left
194                         prompt.cursorOperation(
195                                 ChatPrompt::CURSOROP_MOVE,
196                                 ChatPrompt::CURSOROP_DIR_LEFT,
197                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
198                         break;
199                 case 545:
200                         // Ctrl-Left pressed
201                         // move word to the left
202                         prompt.cursorOperation(
203                                 ChatPrompt::CURSOROP_MOVE,
204                                 ChatPrompt::CURSOROP_DIR_LEFT,
205                                 ChatPrompt::CURSOROP_SCOPE_WORD);
206                         break;
207                 case KEY_RIGHT:
208                         // Right pressed
209                         // move character to the right
210                         prompt.cursorOperation(
211                                 ChatPrompt::CURSOROP_MOVE,
212                                 ChatPrompt::CURSOROP_DIR_RIGHT,
213                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
214                         break;
215                 case 560:
216                         // Ctrl-Right pressed
217                         // move word to the right
218                         prompt.cursorOperation(
219                                 ChatPrompt::CURSOROP_MOVE,
220                                 ChatPrompt::CURSOROP_DIR_RIGHT,
221                                 ChatPrompt::CURSOROP_SCOPE_WORD);
222                         break;
223                 case KEY_HOME:
224                         // Home pressed
225                         // move to beginning of line
226                         prompt.cursorOperation(
227                                 ChatPrompt::CURSOROP_MOVE,
228                                 ChatPrompt::CURSOROP_DIR_LEFT,
229                                 ChatPrompt::CURSOROP_SCOPE_LINE);
230                         break;
231                 case KEY_END:
232                         // End pressed
233                         // move to end of line
234                         prompt.cursorOperation(
235                                 ChatPrompt::CURSOROP_MOVE,
236                                 ChatPrompt::CURSOROP_DIR_RIGHT,
237                                 ChatPrompt::CURSOROP_SCOPE_LINE);
238                         break;
239                 case KEY_BACKSPACE:
240                 case '\b':
241                 case 127:
242                         // Backspace pressed
243                         // delete character to the left
244                         prompt.cursorOperation(
245                                 ChatPrompt::CURSOROP_DELETE,
246                                 ChatPrompt::CURSOROP_DIR_LEFT,
247                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
248                         break;
249                 case KEY_DC:
250                         // Delete pressed
251                         // delete character to the right
252                         prompt.cursorOperation(
253                                 ChatPrompt::CURSOROP_DELETE,
254                                 ChatPrompt::CURSOROP_DIR_RIGHT,
255                                 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
256                         break;
257                 case 519:
258                         // Ctrl-Delete pressed
259                         // delete word to the right
260                         prompt.cursorOperation(
261                                 ChatPrompt::CURSOROP_DELETE,
262                                 ChatPrompt::CURSOROP_DIR_RIGHT,
263                                 ChatPrompt::CURSOROP_SCOPE_WORD);
264                         break;
265                 case 21:
266                         // Ctrl-U pressed
267                         // kill line to left end
268                         prompt.cursorOperation(
269                                 ChatPrompt::CURSOROP_DELETE,
270                                 ChatPrompt::CURSOROP_DIR_LEFT,
271                                 ChatPrompt::CURSOROP_SCOPE_LINE);
272                         break;
273                 case 11:
274                         // Ctrl-K pressed
275                         // kill line to right end
276                         prompt.cursorOperation(
277                                 ChatPrompt::CURSOROP_DELETE,
278                                 ChatPrompt::CURSOROP_DIR_RIGHT,
279                                 ChatPrompt::CURSOROP_SCOPE_LINE);
280                         break;
281                 case KEY_TAB:
282                         // Tab pressed
283                         // Nick completion
284                         prompt.nickCompletion(m_nicks, false);
285                         break;
286                 default:
287                         // Add character to the prompt,
288                         // assuming UTF-8.
289                         if (IS_UTF8_MULTB_START(ch)) {
290                                 m_pending_utf8_bytes.append(1, (char)ch);
291                                 m_utf8_bytes_to_wait += UTF8_MULTB_START_LEN(ch) - 1;
292                         } else if (m_utf8_bytes_to_wait != 0) {
293                                 m_pending_utf8_bytes.append(1, (char)ch);
294                                 m_utf8_bytes_to_wait--;
295                                 if (m_utf8_bytes_to_wait == 0) {
296                                         std::wstring w = utf8_to_wide(m_pending_utf8_bytes);
297                                         m_pending_utf8_bytes = "";
298                                         // hopefully only one char in the wstring...
299                                         for (size_t i = 0; i < w.size(); i++) {
300                                                 prompt.input(w.c_str()[i]);
301                                         }
302                                 }
303                         } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
304                                 prompt.input(ch);
305                         } else {
306                                 // Silently ignore characters we don't handle
307
308                                 //warningstream << "Pressed invalid character '"
309                                 //      << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
310                         }
311                         break;
312         }
313 }
314
315 void TerminalChatConsole::step(int ch)
316 {
317         bool complete_redraw_needed = false;
318
319         // empty queues
320         while (!m_chat_interface->outgoing_queue.empty()) {
321                 ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
322                 switch (evt->type) {
323                         case CET_NICK_REMOVE:
324                                 m_nicks.remove(((ChatEventNick *)evt)->nick);
325                                 break;
326                         case CET_NICK_ADD:
327                                 m_nicks.push_back(((ChatEventNick *)evt)->nick);
328                                 break;
329                         case CET_CHAT:
330                                 complete_redraw_needed = true;
331                                 // This is only used for direct replies from commands
332                                 // or for lua's print() functionality
333                                 m_chat_backend.addMessage(L"", ((ChatEventChat *)evt)->evt_msg);
334                                 break;
335                         case CET_TIME_INFO:
336                                 ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
337                                 m_game_time = tevt->game_time;
338                                 m_time_of_day = tevt->time;
339                 };
340                 delete evt;
341         }
342         while (!m_log_output.queue.empty()) {
343                 complete_redraw_needed = true;
344                 std::pair<LogLevel, std::string> p = m_log_output.queue.pop_frontNoEx();
345                 if (p.first > m_log_level)
346                         continue;
347
348                 std::wstring error_message = utf8_to_wide(Logger::getLevelLabel(p.first));
349                 if (!g_settings->getBool("disable_escape_sequences")) {
350                         error_message = L"\x1b(c@red)" + error_message + L"\x1b(c@white)";
351                 }
352                 m_chat_backend.addMessage(error_message, utf8_to_wide(p.second));
353         }
354
355         // handle input
356         if (!m_esc_mode) {
357                 handleInput(ch, complete_redraw_needed);
358         } else {
359                 switch (ch) {
360                         case ERR: // no input
361                                 break;
362                         case 27: // ESC
363                                 // Toggle ESC mode
364                                 m_esc_mode = !m_esc_mode;
365                                 break;
366                         case 'L':
367                                 m_log_level--;
368                                 m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
369                                 break;
370                         case 'l':
371                                 m_log_level++;
372                                 m_log_level = MYMIN(m_log_level, LL_MAX - 1);
373                                 break;
374                 }
375         }
376
377         // was there a resize?
378         int xn, yn;
379         getmaxyx(stdscr, yn, xn);
380         if (xn != m_cols || yn != m_rows) {
381                 m_cols = xn;
382                 m_rows = yn;
383                 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
384                 complete_redraw_needed = true;
385         }
386
387         // draw title
388         move(0, 0);
389         clrtoeol();
390         addstr(PROJECT_NAME_C);
391         addstr(" ");
392         addstr(g_version_hash);
393
394         u32 minutes = m_time_of_day % 1000;
395         u32 hours = m_time_of_day / 1000;
396         minutes = (float)minutes / 1000 * 60;
397
398         if (m_game_time)
399                 printw(" | Game %d Time of day %02d:%02d ",
400                         m_game_time, hours, minutes);
401
402         // draw text
403         if (complete_redraw_needed && m_can_draw_text)
404                 draw_text();
405
406         // draw prompt
407         if (!m_esc_mode) {
408                 // normal prompt
409                 ChatPrompt& prompt = m_chat_backend.getPrompt();
410                 std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
411                 move(m_rows - 1, 0);
412                 clrtoeol();
413                 addstr(prompt_text.c_str());
414                 // Draw cursor
415                 s32 cursor_pos = prompt.getVisibleCursorPosition();
416                 if (cursor_pos >= 0) {
417                         move(m_rows - 1, cursor_pos);
418                 }
419         } else {
420                 // esc prompt
421                 move(m_rows - 1, 0);
422                 clrtoeol();
423                 printw("[ESC] Toggle ESC mode |"
424                         " [CTRL+C] Shut down |"
425                         " (L) in-, (l) decrease loglevel %s",
426                         Logger::getLevelLabel((LogLevel) m_log_level).c_str());
427         }
428
429         refresh();
430 }
431
432 void TerminalChatConsole::draw_text()
433 {
434         ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
435         for (u32 row = 0; row < buf.getRows(); row++) {
436                 move_for_backend(row, 0);
437                 clrtoeol();
438                 const ChatFormattedLine& line = buf.getFormattedLine(row);
439                 if (line.fragments.empty())
440                         continue;
441                 for (u32 i = 0; i < line.fragments.size(); ++i) {
442                         const ChatFormattedFragment& fragment = line.fragments[i];
443                         addstr(wide_to_utf8(fragment.text.getString()).c_str());
444                 }
445         }
446 }
447
448 void TerminalChatConsole::stopAndWaitforThread()
449 {
450         clearKillStatus();
451         stop();
452         wait();
453 }
454
455 #endif