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()