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"
29 TerminalChatConsole g_term_console;
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
39 #elif CURSES_HAVE_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>
47 // Some functions to make drawing etc position independent
48 static bool reformat_backend(ChatBackend *backend, int rows, int cols)
52 backend->reformat(cols, rows - 2);
56 static void move_for_backend(int row, int col)
61 void TerminalChatConsole::initOfCurses()
67 nodelay(stdscr, TRUE);
70 // To make esc not delay up to one second. According to the internet,
71 // this is the value vim uses, too.
74 getmaxyx(stdscr, m_rows, m_cols);
75 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
78 void TerminalChatConsole::deInitOfCurses()
83 void *TerminalChatConsole::run()
85 BEGIN_DEBUG_EXCEPTION_HANDLER
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);
95 g_logger.addOutput(&m_log_output);
97 // Inform the server of our nick
98 m_chat_interface->command_queue.push_back(
99 new ChatEventNick(CET_NICK_ADD, m_nick));
102 // Ensures that curses is deinitialized even on an exception being thrown
103 CursesInitHelper helper(this);
105 while (!stopRequested()) {
115 if (m_kill_requested)
116 *m_kill_requested = true;
118 g_logger.removeOutput(&m_log_output);
119 g_logger.addOutputMasked(&stderr_output, err_mask);
120 g_logger.addOutputMasked(&stdout_output, out_mask);
122 std::cout << "End log output over terminal"
123 << " (no stdout/stderr backlog during that)" << std::endl;
124 std::cout << "========================" << std::endl;
126 END_DEBUG_EXCEPTION_HANDLER
131 void TerminalChatConsole::typeChatMessage(const std::wstring &msg)
133 // Discard empty line
138 m_chat_interface->command_queue.push_back(
139 new ChatEventChat(m_nick, msg));
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);
147 void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed)
149 ChatPrompt &prompt = m_chat_backend.getPrompt();
150 // Helpful if you want to collect key codes that aren't documented
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;
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.
164 case ERR: // no input
168 m_esc_mode = !m_esc_mode;
171 m_chat_backend.scrollPageUp();
172 complete_redraw_needed = true;
175 m_chat_backend.scrollPageDown();
176 complete_redraw_needed = true;
181 prompt.addToHistory(prompt.getLine());
182 typeChatMessage(prompt.replace(L""));
186 prompt.historyPrev();
189 prompt.historyNext();
193 // move character to the left
194 prompt.cursorOperation(
195 ChatPrompt::CURSOROP_MOVE,
196 ChatPrompt::CURSOROP_DIR_LEFT,
197 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
201 // move word to the left
202 prompt.cursorOperation(
203 ChatPrompt::CURSOROP_MOVE,
204 ChatPrompt::CURSOROP_DIR_LEFT,
205 ChatPrompt::CURSOROP_SCOPE_WORD);
209 // move character to the right
210 prompt.cursorOperation(
211 ChatPrompt::CURSOROP_MOVE,
212 ChatPrompt::CURSOROP_DIR_RIGHT,
213 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
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);
225 // move to beginning of line
226 prompt.cursorOperation(
227 ChatPrompt::CURSOROP_MOVE,
228 ChatPrompt::CURSOROP_DIR_LEFT,
229 ChatPrompt::CURSOROP_SCOPE_LINE);
233 // move to end of line
234 prompt.cursorOperation(
235 ChatPrompt::CURSOROP_MOVE,
236 ChatPrompt::CURSOROP_DIR_RIGHT,
237 ChatPrompt::CURSOROP_SCOPE_LINE);
243 // delete character to the left
244 prompt.cursorOperation(
245 ChatPrompt::CURSOROP_DELETE,
246 ChatPrompt::CURSOROP_DIR_LEFT,
247 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
251 // delete character to the right
252 prompt.cursorOperation(
253 ChatPrompt::CURSOROP_DELETE,
254 ChatPrompt::CURSOROP_DIR_RIGHT,
255 ChatPrompt::CURSOROP_SCOPE_CHARACTER);
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);
267 // kill line to left end
268 prompt.cursorOperation(
269 ChatPrompt::CURSOROP_DELETE,
270 ChatPrompt::CURSOROP_DIR_LEFT,
271 ChatPrompt::CURSOROP_SCOPE_LINE);
275 // kill line to right end
276 prompt.cursorOperation(
277 ChatPrompt::CURSOROP_DELETE,
278 ChatPrompt::CURSOROP_DIR_RIGHT,
279 ChatPrompt::CURSOROP_SCOPE_LINE);
284 prompt.nickCompletion(m_nicks, false);
287 // Add character to the prompt,
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]);
303 } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
306 // Silently ignore characters we don't handle
308 //warningstream << "Pressed invalid character '"
309 // << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
315 void TerminalChatConsole::step(int ch)
317 bool complete_redraw_needed = false;
320 while (!m_chat_interface->outgoing_queue.empty()) {
321 ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
323 case CET_NICK_REMOVE:
324 m_nicks.remove(((ChatEventNick *)evt)->nick);
327 m_nicks.push_back(((ChatEventNick *)evt)->nick);
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);
336 ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
337 m_game_time = tevt->game_time;
338 m_time_of_day = tevt->time;
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)
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)";
352 m_chat_backend.addMessage(error_message, utf8_to_wide(p.second));
357 handleInput(ch, complete_redraw_needed);
360 case ERR: // no input
364 m_esc_mode = !m_esc_mode;
368 m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
372 m_log_level = MYMIN(m_log_level, LL_MAX - 1);
377 // was there a resize?
379 getmaxyx(stdscr, yn, xn);
380 if (xn != m_cols || yn != m_rows) {
383 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
384 complete_redraw_needed = true;
390 addstr(PROJECT_NAME_C);
392 addstr(g_version_hash);
394 u32 minutes = m_time_of_day % 1000;
395 u32 hours = m_time_of_day / 1000;
396 minutes = (float)minutes / 1000 * 60;
399 printw(" | Game %d Time of day %02d:%02d ",
400 m_game_time, hours, minutes);
403 if (complete_redraw_needed && m_can_draw_text)
409 ChatPrompt& prompt = m_chat_backend.getPrompt();
410 std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
413 addstr(prompt_text.c_str());
415 s32 cursor_pos = prompt.getVisibleCursorPosition();
416 if (cursor_pos >= 0) {
417 move(m_rows - 1, cursor_pos);
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());
432 void TerminalChatConsole::draw_text()
434 ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
435 for (u32 row = 0; row < buf.getRows(); row++) {
436 move_for_backend(row, 0);
438 const ChatFormattedLine& line = buf.getFormattedLine(row);
439 if (line.fragments.empty())
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());
448 void TerminalChatConsole::stopAndWaitforThread()