summaryrefslogtreecommitdiff
path: root/src/terminal_chat_console.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/terminal_chat_console.cpp')
-rw-r--r--src/terminal_chat_console.cpp452
1 files changed, 452 insertions, 0 deletions
diff --git a/src/terminal_chat_console.cpp b/src/terminal_chat_console.cpp
new file mode 100644
index 000000000..ac06285eb
--- /dev/null
+++ b/src/terminal_chat_console.cpp
@@ -0,0 +1,452 @@
+/*
+Minetest
+Copyright (C) 2015 est31 <MTest31@outlook.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "config.h"
+#if USE_CURSES
+#include "version.h"
+#include "terminal_chat_console.h"
+#include "porting.h"
+#include "settings.h"
+#include "util/numeric.h"
+#include "util/string.h"
+
+TerminalChatConsole g_term_console;
+
+// include this last to avoid any conflicts
+// (likes to set macros to common names, conflicting various stuff)
+#if CURSES_HAVE_NCURSESW_NCURSES_H
+#include <ncursesw/ncurses.h>
+#elif CURSES_HAVE_NCURSESW_CURSES_H
+#include <ncursesw/curses.h>
+#elif CURSES_HAVE_CURSES_H
+#include <curses.h>
+#elif CURSES_HAVE_NCURSES_H
+#include <ncurses.h>
+#elif CURSES_HAVE_NCURSES_NCURSES_H
+#include <ncurses/ncurses.h>
+#elif CURSES_HAVE_NCURSES_CURSES_H
+#include <ncurses/curses.h>
+#endif
+
+// Some functions to make drawing etc position independent
+static bool reformat_backend(ChatBackend *backend, int rows, int cols)
+{
+ if (rows < 2)
+ return false;
+ backend->reformat(cols, rows - 2);
+ return true;
+}
+
+static void move_for_backend(int row, int col)
+{
+ move(row + 1, col);
+}
+
+void TerminalChatConsole::initOfCurses()
+{
+ initscr();
+ cbreak(); //raw();
+ noecho();
+ keypad(stdscr, TRUE);
+ nodelay(stdscr, TRUE);
+ timeout(100);
+
+ // To make esc not delay up to one second. According to the internet,
+ // this is the value vim uses, too.
+ set_escdelay(25);
+
+ getmaxyx(stdscr, m_rows, m_cols);
+ m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
+}
+
+void TerminalChatConsole::deInitOfCurses()
+{
+ endwin();
+}
+
+void *TerminalChatConsole::run()
+{
+ BEGIN_DEBUG_EXCEPTION_HANDLER
+
+ std::cout << "========================" << std::endl;
+ std::cout << "Begin log output over terminal"
+ << " (no stdout/stderr backlog during that)" << std::endl;
+ // Make the loggers to stdout/stderr shut up.
+ // Go over our own loggers instead.
+ LogLevelMask err_mask = g_logger.removeOutput(&stderr_output);
+ LogLevelMask out_mask = g_logger.removeOutput(&stdout_output);
+
+ g_logger.addOutput(&m_log_output);
+
+ // Inform the server of our nick
+ m_chat_interface->command_queue.push_back(
+ new ChatEventNick(CET_NICK_ADD, m_nick));
+
+ {
+ // Ensures that curses is deinitialized even on an exception being thrown
+ CursesInitHelper helper(this);
+
+ while (!stopRequested()) {
+
+ int ch = getch();
+ if (stopRequested())
+ break;
+
+ step(ch);
+ }
+ }
+
+ if (m_kill_requested)
+ *m_kill_requested = true;
+
+ g_logger.removeOutput(&m_log_output);
+ g_logger.addOutputMasked(&stderr_output, err_mask);
+ g_logger.addOutputMasked(&stdout_output, out_mask);
+
+ std::cout << "End log output over terminal"
+ << " (no stdout/stderr backlog during that)" << std::endl;
+ std::cout << "========================" << std::endl;
+
+ END_DEBUG_EXCEPTION_HANDLER
+
+ return NULL;
+}
+
+void TerminalChatConsole::typeChatMessage(const std::wstring &msg)
+{
+ // Discard empty line
+ if (msg.empty())
+ return;
+
+ // Send to server
+ m_chat_interface->command_queue.push_back(
+ new ChatEventChat(m_nick, msg));
+
+ // Print if its a command (gets eaten by server otherwise)
+ if (msg[0] == L'/') {
+ m_chat_backend.addMessage(L"", (std::wstring)L"Issued command: " + msg);
+ }
+}
+
+void TerminalChatConsole::handleInput(int ch, bool &complete_redraw_needed)
+{
+ // Helpful if you want to collect key codes that aren't documented
+ /*if (ch != ERR) {
+ m_chat_backend.addMessage(L"",
+ (std::wstring)L"Pressed key " + utf8_to_wide(
+ std::string(keyname(ch)) + " (code " + itos(ch) + ")"));
+ complete_redraw_needed = true;
+ }//*/
+
+ // All the key codes below are compatible to xterm
+ // Only add new ones if you have tried them there,
+ // to ensure compatibility with not just xterm but the wide
+ // range of terminals that are compatible to xterm.
+
+ switch (ch) {
+ case ERR: // no input
+ break;
+ case 27: // ESC
+ // Toggle ESC mode
+ m_esc_mode = !m_esc_mode;
+ break;
+ case KEY_PPAGE:
+ m_chat_backend.scrollPageUp();
+ complete_redraw_needed = true;
+ break;
+ case KEY_NPAGE:
+ m_chat_backend.scrollPageDown();
+ complete_redraw_needed = true;
+ break;
+ case KEY_ENTER:
+ case '\r':
+ case '\n': {
+ std::wstring text = m_chat_backend.getPrompt().submit();
+ typeChatMessage(text);
+ break;
+ }
+ case KEY_UP:
+ m_chat_backend.getPrompt().historyPrev();
+ break;
+ case KEY_DOWN:
+ m_chat_backend.getPrompt().historyNext();
+ break;
+ case KEY_LEFT:
+ // Left pressed
+ // move character to the left
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+ break;
+ case 545:
+ // Ctrl-Left pressed
+ // move word to the left
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_WORD);
+ break;
+ case KEY_RIGHT:
+ // Right pressed
+ // move character to the right
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+ break;
+ case 560:
+ // Ctrl-Right pressed
+ // move word to the right
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_WORD);
+ break;
+ case KEY_HOME:
+ // Home pressed
+ // move to beginning of line
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ break;
+ case KEY_END:
+ // End pressed
+ // move to end of line
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_MOVE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ break;
+ case KEY_BACKSPACE:
+ case '\b':
+ case 127:
+ // Backspace pressed
+ // delete character to the left
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+ break;
+ case KEY_DC:
+ // Delete pressed
+ // delete character to the right
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_CHARACTER);
+ break;
+ case 519:
+ // Ctrl-Delete pressed
+ // delete word to the right
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_WORD);
+ break;
+ case 21:
+ // Ctrl-U pressed
+ // kill line to left end
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_LEFT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ break;
+ case 11:
+ // Ctrl-K pressed
+ // kill line to right end
+ m_chat_backend.getPrompt().cursorOperation(
+ ChatPrompt::CURSOROP_DELETE,
+ ChatPrompt::CURSOROP_DIR_RIGHT,
+ ChatPrompt::CURSOROP_SCOPE_LINE);
+ break;
+ case KEY_TAB:
+ // Tab pressed
+ // Nick completion
+ m_chat_backend.getPrompt().nickCompletion(m_nicks, false);
+ break;
+ default:
+ // Add character to the prompt,
+ // assuming UTF-8.
+ if (IS_UTF8_MULTB_START(ch)) {
+ m_pending_utf8_bytes.append(1, (char)ch);
+ m_utf8_bytes_to_wait += UTF8_MULTB_START_LEN(ch) - 1;
+ } else if (m_utf8_bytes_to_wait != 0) {
+ m_pending_utf8_bytes.append(1, (char)ch);
+ m_utf8_bytes_to_wait--;
+ if (m_utf8_bytes_to_wait == 0) {
+ std::wstring w = utf8_to_wide(m_pending_utf8_bytes);
+ m_pending_utf8_bytes = "";
+ // hopefully only one char in the wstring...
+ for (size_t i = 0; i < w.size(); i++) {
+ m_chat_backend.getPrompt().input(w.c_str()[i]);
+ }
+ }
+ } else if (IS_ASCII_PRINTABLE_CHAR(ch)) {
+ m_chat_backend.getPrompt().input(ch);
+ } else {
+ // Silently ignore characters we don't handle
+
+ //warningstream << "Pressed invalid character '"
+ // << keyname(ch) << "' (code " << itos(ch) << ")" << std::endl;
+ }
+ break;
+ }
+}
+
+void TerminalChatConsole::step(int ch)
+{
+ bool complete_redraw_needed = false;
+
+ // empty queues
+ while (!m_chat_interface->outgoing_queue.empty()) {
+ ChatEvent *evt = m_chat_interface->outgoing_queue.pop_frontNoEx();
+ switch (evt->type) {
+ case CET_NICK_REMOVE:
+ m_nicks.remove(((ChatEventNick *)evt)->nick);
+ break;
+ case CET_NICK_ADD:
+ m_nicks.push_back(((ChatEventNick *)evt)->nick);
+ break;
+ case CET_CHAT:
+ complete_redraw_needed = true;
+ // This is only used for direct replies from commands
+ // or for lua's print() functionality
+ m_chat_backend.addMessage(L"", ((ChatEventChat *)evt)->evt_msg);
+ break;
+ case CET_TIME_INFO:
+ ChatEventTimeInfo *tevt = (ChatEventTimeInfo *)evt;
+ m_game_time = tevt->game_time;
+ m_time_of_day = tevt->time;
+ };
+ delete evt;
+ }
+ while (!m_log_output.queue.empty()) {
+ complete_redraw_needed = true;
+ std::pair<LogLevel, std::string> p = m_log_output.queue.pop_frontNoEx();
+ if (p.first > m_log_level)
+ continue;
+
+ m_chat_backend.addMessage(
+ utf8_to_wide(Logger::getLevelLabel(p.first)),
+ utf8_to_wide(p.second));
+ }
+
+ // handle input
+ if (!m_esc_mode) {
+ handleInput(ch, complete_redraw_needed);
+ } else {
+ switch (ch) {
+ case ERR: // no input
+ break;
+ case 27: // ESC
+ // Toggle ESC mode
+ m_esc_mode = !m_esc_mode;
+ break;
+ case 'L':
+ m_log_level--;
+ m_log_level = MYMAX(m_log_level, LL_NONE + 1); // LL_NONE isn't accessible
+ break;
+ case 'l':
+ m_log_level++;
+ m_log_level = MYMIN(m_log_level, LL_MAX - 1);
+ break;
+ }
+ }
+
+ // was there a resize?
+ int xn, yn;
+ getmaxyx(stdscr, yn, xn);
+ if (xn != m_cols || yn != m_rows) {
+ m_cols = xn;
+ m_rows = yn;
+ m_can_draw_text = reformat_backend(&m_chat_backend, m_rows, m_cols);
+ complete_redraw_needed = true;
+ }
+
+ // draw title
+ move(0, 0);
+ clrtoeol();
+ addstr(PROJECT_NAME_C);
+ addstr(" ");
+ addstr(g_version_hash);
+
+ u32 minutes = m_time_of_day % 1000;
+ u32 hours = m_time_of_day / 1000;
+ minutes = (float)minutes / 1000 * 60;
+
+ if (m_game_time)
+ printw(" | Game %d Time of day %02d:%02d ",
+ m_game_time, hours, minutes);
+
+ // draw text
+ if (complete_redraw_needed && m_can_draw_text)
+ draw_text();
+
+ // draw prompt
+ if (!m_esc_mode) {
+ // normal prompt
+ ChatPrompt& prompt = m_chat_backend.getPrompt();
+ std::string prompt_text = wide_to_utf8(prompt.getVisiblePortion());
+ move(m_rows - 1, 0);
+ clrtoeol();
+ addstr(prompt_text.c_str());
+ // Draw cursor
+ s32 cursor_pos = prompt.getVisibleCursorPosition();
+ if (cursor_pos >= 0) {
+ move(m_rows - 1, cursor_pos);
+ }
+ } else {
+ // esc prompt
+ move(m_rows - 1, 0);
+ clrtoeol();
+ printw("[ESC] Toggle ESC mode |"
+ " [CTRL+C] Shut down |"
+ " (L) in-, (l) decrease loglevel %s",
+ Logger::getLevelLabel((LogLevel) m_log_level).c_str());
+ }
+
+ refresh();
+}
+
+void TerminalChatConsole::draw_text()
+{
+ ChatBuffer& buf = m_chat_backend.getConsoleBuffer();
+ for (u32 row = 0; row < buf.getRows(); row++) {
+ move_for_backend(row, 0);
+ clrtoeol();
+ const ChatFormattedLine& line = buf.getFormattedLine(row);
+ if (line.fragments.empty())
+ continue;
+ for (u32 i = 0; i < line.fragments.size(); ++i) {
+ const ChatFormattedFragment& fragment = line.fragments[i];
+ addstr(wide_to_utf8(fragment.text).c_str());
+ }
+ }
+}
+
+void TerminalChatConsole::stopAndWaitforThread()
+{
+ clearKillStatus();
+ stop();
+ wait();
+}
+
+#endif