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 // Helpful if you want to collect key codes that aren't documented
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;
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.
163 case ERR: // no input
167 m_esc_mode = !m_esc_mode;
170 m_chat_backend.scrollPageUp();
171 complete_redraw_needed = true;
174 m_chat_backend.scrollPageDown();
175 complete_redraw_needed = true;
180 std::wstring text = m_chat_backend.getPrompt().submit();
181 typeChatMessage(text);
185 m_chat_backend.getPrompt().historyPrev();
188 m_chat_backend.getPrompt().historyNext();
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
283 m_chat_backend.getPrompt().nickCompletion(m_nicks, false);
286 // Add character to the prompt,
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]);
302 } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
303 m_chat_backend.getPrompt().input(ch);
305 // Silently ignore characters we don't handle
307 //warningstream << "Pressed invalid character '"
308 // << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
314 void TerminalChatConsole::step(int ch)
316 bool complete_redraw_needed = false;
319 while (!m_chat_interface->outgoing_queue.empty()) {
320 ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
322 case CET_NICK_REMOVE:
323 m_nicks.remove(((ChatEventNick *)evt)->nick);
326 m_nicks.push_back(((ChatEventNick *)evt)->nick);
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);
335 ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
336 m_game_time = tevt->game_time;
337 m_time_of_day = tevt->time;
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)
347 m_chat_backend.addMessage(
348 utf8_to_wide(Logger::getLevelLabel(p.first)),
349 utf8_to_wide(p.second));
354 handleInput(ch, complete_redraw_needed);
357 case ERR: // no input
361 m_esc_mode = !m_esc_mode;
365 m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
369 m_log_level = MYMIN(m_log_level, LL_MAX - 1);
374 // was there a resize?
376 getmaxyx(stdscr, yn, xn);
377 if (xn != m_cols || yn != m_rows) {
380 m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
381 complete_redraw_needed = true;
387 addstr(PROJECT_NAME_C);
389 addstr(g_version_hash);
391 u32 minutes = m_time_of_day % 1000;
392 u32 hours = m_time_of_day / 1000;
393 minutes = (float)minutes / 1000 * 60;
396 printw(" | Game %d Time of day %02d:%02d ",
397 m_game_time, hours, minutes);
400 if (complete_redraw_needed && m_can_draw_text)
406 ChatPrompt& prompt = m_chat_backend.getPrompt();
407 std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
410 addstr(prompt_text.c_str());
412 s32 cursor_pos = prompt.getVisibleCursorPosition();
413 if (cursor_pos >= 0) {
414 move(m_rows - 1, cursor_pos);
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());
429 void TerminalChatConsole::draw_text()
431 ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
432 for (u32 row = 0; row < buf.getRows(); row++) {
433 move_for_backend(row, 0);
435 const ChatFormattedLine& line = buf.getFormattedLine(row);
436 if (line.fragments.empty())
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());
445 void TerminalChatConsole::stopAndWaitforThread()