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
reformat_backend(ChatBackend * backend,int rows,int cols)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 
move_for_backend(int row,int col)57 static void move_for_backend(int row, int col)
58 {
59 	move(row + 1, col);
60 }
61 
initOfCurses()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 
deInitOfCurses()79 void TerminalChatConsole::deInitOfCurses()
80 {
81 	endwin();
82 }
83 
run()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 
typeChatMessage(const std::wstring & msg)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 
handleInput(int ch,bool & complete_redraw_needed)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 
step(int ch)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 
draw_text()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 
stopAndWaitforThread()449 void TerminalChatConsole::stopAndWaitforThread()
450 {
451 	clearKillStatus();
452 	stop();
453 	wait();
454 }
455 
456 #endif
457