diff --git a/builtin/init.lua b/builtin/init.lua
index 02fb9db93674a155fb22602b264869d4065bf648..b3004468e5efd5ce853d6f942181ba5ccd9d516c 100644
--- a/builtin/init.lua
+++ b/builtin/init.lua
@@ -7,6 +7,15 @@
 
 -- Initialize some very basic things
 function core.debug(...) core.log(table.concat({...}, "\t")) end
+if core.print then
+	local core_print = core.print
+	-- Override native print and use
+	-- terminal if that's turned on
+	function print(...)
+		core_print(table.concat({...}, "\t"))
+	end
+	core.print = nil -- don't pollute our namespace
+end
 math.randomseed(os.time())
 os.setlocale("C", "numeric")
 minetest = core
diff --git a/cmake/Modules/FindNcursesw.cmake b/cmake/Modules/FindNcursesw.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..dcb7cdda83c1527d48c8f53f5827f1ac702c0c79
--- /dev/null
+++ b/cmake/Modules/FindNcursesw.cmake
@@ -0,0 +1,189 @@
+#.rst:
+# FindNcursesw
+# ------------
+#
+# Find the ncursesw (wide ncurses) include file and library.
+#
+# Based on FindCurses.cmake which comes with CMake.
+#
+# Checks for ncursesw first. If not found, it then executes the
+# regular old FindCurses.cmake to look for for ncurses (or curses).
+#
+#
+# Result Variables
+# ^^^^^^^^^^^^^^^^
+#
+# This module defines the following variables:
+#
+# ``CURSES_FOUND``
+#   True if curses is found.
+# ``NCURSESW_FOUND``
+#   True if ncursesw is found.
+# ``CURSES_INCLUDE_DIRS``
+#   The include directories needed to use Curses.
+# ``CURSES_LIBRARIES``
+#   The libraries needed to use Curses.
+# ``CURSES_HAVE_CURSES_H``
+#   True if curses.h is available.
+# ``CURSES_HAVE_NCURSES_H``
+#   True if ncurses.h is available.
+# ``CURSES_HAVE_NCURSES_NCURSES_H``
+#   True if ``ncurses/ncurses.h`` is available.
+# ``CURSES_HAVE_NCURSES_CURSES_H``
+#   True if ``ncurses/curses.h`` is available.
+# ``CURSES_HAVE_NCURSESW_NCURSES_H``
+#   True if ``ncursesw/ncurses.h`` is available.
+# ``CURSES_HAVE_NCURSESW_CURSES_H``
+#   True if ``ncursesw/curses.h`` is available.
+#
+# Set ``CURSES_NEED_NCURSES`` to ``TRUE`` before the
+# ``find_package(Ncursesw)`` call if NCurses functionality is required.
+#
+#=============================================================================
+# Copyright 2001-2014 Kitware, Inc.
+# modifications: Copyright 2015 kahrl <kahrl@gmx.net>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+#   notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+#   notice, this list of conditions and the following disclaimer in the
+#   documentation and/or other materials provided with the distribution.
+#
+# * Neither the names of Kitware, Inc., the Insight Software Consortium,
+#   nor the names of their contributors may be used to endorse or promote
+#   products derived from this software without specific prior written
+#   permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# ------------------------------------------------------------------------------
+#
+# The above copyright and license notice applies to distributions of
+# CMake in source and binary form.  Some source files contain additional
+# notices of original copyright by their contributors; see each source
+# for details.  Third-party software packages supplied with CMake under
+# compatible licenses provide their own copyright notices documented in
+# corresponding subdirectories.
+#
+# ------------------------------------------------------------------------------
+#
+# CMake was initially developed by Kitware with the following sponsorship:
+#
+#  * National Library of Medicine at the National Institutes of Health
+#    as part of the Insight Segmentation and Registration Toolkit (ITK).
+#
+#  * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel
+#    Visualization Initiative.
+#
+#  * National Alliance for Medical Image Computing (NAMIC) is funded by the
+#    National Institutes of Health through the NIH Roadmap for Medical Research,
+#    Grant U54 EB005149.
+#
+#  * Kitware, Inc.
+#=============================================================================
+
+include(CheckLibraryExists)
+
+find_library(CURSES_NCURSESW_LIBRARY NAMES ncursesw
+  DOC "Path to libncursesw.so or .lib or .a")
+
+set(CURSES_USE_NCURSES FALSE)
+set(CURSES_USE_NCURSESW FALSE)
+
+if(CURSES_NCURSESW_LIBRARY)
+  set(CURSES_USE_NCURSES TRUE)
+  set(CURSES_USE_NCURSESW TRUE)
+endif()
+
+if(CURSES_USE_NCURSESW)
+  get_filename_component(_cursesLibDir "${CURSES_NCURSESW_LIBRARY}" PATH)
+  get_filename_component(_cursesParentDir "${_cursesLibDir}" PATH)
+
+  find_path(CURSES_INCLUDE_PATH
+    NAMES ncursesw/ncurses.h ncursesw/curses.h
+    HINTS "${_cursesParentDir}/include"
+    )
+
+  # Previous versions of FindCurses provided these values.
+  if(NOT DEFINED CURSES_LIBRARY)
+    set(CURSES_LIBRARY "${CURSES_NCURSESW_LIBRARY}")
+  endif()
+
+  CHECK_LIBRARY_EXISTS("${CURSES_NCURSESW_LIBRARY}"
+    cbreak "" CURSES_NCURSESW_HAS_CBREAK)
+  if(NOT CURSES_NCURSESW_HAS_CBREAK)
+    find_library(CURSES_EXTRA_LIBRARY tinfo HINTS "${_cursesLibDir}"
+      DOC "Path to libtinfo.so or .lib or .a")
+    find_library(CURSES_EXTRA_LIBRARY tinfo )
+  endif()
+
+  # Report whether each possible header name exists in the include directory.
+  if(NOT DEFINED CURSES_HAVE_NCURSESW_NCURSES_H)
+    if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h")
+      set(CURSES_HAVE_NCURSESW_NCURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h")
+    else()
+      set(CURSES_HAVE_NCURSESW_NCURSES_H "CURSES_HAVE_NCURSESW_NCURSES_H-NOTFOUND")
+    endif()
+  endif()
+  if(NOT DEFINED CURSES_HAVE_NCURSESW_CURSES_H)
+    if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/curses.h")
+      set(CURSES_HAVE_NCURSESW_CURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/curses.h")
+    else()
+      set(CURSES_HAVE_NCURSESW_CURSES_H "CURSES_HAVE_NCURSESW_CURSES_H-NOTFOUND")
+    endif()
+  endif()
+
+  find_library(CURSES_FORM_LIBRARY form HINTS "${_cursesLibDir}"
+    DOC "Path to libform.so or .lib or .a")
+  find_library(CURSES_FORM_LIBRARY form )
+
+  # Need to provide the *_LIBRARIES
+  set(CURSES_LIBRARIES ${CURSES_LIBRARY})
+
+  if(CURSES_EXTRA_LIBRARY)
+    set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_EXTRA_LIBRARY})
+  endif()
+
+  if(CURSES_FORM_LIBRARY)
+    set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_FORM_LIBRARY})
+  endif()
+
+  # Provide the *_INCLUDE_DIRS result.
+  set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_PATH})
+  set(CURSES_INCLUDE_DIR ${CURSES_INCLUDE_PATH}) # compatibility
+
+  # handle the QUIETLY and REQUIRED arguments and set CURSES_FOUND to TRUE if
+  # all listed variables are TRUE
+  include(FindPackageHandleStandardArgs)
+  FIND_PACKAGE_HANDLE_STANDARD_ARGS(Ncursesw DEFAULT_MSG
+    CURSES_LIBRARY CURSES_INCLUDE_PATH)
+  set(CURSES_FOUND ${NCURSESW_FOUND})
+
+else()
+  find_package(Curses)
+  set(NCURSESW_FOUND FALSE)
+endif()
+
+mark_as_advanced(
+  CURSES_INCLUDE_PATH
+  CURSES_CURSES_LIBRARY
+  CURSES_NCURSES_LIBRARY
+  CURSES_NCURSESW_LIBRARY
+  CURSES_EXTRA_LIBRARY
+  CURSES_FORM_LIBRARY
+  )
diff --git a/doc/minetest.6 b/doc/minetest.6
index 036cea6c93aea61a793525ea27037be6677deee9..a135e541cfbc4bc7fcd4dccefdeb8140685c3d88 100644
--- a/doc/minetest.6
+++ b/doc/minetest.6
@@ -89,6 +89,9 @@ Run speed tests
 .B \-\-migrate <value>
 Migrate from current map backend to another. Possible values are sqlite3,
 leveldb, redis, and dummy.
+.TP
+.B \-\-terminal
+Display an interactive terminal over ncurses during execution.
 
 .SH ENVIRONMENT
 .TP
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 72b52436ce45f237f226474b3d9408c2e7c0d98c..55f5d4ad850777a2b1e122a7e04a2132171eed0a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -160,6 +160,20 @@ find_package(Lua REQUIRED)
 
 find_package(GMP REQUIRED)
 
+option(ENABLE_CURSES "Enable ncurses console" TRUE)
+set(USE_CURSES FALSE)
+
+if(ENABLE_CURSES)
+	find_package(Ncursesw)
+	if(CURSES_FOUND)
+		set(USE_CURSES TRUE)
+		message(STATUS "ncurses console enabled.")
+		include_directories(${CURSES_INCLUDE_DIRS})
+	else()
+		message(STATUS "ncurses not found!")
+	endif()
+endif(ENABLE_CURSES)
+
 option(ENABLE_LEVELDB "Enable LevelDB backend" TRUE)
 set(USE_LEVELDB FALSE)
 
@@ -322,6 +336,7 @@ set(common_SRCS
 	areastore.cpp
 	ban.cpp
 	cavegen.cpp
+	chat.cpp
 	clientiface.cpp
 	collision.cpp
 	content_abm.cpp
@@ -387,6 +402,7 @@ set(common_SRCS
 	sound.cpp
 	staticobject.cpp
 	subgame.cpp
+	terminal_chat_console.cpp
 	tool.cpp
 	treegen.cpp
 	version.cpp
@@ -431,7 +447,6 @@ set(client_SRCS
 	${sound_SRCS}
 	${client_network_SRCS}
 	camera.cpp
-	chat.cpp
 	client.cpp
 	clientmap.cpp
 	clientmedia.cpp
@@ -558,6 +573,9 @@ if(BUILD_CLIENT)
 			${CGUITTFONT_LIBRARY}
 		)
 	endif()
+	if (USE_CURSES)
+		target_link_libraries(${PROJECT_NAME} ${CURSES_LIBRARIES})
+	endif()
 	if (USE_LEVELDB)
 		target_link_libraries(${PROJECT_NAME} ${LEVELDB_LIBRARY})
 	endif()
@@ -585,6 +603,9 @@ if(BUILD_SERVER)
 	)
 	set_target_properties(${PROJECT_NAME}server PROPERTIES
 			COMPILE_DEFINITIONS "SERVER")
+	if (USE_CURSES)
+		target_link_libraries(${PROJECT_NAME}server ${CURSES_LIBRARIES})
+	endif()
 	if (USE_LEVELDB)
 		target_link_libraries(${PROJECT_NAME}server ${LEVELDB_LIBRARY})
 	endif()
diff --git a/src/chat.h b/src/chat.h
index 82ce80875a2bd7d38e635e13015b7085af070078..5d26baf7bbdb247f09197bf52b5d74d5eacb2785 100644
--- a/src/chat.h
+++ b/src/chat.h
@@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <vector>
 #include <list>
 
-// Chat console related classes, only used by the client
+// Chat console related classes
 
 struct ChatLine
 {
@@ -123,7 +123,7 @@ class ChatBuffer
 	u32 m_scrollback;
 	// Array of unformatted chat lines
 	std::vector<ChatLine> m_unformatted;
-	
+
 	// Number of character columns in console
 	u32 m_cols;
 	// Number of character rows in console
@@ -213,7 +213,7 @@ class ChatPrompt
 	std::wstring m_line;
 	// History buffer
 	std::vector<std::wstring> m_history;
-	// History index (0 <= m_history_index <= m_history.size()) 
+	// History index (0 <= m_history_index <= m_history.size())
 	u32 m_history_index;
 	// Maximum number of history entries
 	u32 m_history_limit;
diff --git a/src/chat_interface.h b/src/chat_interface.h
new file mode 100644
index 0000000000000000000000000000000000000000..4784821fc60e56cac5739cee1ecacba6479fc32a
--- /dev/null
+++ b/src/chat_interface.h
@@ -0,0 +1,82 @@
+/*
+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.
+*/
+
+#ifndef CHAT_INTERFACE_H
+#define CHAT_INTERFACE_H
+
+#include "util/container.h"
+#include <string>
+#include <queue>
+#include "irrlichttypes.h"
+
+enum ChatEventType {
+	CET_CHAT,
+	CET_NICK_ADD,
+	CET_NICK_REMOVE,
+	CET_TIME_INFO,
+};
+
+class ChatEvent {
+protected:
+	ChatEvent(ChatEventType a_type) { type = a_type; }
+public:
+	ChatEventType type;
+};
+
+struct ChatEventTimeInfo : public ChatEvent {
+	ChatEventTimeInfo(
+		u64 a_game_time,
+		u32 a_time) :
+	ChatEvent(CET_TIME_INFO),
+	game_time(a_game_time),
+	time(a_time)
+	{}
+
+	u64 game_time;
+	u32 time;
+};
+
+struct ChatEventNick : public ChatEvent {
+	ChatEventNick(ChatEventType a_type,
+		const std::string &a_nick) :
+	ChatEvent(a_type), // one of CET_NICK_ADD, CET_NICK_REMOVE
+	nick(a_nick)
+	{}
+
+	std::string nick;
+};
+
+struct ChatEventChat : public ChatEvent {
+	ChatEventChat(const std::string &a_nick,
+		const std::wstring &an_evt_msg) :
+	ChatEvent(CET_CHAT),
+	nick(a_nick),
+	evt_msg(an_evt_msg)
+	{}
+
+	std::string nick;
+	std::wstring evt_msg;
+};
+
+struct ChatInterface {
+	MutexedQueue<ChatEvent *> command_queue; // chat backend --> server
+	MutexedQueue<ChatEvent *> outgoing_queue; // server --> chat backend
+};
+
+#endif
diff --git a/src/cmake_config.h.in b/src/cmake_config.h.in
index bda7a891abc5ba7b26a747b1701cb3fbb544a751..018532d131927f5fb94b67e4438c365f1a2acb9a 100644
--- a/src/cmake_config.h.in
+++ b/src/cmake_config.h.in
@@ -19,12 +19,19 @@
 #cmakedefine01 USE_CURL
 #cmakedefine01 USE_SOUND
 #cmakedefine01 USE_FREETYPE
+#cmakedefine01 USE_CURSES
 #cmakedefine01 USE_LEVELDB
 #cmakedefine01 USE_LUAJIT
 #cmakedefine01 USE_SPATIAL
 #cmakedefine01 USE_SYSTEM_GMP
 #cmakedefine01 USE_REDIS
 #cmakedefine01 HAVE_ENDIAN_H
+#cmakedefine01 CURSES_HAVE_CURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSES_CURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSESW_NCURSES_H
+#cmakedefine01 CURSES_HAVE_NCURSESW_CURSES_H
 
 #endif
 
diff --git a/src/debug.cpp b/src/debug.cpp
index 3761e416d5c0b2ef4bbf7ace693b2cb086121a75..8647160b1bed7c20cae3dae6e035bfc23bef521e 100644
--- a/src/debug.cpp
+++ b/src/debug.cpp
@@ -37,6 +37,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 	#include "filesys.h"
 #endif
 
+#if USE_CURSES
+	#include "terminal_chat_console.h"
+#endif
+
 /*
 	Assert
 */
@@ -44,6 +48,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 void sanity_check_fn(const char *assertion, const char *file,
 		unsigned int line, const char *function)
 {
+#if USE_CURSES
+	g_term_console.stopAndWaitforThread();
+#endif
+
 	errorstream << std::endl << "In thread " << std::hex
 		<< thr_get_current_thread_id() << ":" << std::endl;
 	errorstream << file << ":" << line << ": " << function
@@ -57,6 +65,10 @@ void sanity_check_fn(const char *assertion, const char *file,
 void fatal_error_fn(const char *msg, const char *file,
 		unsigned int line, const char *function)
 {
+#if USE_CURSES
+	g_term_console.stopAndWaitforThread();
+#endif
+
 	errorstream << std::endl << "In thread " << std::hex
 		<< thr_get_current_thread_id() << ":" << std::endl;
 	errorstream << file << ":" << line << ": " << function
diff --git a/src/log.cpp b/src/log.cpp
index 5cba8f70056a53ccd90199e9df656ef7c1f407dc..600e715c12680653379b935551aa2e48384bbffe 100644
--- a/src/log.cpp
+++ b/src/log.cpp
@@ -181,6 +181,14 @@ void Logger::addOutput(ILogOutput *out, LogLevel lev)
 	m_outputs[lev].push_back(out);
 }
 
+void Logger::addOutputMasked(ILogOutput *out, LogLevelMask mask)
+{
+	for (size_t i = 0; i < LL_MAX; i++) {
+		if (mask & LOGLEVEL_TO_MASKLEVEL(i))
+			m_outputs[i].push_back(out);
+	}
+}
+
 void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev)
 {
 	assert(lev < LL_MAX);
@@ -188,15 +196,19 @@ void Logger::addOutputMaxLevel(ILogOutput *out, LogLevel lev)
 		m_outputs[i].push_back(out);
 }
 
-void Logger::removeOutput(ILogOutput *out)
+LogLevelMask Logger::removeOutput(ILogOutput *out)
 {
+	LogLevelMask ret_mask = 0;
 	for (size_t i = 0; i < LL_MAX; i++) {
 		std::vector<ILogOutput *>::iterator it;
 
 		it = std::find(m_outputs[i].begin(), m_outputs[i].end(), out);
-		if (it != m_outputs[i].end())
+		if (it != m_outputs[i].end()) {
+			ret_mask |= LOGLEVEL_TO_MASKLEVEL(i);
 			m_outputs[i].erase(it);
+		}
 	}
+	return ret_mask;
 }
 
 void Logger::setLevelSilenced(LogLevel lev, bool silenced)
diff --git a/src/log.h b/src/log.h
index f877f2f8a1c26f9767996c5c72d04a4e67327ff5..219255d9abfd6af92f042efa32ea0df3946d56e2 100644
--- a/src/log.h
+++ b/src/log.h
@@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include <string>
 #include <fstream>
 #include "threads.h"
+#include "irrlichttypes.h"
 
 class ILogOutput;
 
@@ -38,12 +39,16 @@ enum LogLevel {
 	LL_MAX,
 };
 
+typedef u8 LogLevelMask;
+#define LOGLEVEL_TO_MASKLEVEL(x) (1 << x)
+
 class Logger {
 public:
 	void addOutput(ILogOutput *out);
 	void addOutput(ILogOutput *out, LogLevel lev);
+	void addOutputMasked(ILogOutput *out, LogLevelMask mask);
 	void addOutputMaxLevel(ILogOutput *out, LogLevel lev);
-	void removeOutput(ILogOutput *out);
+	LogLevelMask removeOutput(ILogOutput *out);
 	void setLevelSilenced(LogLevel lev, bool silenced);
 
 	void registerThread(const std::string &name);
diff --git a/src/main.cpp b/src/main.cpp
index 48b1af60313d306477169b42976fc9d8fabe2917..5046181b548ee25dc68b8154e2fe167676026188 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -45,10 +45,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "httpfetch.h"
 #include "guiEngine.h"
 #include "map.h"
+#include "player.h"
 #include "mapsector.h"
 #include "fontengine.h"
 #include "gameparams.h"
 #include "database.h"
+#include "config.h"
+#if USE_CURSES
+	#include "terminal_chat_console.h"
+#endif
 #ifndef SERVER
 #include "client/clientlauncher.h"
 #endif
@@ -277,6 +282,8 @@ static void set_allowed_options(OptionList *allowed_options)
 			_("Set gameid (\"--gameid list\" prints available ones)"))));
 	allowed_options->insert(std::make_pair("migrate", ValueSpec(VALUETYPE_STRING,
 			_("Migrate from current map backend to another (Only works when using minetestserver or with --server)"))));
+	allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
+			_("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
 #ifndef SERVER
 	allowed_options->insert(std::make_pair("videomodes", ValueSpec(VALUETYPE_FLAG,
 			_("Show available video modes"))));
@@ -816,21 +823,83 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
 	if (cmd_args.exists("migrate"))
 		return migrate_database(game_params, cmd_args);
 
-	try {
-		// Create server
-		Server server(game_params.world_path, game_params.game_spec, false,
-			bind_addr.isIPv6());
-		server.start(bind_addr);
-
-		// Run server
+	if (cmd_args.exists("terminal")) {
+#if USE_CURSES
+		bool name_ok = true;
+		std::string admin_nick = g_settings->get("name");
+
+		name_ok = name_ok && !admin_nick.empty();
+		name_ok = name_ok && string_allowed(admin_nick, PLAYERNAME_ALLOWED_CHARS);
+
+		if (!name_ok) {
+			if (admin_nick.empty()) {
+				errorstream << "No name given for admin. "
+					<< "Please check your minetest.conf that it "
+					<< "contains a 'name = ' to your main admin account."
+					<< std::endl;
+			} else {
+				errorstream << "Name for admin '"
+					<< admin_nick << "' is not valid. "
+					<< "Please check that it only contains allowed characters. "
+					<< "Valid characters are: " << PLAYERNAME_ALLOWED_CHARS_USER_EXPL
+					<< std::endl;
+			}
+			return false;
+		}
+		ChatInterface iface;
 		bool &kill = *porting::signal_handler_killstatus();
-		dedicated_server_loop(server, kill);
-	} catch (const ModError &e) {
-		errorstream << "ModError: " << e.what() << std::endl;
-		return false;
-	} catch (const ServerError &e) {
-		errorstream << "ServerError: " << e.what() << std::endl;
-		return false;
+
+		try {
+			// Create server
+			Server server(game_params.world_path,
+				game_params.game_spec, false, bind_addr.isIPv6(), &iface);
+
+			g_term_console.setup(&iface, &kill, admin_nick);
+
+			g_term_console.start();
+
+			server.start(bind_addr);
+			// Run server
+			dedicated_server_loop(server, kill);
+		} catch (const ModError &e) {
+			g_term_console.stopAndWaitforThread();
+			errorstream << "ModError: " << e.what() << std::endl;
+			return false;
+		} catch (const ServerError &e) {
+			g_term_console.stopAndWaitforThread();
+			errorstream << "ServerError: " << e.what() << std::endl;
+			return false;
+		}
+
+		// Tell the console to stop, and wait for it to finish,
+		// only then leave context and free iface
+		g_term_console.stop();
+		g_term_console.wait();
+
+		g_term_console.clearKillStatus();
+	} else {
+#else
+		errorstream << "Cmd arg --terminal passed, but "
+			<< "compiled without ncurses. Ignoring." << std::endl;
+	} {
+#endif
+		try {
+			// Create server
+			Server server(game_params.world_path, game_params.game_spec, false,
+				bind_addr.isIPv6());
+			server.start(bind_addr);
+
+			// Run server
+			bool &kill = *porting::signal_handler_killstatus();
+			dedicated_server_loop(server, kill);
+
+		} catch (const ModError &e) {
+			errorstream << "ModError: " << e.what() << std::endl;
+			return false;
+		} catch (const ServerError &e) {
+			errorstream << "ServerError: " << e.what() << std::endl;
+			return false;
+		}
 	}
 
 	return true;
diff --git a/src/network/serverpackethandler.cpp b/src/network/serverpackethandler.cpp
index d9ff564da924f03d890d4295a641f0de5f1a4dae..3c446e31d2926c973bef211ceaf399b21ebb371b 100644
--- a/src/network/serverpackethandler.cpp
+++ b/src/network/serverpackethandler.cpp
@@ -1059,69 +1059,14 @@ void Server::handleCommand_ChatMessage(NetworkPacket* pkt)
 		return;
 	}
 
-	// If something goes wrong, this player is to blame
-	RollbackScopeActor rollback_scope(m_rollback,
-			std::string("player:")+player->getName());
-
 	// Get player name of this client
-	std::wstring name = narrow_to_wide(player->getName());
-
-	// Run script hook
-	bool ate = m_script->on_chat_message(player->getName(),
-			wide_to_narrow(message));
-	// If script ate the message, don't proceed
-	if (ate)
-		return;
-
-	// Line to send to players
-	std::wstring line;
-	// Whether to send to the player that sent the line
-	bool send_to_sender_only = false;
-
-	// Commands are implemented in Lua, so only catch invalid
-	// commands that were not "eaten" and send an error back
-	if (message[0] == L'/') {
-		message = message.substr(1);
-		send_to_sender_only = true;
-		if (message.length() == 0)
-			line += L"-!- Empty command";
-		else
-			line += L"-!- Invalid command: " + str_split(message, L' ')[0];
-	}
-	else {
-		if (checkPriv(player->getName(), "shout")) {
-			line += L"<";
-			line += name;
-			line += L"> ";
-			line += message;
-		} else {
-			line += L"-!- You don't have permission to shout.";
-			send_to_sender_only = true;
-		}
-	}
+	std::string name = player->getName();
+	std::wstring wname = narrow_to_wide(name);
 
-	if (line != L"")
-	{
-		/*
-			Send the message to sender
-		*/
-		if (send_to_sender_only) {
-			SendChatMessage(pkt->getPeerId(), line);
-		}
-		/*
-			Send the message to others
-		*/
-		else {
-			actionstream << "CHAT: " << wide_to_narrow(line)<<std::endl;
-
-			std::vector<u16> clients = m_clients.getClientIDs();
-
-			for (std::vector<u16>::iterator i = clients.begin();
-				i != clients.end(); ++i) {
-				if (*i != pkt->getPeerId())
-					SendChatMessage(*i, line);
-			}
-		}
+	std::wstring answer_to_sender = handleChat(name, wname, message, pkt->getPeerId());
+	if (!answer_to_sender.empty()) {
+		// Send the answer to sender
+		SendChatMessage(pkt->getPeerId(), answer_to_sender);
 	}
 }
 
diff --git a/src/player.h b/src/player.h
index ec30e59d2982494f923282ded08649adba5e85d2..c112618761d50572c5cc35ca792d9077d0faa0ad 100644
--- a/src/player.h
+++ b/src/player.h
@@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define PLAYERNAME_SIZE 20
 
 #define PLAYERNAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
+#define PLAYERNAME_ALLOWED_CHARS_USER_EXPL "'a' to 'z', 'A' to 'Z', '0' to '9', '-', '_'"
 
 struct PlayerControl
 {
diff --git a/src/script/lua_api/l_server.cpp b/src/script/lua_api/l_server.cpp
index 85314a3bc319653d82da9eb471fc245e97cd2979..59d3f5c70351274947e3fe7b9c159df5b519e05e 100644
--- a/src/script/lua_api/l_server.cpp
+++ b/src/script/lua_api/l_server.cpp
@@ -45,6 +45,16 @@ int ModApiServer::l_get_server_status(lua_State *L)
 	return 1;
 }
 
+// print(text)
+int ModApiServer::l_print(lua_State *L)
+{
+	NO_MAP_LOCK_REQUIRED;
+	std::string text;
+	text = luaL_checkstring(L, 1);
+	getServer(L)->printToConsoleOnly(text);
+	return 0;
+}
+
 // chat_send_all(text)
 int ModApiServer::l_chat_send_all(lua_State *L)
 {
@@ -505,6 +515,8 @@ void ModApiServer::Initialize(lua_State *L, int top)
 	API_FCT(get_modpath);
 	API_FCT(get_modnames);
 
+	API_FCT(print);
+
 	API_FCT(chat_send_all);
 	API_FCT(chat_send_player);
 	API_FCT(show_formspec);
diff --git a/src/script/lua_api/l_server.h b/src/script/lua_api/l_server.h
index df31f325f03281f323a03e29f1bb2f0532cb23b4..06a5ddc24d23a66e229864f0d1722c770d20b314 100644
--- a/src/script/lua_api/l_server.h
+++ b/src/script/lua_api/l_server.h
@@ -46,6 +46,9 @@ class ModApiServer : public ModApiBase {
 	// the returned list is sorted alphabetically for you
 	static int l_get_modnames(lua_State *L);
 
+	// print(text)
+	static int l_print(lua_State *L);
+
 	// chat_send_all(text)
 	static int l_chat_send_all(lua_State *L);
 
diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h
index 68c24520c8edfd505cb93ce182b0f59c60be2ab8..6fac7e7eb67608014e044b66ea949b5bb8dcc79d 100644
--- a/src/script/lua_api/l_util.h
+++ b/src/script/lua_api/l_util.h
@@ -38,7 +38,7 @@ class ModApiUtil : public ModApiBase {
 	// log([level,] text)
 	// Writes a line to the logger.
 	// The one-argument version logs to infostream.
-	// The two-argument version accept a log level: error, action, info, or verbose.
+	// The two-argument version accepts a log level.
 	static int l_log(lua_State *L);
 
 	// get us precision time
diff --git a/src/server.cpp b/src/server.cpp
index 8c42ab5fd29d550ea04a35c72e7b21ac8c1979cb..6cb79c8754b77eab96613ad983aa2147fde8b358 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -148,7 +148,8 @@ Server::Server(
 		const std::string &path_world,
 		const SubgameSpec &gamespec,
 		bool simple_singleplayer_mode,
-		bool ipv6
+		bool ipv6,
+		ChatInterface *iface
 	):
 	m_path_world(path_world),
 	m_gamespec(gamespec),
@@ -175,6 +176,7 @@ Server::Server(
 	m_clients(&m_con),
 	m_shutdown_requested(false),
 	m_shutdown_ask_reconnect(false),
+	m_admin_chat(iface),
 	m_ignore_map_edit_events(false),
 	m_ignore_map_edit_events_peer_id(0),
 	m_next_sound_id(0)
@@ -575,6 +577,36 @@ void Server::AsyncRunStep(bool initial_step)
 			U32_MAX);
 	}
 
+	/*
+		Listen to the admin chat, if available
+	*/
+	if (m_admin_chat) {
+		if (!m_admin_chat->command_queue.empty()) {
+			MutexAutoLock lock(m_env_mutex);
+			while (!m_admin_chat->command_queue.empty()) {
+				ChatEvent *evt = m_admin_chat->command_queue.pop_frontNoEx();
+				if (evt->type == CET_NICK_ADD) {
+					// The terminal informed us of its nick choice
+					m_admin_nick = ((ChatEventNick *)evt)->nick;
+					if (!m_script->getAuth(m_admin_nick, NULL, NULL)) {
+						errorstream << "You haven't set up an account." << std::endl
+							<< "Please log in using the client as '"
+							<< m_admin_nick << "' with a secure password." << std::endl
+							<< "Until then, you can't execute admin tasks via the console," << std::endl
+							<< "and everybody can claim the user account instead of you," << std::endl
+							<< "giving them full control over this server." << std::endl;
+					}
+				} else {
+					assert(evt->type == CET_CHAT);
+					handleAdminChat((ChatEventChat *)evt);
+				}
+				delete evt;
+			}
+		}
+		m_admin_chat->outgoing_queue.push_back(
+			new ChatEventTimeInfo(m_env->getGameTime(), m_env->getTimeOfDay()));
+	}
+
 	/*
 		Do background stuff
 	*/
@@ -1100,16 +1132,19 @@ PlayerSAO* Server::StageTwoClientInit(u16 peer_id)
 
 		// Send information about joining in chat
 		{
-			std::wstring name = L"unknown";
+			std::string name = "unknown";
 			Player *player = m_env->getPlayer(peer_id);
 			if(player != NULL)
-				name = narrow_to_wide(player->getName());
+				name = player->getName();
 
 			std::wstring message;
 			message += L"*** ";
-			message += name;
+			message += narrow_to_wide(name);
 			message += L" joined the game.";
 			SendChatMessage(PEER_ID_INEXISTENT,message);
+			if (m_admin_chat)
+				m_admin_chat->outgoing_queue.push_back(
+					new ChatEventNick(CET_NICK_ADD, name));
 		}
 	}
 	Address addr = getPeerAddress(player->peer_id);
@@ -1432,6 +1467,16 @@ void Server::handlePeerChanges()
 	}
 }
 
+void Server::printToConsoleOnly(const std::string &text)
+{
+	if (m_admin_chat) {
+		m_admin_chat->outgoing_queue.push_back(
+			new ChatEventChat("", utf8_to_wide(text)));
+	} else {
+		std::cout << text;
+	}
+}
+
 void Server::Send(NetworkPacket* pkt)
 {
 	m_clients.send(pkt->getPeerId(),
@@ -2665,9 +2710,13 @@ void Server::DeleteClient(u16 peer_id, ClientDeletionReason reason)
 					os << player->getName() << " ";
 				}
 
-				actionstream << player->getName() << " "
+				std::string name = player->getName();
+				actionstream << name << " "
 						<< (reason == CDR_TIMEOUT ? "times out." : "leaves game.")
 						<< " List of players: " << os.str() << std::endl;
+				if (m_admin_chat)
+					m_admin_chat->outgoing_queue.push_back(
+						new ChatEventNick(CET_NICK_REMOVE, name));
 			}
 		}
 		{
@@ -2700,6 +2749,77 @@ void Server::UpdateCrafting(Player* player)
 	plist->changeItem(0, preview);
 }
 
+std::wstring Server::handleChat(const std::string &name, const std::wstring &wname,
+	const std::wstring &wmessage, u16 peer_id_to_avoid_sending)
+{
+	// If something goes wrong, this player is to blame
+	RollbackScopeActor rollback_scope(m_rollback,
+		std::string("player:") + name);
+
+	// Line to send
+	std::wstring line;
+	// Whether to send line to the player that sent the message, or to all players
+	bool broadcast_line = true;
+
+	// Run script hook
+	bool ate = m_script->on_chat_message(name,
+		wide_to_utf8(wmessage));
+	// If script ate the message, don't proceed
+	if (ate)
+		return L"";
+
+	// Commands are implemented in Lua, so only catch invalid
+	// commands that were not "eaten" and send an error back
+	if (wmessage[0] == L'/') {
+		std::wstring wcmd = wmessage.substr(1);
+		broadcast_line = false;
+		if (wcmd.length() == 0)
+			line += L"-!- Empty command";
+		else
+			line += L"-!- Invalid command: " + str_split(wcmd, L' ')[0];
+	} else {
+		line += L"<";
+		line += wname;
+		line += L"> ";
+		line += wmessage;
+	}
+
+	/*
+		Tell calling method to send the message to sender
+	*/
+	if (!broadcast_line) {
+		return line;
+	} else {
+		/*
+			Send the message to others
+		*/
+		actionstream << "CHAT: " << wide_to_narrow(line) << std::endl;
+
+		std::vector<u16> clients = m_clients.getClientIDs();
+
+		for (u16 i = 0; i < clients.size(); i++) {
+			u16 cid = clients[i];
+			if (cid != peer_id_to_avoid_sending)
+				SendChatMessage(cid, line);
+		}
+	}
+	return L"";
+}
+
+void Server::handleAdminChat(const ChatEventChat *evt)
+{
+	std::string name = evt->nick;
+	std::wstring wname = utf8_to_wide(name);
+	std::wstring wmessage = evt->evt_msg;
+
+	std::wstring answer = handleChat(name, wname, wmessage);
+
+	// If asked to send answer to sender
+	if (!answer.empty()) {
+		m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", answer));
+	}
+}
+
 RemoteClient* Server::getClient(u16 peer_id, ClientState state_min)
 {
 	RemoteClient *client = getClientNoEx(peer_id,state_min);
@@ -2831,9 +2951,14 @@ void Server::notifyPlayer(const char *name, const std::wstring &msg)
 	if (!m_env)
 		return;
 
+	if (m_admin_nick == name && !m_admin_nick.empty()) {
+		m_admin_chat->outgoing_queue.push_back(new ChatEventChat("", msg));
+	}
+
 	Player *player = m_env->getPlayer(name);
-	if (!player)
+	if (!player) {
 		return;
+	}
 
 	if (player->peer_id == PEER_ID_INEXISTENT)
 		return;
diff --git a/src/server.h b/src/server.h
index bee978de2505b9ec95d772853226c08962643191..6d66c938602e20b476c57656cbd54a8fc9c46978 100644
--- a/src/server.h
+++ b/src/server.h
@@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "util/numeric.h"
 #include "util/thread.h"
 #include "environment.h"
+#include "chat_interface.h"
 #include "clientiface.h"
 #include "network/networkpacket.h"
 #include <string>
@@ -171,7 +172,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 		const std::string &path_world,
 		const SubgameSpec &gamespec,
 		bool simple_singleplayer_mode,
-		bool ipv6
+		bool ipv6,
+		ChatInterface *iface = NULL
 	);
 	~Server();
 	void start(Address bind_addr);
@@ -369,6 +371,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 			u8* ser_vers, u16* prot_vers, u8* major, u8* minor, u8* patch,
 			std::string* vers_string);
 
+	void printToConsoleOnly(const std::string &text);
+
 	void SendPlayerHPOrDie(PlayerSAO *player);
 	void SendPlayerBreath(u16 peer_id);
 	void SendInventory(PlayerSAO* playerSAO);
@@ -472,6 +476,12 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	void DeleteClient(u16 peer_id, ClientDeletionReason reason);
 	void UpdateCrafting(Player *player);
 
+	// This returns the answer to the sender of wmessage, or "" if there is none
+	std::wstring handleChat(const std::string &name, const std::wstring &wname,
+		const std::wstring &wmessage,
+		u16 peer_id_to_avoid_sending = PEER_ID_INEXISTENT);
+	void handleAdminChat(const ChatEventChat *evt);
+
 	v3f findSpawnPos();
 
 	// When called, connection mutex should be locked
@@ -597,6 +607,9 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	std::string m_shutdown_msg;
 	bool m_shutdown_ask_reconnect;
 
+	ChatInterface *m_admin_chat;
+	std::string m_admin_nick;
+
 	/*
 		Map edit event queue. Automatically receives all map edits.
 		The constructor of this class registers us to receive them through
diff --git a/src/terminal_chat_console.cpp b/src/terminal_chat_console.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ac06285eb106478d4386e068ede30e6fec1d0bd9
--- /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
diff --git a/src/terminal_chat_console.h b/src/terminal_chat_console.h
new file mode 100644
index 0000000000000000000000000000000000000000..2111b7ecba31434bc338c7b02caaf07f03cdacd6
--- /dev/null
+++ b/src/terminal_chat_console.h
@@ -0,0 +1,131 @@
+/*
+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.
+*/
+
+#ifndef TERMINAL_CHAT_CONSOLE_H
+#define TERMINAL_CHAT_CONSOLE_H
+
+#include "chat.h"
+#include "threading/thread.h"
+#include "chat_interface.h"
+#include "log.h"
+
+#include <sstream>
+
+class TermLogOutput : public ILogOutput {
+public:
+
+	void logRaw(LogLevel lev, const std::string &line)
+	{
+		queue.push_back(std::make_pair(lev, line));
+	}
+
+	virtual void log(LogLevel lev, const std::string &combined,
+		const std::string &time, const std::string &thread_name,
+		const std::string &payload_text)
+	{
+		std::ostringstream os(std::ios_base::binary);
+		os << time << ": [" << thread_name << "] " << payload_text;
+
+		queue.push_back(std::make_pair(lev, os.str()));
+	}
+
+	MutexedQueue<std::pair<LogLevel, std::string> > queue;
+};
+
+class TerminalChatConsole : public Thread {
+public:
+
+	TerminalChatConsole() :
+		Thread("TerminalThread"),
+		m_log_level(LL_ACTION),
+		m_utf8_bytes_to_wait(0),
+		m_kill_requested(NULL),
+		m_esc_mode(false),
+		m_game_time(0),
+		m_time_of_day(0)
+	{}
+
+	void setup(
+		ChatInterface *iface,
+		bool *kill_requested,
+		const std::string &nick)
+	{
+		m_nick = nick;
+		m_kill_requested = kill_requested;
+		m_chat_interface = iface;
+	}
+
+	virtual void *run();
+
+	// Highly required!
+	void clearKillStatus() { m_kill_requested = NULL; }
+
+	void stopAndWaitforThread();
+
+private:
+	// these have stupid names so that nobody missclassifies them
+	// as curses functions. Oh, curses has stupid names too?
+	// Well, at least it was worth a try...
+	void initOfCurses();
+	void deInitOfCurses();
+
+	void draw_text();
+
+	void typeChatMessage(const std::wstring &m);
+
+	void handleInput(int ch, bool &complete_redraw_needed);
+
+	void step(int ch);
+
+	// Used to ensure the deinitialisation is always called.
+	struct CursesInitHelper {
+		TerminalChatConsole *cons;
+		CursesInitHelper(TerminalChatConsole * a_console)
+			: cons(a_console)
+		{ cons->initOfCurses(); }
+		~CursesInitHelper() { cons->deInitOfCurses(); }
+	};
+
+	int m_log_level;
+	std::string m_nick;
+
+	u8 m_utf8_bytes_to_wait;
+	std::string m_pending_utf8_bytes;
+
+	std::list<std::string> m_nicks;
+
+	int m_cols;
+	int m_rows;
+	bool m_can_draw_text;
+
+	bool *m_kill_requested;
+	ChatBackend m_chat_backend;
+	ChatInterface *m_chat_interface;
+
+	TermLogOutput m_log_output;
+
+	bool m_esc_mode;
+
+	u64 m_game_time;
+	u32 m_time_of_day;
+};
+
+extern TerminalChatConsole g_term_console;
+
+#endif
diff --git a/src/unittest/test_utilities.cpp b/src/unittest/test_utilities.cpp
index 3c000e76052627fe7488f0b665c9b25fe80ffb6f..1785997deb79fbd9390e8ae1026a28f24a5ac42d 100644
--- a/src/unittest/test_utilities.cpp
+++ b/src/unittest/test_utilities.cpp
@@ -43,6 +43,7 @@ class TestUtilities : public TestBase {
 	void testStrToIntConversion();
 	void testStringReplace();
 	void testStringAllowed();
+	void testAsciiPrintableHelper();
 	void testUTF8();
 	void testWrapRows();
 	void testIsNumber();
@@ -68,6 +69,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
 	TEST(testStrToIntConversion);
 	TEST(testStringReplace);
 	TEST(testStringAllowed);
+	TEST(testAsciiPrintableHelper);
 	TEST(testUTF8);
 	TEST(testWrapRows);
 	TEST(testIsNumber);
@@ -232,6 +234,18 @@ void TestUtilities::testStringAllowed()
 	UASSERT(string_allowed_blacklist("hello123", "123") == false);
 }
 
+void TestUtilities::testAsciiPrintableHelper()
+{
+	UASSERT(IS_ASCII_PRINTABLE_CHAR('e') == true);
+	UASSERT(IS_ASCII_PRINTABLE_CHAR('\0') == false);
+
+	// Ensures that there is no cutting off going on...
+	// If there were, 331 would be cut to 75 in this example
+	// and 73 is a valid ASCII char.
+	int ch = 331;
+	UASSERT(IS_ASCII_PRINTABLE_CHAR(ch) == false);
+}
+
 void TestUtilities::testUTF8()
 {
 	UASSERT(wide_to_utf8(utf8_to_wide("")) == "");
diff --git a/src/util/string.h b/src/util/string.h
index 793baad0ea729f90c3731ea741bca26f2ca3fccf..c8f60b8023119ed9081c757513b593f9d2dab39d 100644
--- a/src/util/string.h
+++ b/src/util/string.h
@@ -32,8 +32,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #define STRINGIFY(x) #x
 #define TOSTRING(x) STRINGIFY(x)
 
+// Checks whether a value is an ASCII printable character
+#define IS_ASCII_PRINTABLE_CHAR(x)   \
+	(((unsigned int)(x) >= 0x20) &&  \
+	( (unsigned int)(x) <= 0x7e))
+
 // Checks whether a byte is an inner byte for an utf-8 multibyte sequence
-#define IS_UTF8_MULTB_INNER(x) (((unsigned char)x >= 0x80) && ((unsigned char)x < 0xc0))
+#define IS_UTF8_MULTB_INNER(x)       \
+	(((unsigned char)(x) >= 0x80) && \
+	( (unsigned char)(x) <= 0xbf))
+
+// Checks whether a byte is a start byte for an utf-8 multibyte sequence
+#define IS_UTF8_MULTB_START(x)       \
+	(((unsigned char)(x) >= 0xc2) && \
+	( (unsigned char)(x) <= 0xf4))
+
+// Given a start byte x for an utf-8 multibyte sequence
+// it gives the length of the whole sequence in bytes.
+#define UTF8_MULTB_START_LEN(x)            \
+	(((unsigned char)(x) < 0xe0) ? 2 :     \
+	(((unsigned char)(x) < 0xf0) ? 3 : 4))
 
 typedef std::map<std::string, std::string> StringMap;