3 Copyright (C) 2015 est31 <MTest31@outlook.com>
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.
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.
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.
23 #include "terminal_chat_console.h"
26 #include "util/numeric.h"
27 #include "util/string.h"
28 #include "chat_interface.h"
30 TerminalChatConsole g_term_console;
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
40 #elif CURSES_HAVE_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>
48 // Some functions to make drawing etc position independent
49 static bool reformat_backend(ChatBackend *backend, int rows, int cols)
53 backend->reformat(cols, rows - 2);
57 static void move_for_backend(int row, int col)
62 void TerminalChatConsole::initOfCurses()
68 nodelay(stdscr, TRUE);
71 // To make esc not delay up to one second. According to the internet,
72 // this is the value vim uses, too.
75 getmaxyx(stdscr, m_rows, m_cols);
76 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
79 void TerminalChatConsole::deInitOfCurses()
84 void *TerminalChatConsole::run()
86 BEGIN_DEBUG_EXCEPTION_HANDLER
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);
96 g_logger.addOutput(&m_log_output);
98 // Inform the server of our nick
99 m_chat_interface->command_queue.push_back(
100 new ChatEventNick(CET_NICK_ADD, m_nick));
103 // Ensures that curses is deinitialized even on an exception being thrown
104 CursesInitHelper helper(this);
106 while (!stopRequested()) {
116 if (m_kill_requested)
117 *m_kill_requested = true;
119 g_logger.removeOutput(&m_log_output);
120 g_logger.addOutputMasked(&stderr_output, err_mask);
121 g_logger.addOutputMasked(&stdout_output, out_mask);
123 std::cout << "End log output over terminal"
124 << " (no stdout/stderr backlog during that)" << std::endl;
125 std::cout << "========================" << std::endl;
127 END_DEBUG_EXCEPTION_HANDLER
132 void TerminalChatConsole::typeChatMessage(const std::wstring &msg)
134 // Discard empty line
139 m_chat_interface->command_queue.push_back(
140 new ChatEventChat(m_nick, msg));
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);
148 void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed)
150 ChatPrompt &prompt = m_chat_backend.getPrompt();
151 // Helpful if you want to collect key codes that aren't documented
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;
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.
165 case ERR: // no input
169 m_esc_mode = !m_esc_mode;
172 m_chat_backend.scrollPageUp();
173 complete_redraw_needed = true;
176 m_chat_backend.scrollPageDown();
177 complete_redraw_needed = true;
182 prompt.addToHistory(prompt.getLine());
183 typeChatMessage(prompt.replace(L""));
187 prompt.historyPrev();
190 prompt.historyNext();
194 // move character to the left
195 prompt.cursorOperation(
196 ChatPrompt::CURSOROP_MOVE,
197 ChatPrompt::CURSOROP_DIR_LEFT,
198 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
202 // move word to the left
203 prompt.cursorOperation(
204 ChatPrompt::CURSOROP_MOVE,
205 ChatPrompt::CURSOROP_DIR_LEFT,
206 ChatPrompt::CURSOROP_SCOPE_WORD);
210 // move character to the right
211 prompt.cursorOperation(
212 ChatPrompt::CURSOROP_MOVE,
213 ChatPrompt::CURSOROP_DIR_RIGHT,
214 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
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);
226 // move to beginning of line
227 prompt.cursorOperation(
228 ChatPrompt::CURSOROP_MOVE,
229 ChatPrompt::CURSOROP_DIR_LEFT,
230 ChatPrompt::CURSOROP_SCOPE_LINE);
234 // move to end of line
235 prompt.cursorOperation(
236 ChatPrompt::CURSOROP_MOVE,
237 ChatPrompt::CURSOROP_DIR_RIGHT,
238 ChatPrompt::CURSOROP_SCOPE_LINE);
244 // delete character to the left
245 prompt.cursorOperation(
246 ChatPrompt::CURSOROP_DELETE,
247 ChatPrompt::CURSOROP_DIR_LEFT,
248 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
252 // delete character to the right
253 prompt.cursorOperation(
254 ChatPrompt::CURSOROP_DELETE,
255 ChatPrompt::CURSOROP_DIR_RIGHT,
256 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
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);
268 // kill line to left end
269 prompt.cursorOperation(
270 ChatPrompt::CURSOROP_DELETE,
271 ChatPrompt::CURSOROP_DIR_LEFT,
272 ChatPrompt::CURSOROP_SCOPE_LINE);
276 // kill line to right end
277 prompt.cursorOperation(
278 ChatPrompt::CURSOROP_DELETE,
279 ChatPrompt::CURSOROP_DIR_RIGHT,
280 ChatPrompt::CURSOROP_SCOPE_LINE);
285 prompt.nickCompletion(m_nicks, false);
288 // Add character to the prompt,
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]);
304 } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
307 // Silently ignore characters we don't handle
309 //warningstream << "Pressed invalid character '"
310 // << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
316 void TerminalChatConsole::step(int ch)
318 bool complete_redraw_needed = false;
321 while (!m_chat_interface->outgoing_queue.empty()) {
322 ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
324 case CET_NICK_REMOVE:
325 m_nicks.remove(((ChatEventNick *)evt)->nick);
328 m_nicks.push_back(((ChatEventNick *)evt)->nick);
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);
337 ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
338 m_game_time = tevt->game_time;
339 m_time_of_day = tevt->time;
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)
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)");
354 m_chat_backend.addMessage(error_message, utf8_to_wide(p.second));
359 handleInput(ch, complete_redraw_needed);
362 case ERR: // no input
366 m_esc_mode = !m_esc_mode;
370 m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
374 m_log_level = MYMIN(m_log_level, LL_MAX - 1);
379 // was there a resize?
381 getmaxyx(stdscr, yn, xn);
382 if (xn != m_cols || yn != m_rows) {
385 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
386 complete_redraw_needed = true;
392 addstr(PROJECT_NAME_C);
394 addstr(g_version_hash);
396 u32 minutes = m_time_of_day % 1000;
397 u32 hours = m_time_of_day / 1000;
398 minutes = (float)minutes / 1000 * 60;
401 printw(" | Game %d Time of day %02d:%02d ",
402 m_game_time, hours, minutes);
405 if (complete_redraw_needed && m_can_draw_text)
411 ChatPrompt& prompt = m_chat_backend.getPrompt();
412 std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
415 addstr(prompt_text.c_str());
417 s32 cursor_pos = prompt.getVisibleCursorPosition();
418 if (cursor_pos >= 0) {
419 move(m_rows - 1, cursor_pos);
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());
434 void TerminalChatConsole::draw_text()
436 ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
437 for (u32 row = 0; row < buf.getRows(); row++) {
438 move_for_backend(row, 0);
440 const ChatFormattedLine& line = buf.getFormattedLine(row);
441 if (line.fragments.empty())
443 for (const ChatFormattedFragment &fragment : line.fragments) {
444 addstr(wide_to_utf8(fragment.text.getString()).c_str());
449 void TerminalChatConsole::stopAndWaitforThread()