home · contact · privacy
More TUI code refactoring.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 2 Jun 2021 23:22:41 +0000 (01:22 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 2 Jun 2021 23:22:41 +0000 (01:22 +0200)
plomrogue_client/tui.py
rogue_chat_curses.py

index ec04304b0b0358b1083b62b90038ab5096dab8a3..7cfc01139c35a8cbb75aa0039640fc353b98a109 100644 (file)
@@ -3,12 +3,13 @@ import curses
 
 
 
-class CursesScreen:
+class TUI:
 
-    def wrap_loop(self, loop):
-        curses.wrapper(self.start_loop, loop)
+    def __init__(self):
+        self._log = []
+        curses.wrapper(self.run_loop)
 
-    def safe_addstr(self, y, x, line, attr=0):
+    def addstr(self, y, x, line, attr=0):
         if y < self.size.y - 1 or x + len(line) < self.size.x:
             self.stdscr.addstr(y, x, line, attr)
         else:  # workaround to <https://stackoverflow.com/q/7063128>
@@ -25,11 +26,19 @@ class CursesScreen:
         self.size = self.size - YX(self.size.y % 4, 0)
         self.size = self.size - YX(0, self.size.x % 4)
 
-    def start_loop(self, stdscr, loop):
-        self.stdscr = stdscr
+    def init_loop(self):
         curses.curs_set(0)  # hide cursor
-        stdscr.timeout(10)
-        loop()
+        self.stdscr.timeout(10)
+        self.reset_size()
+
+    def run_loop(self, stdscr):
+        self.stdscr = stdscr
+        self.init_loop()
+        while True:
+            self.loop()
+
+    def log(self, msg):
+        self._log += [msg]
 
 
 
index 11fe94868c72ee8891877eec4a487c123ac41368..f798047136b271c0be140f299da0645b33ece7e0 100755 (executable)
@@ -8,7 +8,7 @@ from plomrogue.things import ThingBase
 from plomrogue.misc import quote
 from plomrogue.errors import ArgError
 from plomrogue_client.socket import ClientSocket
-from plomrogue_client.tui import msg_into_lines_of_width, CursesScreen
+from plomrogue_client.tui import msg_into_lines_of_width, TUI
 
 
 
@@ -442,7 +442,7 @@ class Mode:
                 return True
         return False
 
-class TUI:
+class RogueChatTUI(TUI):
     mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
     mode_admin = Mode('admin')
     mode_play = Mode('play')
@@ -470,7 +470,7 @@ class TUI:
     is_admin = False
     tile_draw = False
 
-    def __init__(self, host):
+    def __init__(self, host, *args, **kwargs):
         import os
         import json
         self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
@@ -498,12 +498,10 @@ class TUI:
         self.game = Game()
         self.game.tui = self
         self.parser = Parser(self.game)
-        self.log = []
         self.do_refresh = True
         self.login_name = None
         self.map_mode = 'terrain + things'
         self.password = 'foo'
-        self.switch_mode('waiting_for_server')
         self.keys = {
             'switch_to_chat': 't',
             'switch_to_play': 'p',
@@ -558,8 +556,39 @@ class TUI:
         self.ascii_draw_stage = 0
         self.full_ascii_draw = ''
         self.offset = YX(0,0)
-        self.screen = CursesScreen()
-        self.screen.wrap_loop(self.loop)
+        self.explorer = YX(0, 0)
+        self.input_ = ''
+        self.store_widechar = False
+        self.input_prompt = '> '
+        self.action_descriptions = {
+            'move': 'move',
+            'flatten': 'flatten surroundings',
+            'teleport': 'teleport',
+            'take_thing': 'pick up thing',
+            'drop_thing': 'drop thing',
+            'toggle_map_mode': 'toggle map view',
+            'toggle_tile_draw': 'toggle protection character drawing',
+            'install': '(un-)install',
+            'wear': '(un-)wear',
+            'door': 'open/close',
+            'consume': 'consume',
+            'spin': 'spin',
+            'dance': 'dance',
+        }
+        self.action_tasks = {
+            'flatten': 'FLATTEN_SURROUNDINGS',
+            'take_thing': 'PICK_UP',
+            'drop_thing': 'DROP',
+            'door': 'DOOR',
+            'install': 'INSTALL',
+            'wear': 'WEAR',
+            'move': 'MOVE',
+            'command': 'COMMAND',
+            'consume': 'INTOXICATE',
+            'spin': 'SPIN',
+            'dance': 'DANCE',
+        }
+        super().__init__(*args, **kwargs)
 
     def update_on_connect(self):
         self.socket.send('TASKS')
@@ -569,7 +598,7 @@ class TUI:
 
     def reconnect(self):
         import time
-        self.log_msg('@ attempting reconnect')
+        self.log('@ attempting reconnect')
         self.socket.send('QUIT')
         # necessitated by some strange SSL race conditions with ws4py
         time.sleep(0.1)  # FIXME find out why exactly necessary
@@ -583,12 +612,13 @@ class TUI:
             self.do_refresh = True
 
     def socket_log(self, msg):
-        self.log_msg('@ ' + msg)
+        self.log('@ ' + msg)
 
     def log_msg(self, msg):
-        self.log += [msg]
-        if len(self.log) > 100:
-            self.log = self.log[-100:]
+        super().log(msg)
+        #self.log += [msg]
+        if len(self._log) > 100:
+            self.log = self._log[-100:]
 
     def restore_input_values(self):
         if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
@@ -630,12 +660,12 @@ class TUI:
     def switch_mode(self, mode_name):
 
         def fail(msg, return_mode='play'):
-            self.log_msg('? ' + msg)
+            self.log('? ' + msg)
             self.flash = True
             self.switch_mode(return_mode)
 
         if self.mode and self.mode.name == 'control_tile_draw':
-            self.log_msg('@ finished tile protection drawing.')
+            self.log('@ finished tile protection drawing.')
         self.draw_face = False
         self.tile_draw = False
         self.ascii_draw_stage = 0
@@ -670,14 +700,14 @@ class TUI:
         if self.mode.is_single_char_entry:
             self.show_help = True
         if len(self.mode.intro_msg) > 0:
-            self.log_msg(self.mode.intro_msg)
+            self.log(self.mode.intro_msg)
         if self.mode.name == 'login':
             if self.login_name:
                 self.send('LOGIN ' + quote(self.login_name))
             else:
-                self.log_msg('@ enter username')
+                self.log('@ enter username')
         elif self.mode.name == 'take_thing':
-            self.log_msg('Portable things in reach for pick-up:')
+            self.log('Portable things in reach for pick-up:')
             directed_moves = {
                 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
             }
@@ -711,31 +741,31 @@ class TUI:
             else:
                 for i in range(len(self.selectables)):
                     t = self.game.get_thing(self.selectables[i])
-                    self.log_msg('%s %s: %s' % (i, directions[i],
+                    self.log('%s %s: %s' % (i, directions[i],
                                                 self.get_thing_info(t)))
         elif self.mode.name == 'drop_thing':
-            self.log_msg('Direction to drop thing to:')
+            self.log('Direction to drop thing to:')
             self.selectables =\
                 ['HERE'] + list(self.game.tui.movement_keys.values())
             for i in range(len(self.selectables)):
-                self.log_msg(str(i) + ': ' + self.selectables[i])
+                self.log(str(i) + ': ' + self.selectables[i])
         elif self.mode.name == 'enter_design':
             if self.game.player.carrying.type_ == 'Hat':
-                self.log_msg('@ The design you enter must be %s lines of max %s '
+                self.log('@ The design you enter must be %s lines of max %s '
                              'characters width each'
                              % (self.game.player.carrying.design[0].y,
                                 self.game.player.carrying.design[0].x))
-                self.log_msg('@ Legal characters: ' + self.game.players_hat_chars)
-                self.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)')
+                self.log('@ Legal characters: ' + self.game.players_hat_chars)
+                self.log('@ (Eat cookies to extend the ASCII characters available for drawing.)')
             else:
-                self.log_msg('@ Width of first line determines maximum width for remaining design')
-                self.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
+                self.log('@ Width of first line determines maximum width for remaining design')
+                self.log('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
         elif self.mode.name == 'command_thing':
             self.send('TASK:COMMAND ' + quote('HELP'))
         elif self.mode.name == 'control_pw_pw':
-            self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
+            self.log('@ enter protection password for "%s":' % self.tile_control_char)
         elif self.mode.name == 'control_tile_draw':
-            self.log_msg('@ can draw protection character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
+            self.log('@ can draw protection character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
         self.input_ = ""
         self.restore_input_values()
 
@@ -834,28 +864,39 @@ class TUI:
             info += ')'
         return info
 
-    def loop(self):
+    def reset_size(self):
+        super().reset_size()
+        self.left_window_width = min(52, int(self.size.x / 2))
+        self.right_window_width = self.size.x - self.left_window_width
 
-        def safe_addstr(y, x, line):
-            self.screen.safe_addstr(y, x, line, curses.color_pair(1))
+    def addstr(self, y, x, line, ignore=None):
+        super().addstr(y, x, line, curses.color_pair(1))
+
+    def init_loop(self):
+        self.switch_mode('waiting_for_server')
+        curses.start_color()
+        self.set_default_colors()
+        curses.init_pair(1, 7, 0)
+        if not curses.can_change_color():
+            self.log('@ unfortunately, your terminal does not seem to '
+                         'support re-definition of colors; you might miss out '
+                         'on some color effects')
+        super().init_loop()
+
+    def loop(self):
 
         def handle_input(msg):
             command, args = self.parser.parse(msg)
             command(*args)
 
         def task_action_on(action):
-            return action_tasks[action] in self.game.tasks
-
-        def reset_screen_size():
-            self.screen.reset_size()
-            self.left_window_width = min(52, int(self.screen.size.x / 2))
-            self.right_window_width = self.screen.size.x - self.left_window_width
+            return self.action_tasks[action] in self.game.tasks
 
         def recalc_input_lines():
             if not self.mode.has_input_prompt:
                 self.input_lines = []
             else:
-                self.input_lines = msg_into_lines_of_width(input_prompt
+                self.input_lines = msg_into_lines_of_width(self.input_prompt
                                                            + self.input_ + '█',
                                                            self.right_window_width)
 
@@ -871,15 +912,15 @@ class TUI:
 
         def draw_history():
             lines = []
-            for line in self.log:
+            for line in self._log:
                 lines += msg_into_lines_of_width(line, self.right_window_width)
             lines.reverse()
             height_header = 2
-            max_y = self.screen.size.y - len(self.input_lines)
+            max_y = self.size.y - len(self.input_lines)
             for i in range(len(lines)):
                 if (i >= max_y - height_header):
                     break
-                safe_addstr(max_y - i - 1, self.left_window_width, lines[i])
+                self.addstr(max_y - i - 1, self.left_window_width, lines[i])
 
         def draw_info():
             info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
@@ -887,26 +928,26 @@ class TUI:
             height_header = 2
             for i in range(len(lines)):
                 y = height_header + i
-                if y >= self.screen.size.y - len(self.input_lines):
+                if y >= self.size.y - len(self.input_lines):
                     break
-                safe_addstr(y, self.left_window_width, lines[i])
+                self.addstr(y, self.left_window_width, lines[i])
 
         def draw_input():
-            y = self.screen.size.y - len(self.input_lines)
+            y = self.size.y - len(self.input_lines)
             for i in range(len(self.input_lines)):
-                safe_addstr(y, self.left_window_width, self.input_lines[i])
+                self.addstr(y, self.left_window_width, self.input_lines[i])
                 y += 1
 
         def draw_stats():
             stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
                                                 self.game.bladder_pressure)
-            safe_addstr(0, self.left_window_width, stats)
+            self.addstr(0, self.left_window_width, stats)
 
         def draw_mode():
             help = "hit [%s] for help" % self.keys['help']
             if self.mode.has_input_prompt:
                 help = "enter /help for help"
-            safe_addstr(1, self.left_window_width,
+            self.addstr(1, self.left_window_width,
                         'MODE: %s – %s' % (self.mode.short_desc, help))
 
         def draw_map():
@@ -962,7 +1003,7 @@ class TUI:
                 else:
                     for line in map_lines_split:
                         self.map_lines += [''.join(line)]
-                window_center = YX(int(self.screen.size.y / 2),
+                window_center = YX(int(self.size.y / 2),
                                    int(self.left_window_width / 2))
                 center = self.game.player.position
                 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
@@ -975,9 +1016,9 @@ class TUI:
             term_x = max(0, -self.offset.x)
             map_y = max(0, self.offset.y)
             map_x = max(0, self.offset.x)
-            while term_y < self.screen.size.y and map_y < len(self.map_lines):
+            while term_y < self.size.y and map_y < len(self.map_lines):
                 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
-                safe_addstr(term_y, term_x, to_draw)
+                self.addstr(term_y, term_x, to_draw)
                 term_y += 1
                 map_y += 1
 
@@ -985,7 +1026,7 @@ class TUI:
             players = [t for t in self.game.things if t.type_ == 'Player']
             players.sort(key=lambda t: len(t.name))
             players.reverse()
-            shrink_offset = max(0, (self.screen.size.y - self.left_window_width // 2) // 2)
+            shrink_offset = max(0, (self.size.y - self.left_window_width // 2) // 2)
             y = 0
             for t in players:
                 offset_y = y - shrink_offset
@@ -993,9 +1034,9 @@ class TUI:
                 name = t.name[:]
                 if len(name) > max_len:
                     name = name[:max_len - 1] + '…'
-                safe_addstr(y, 0, '@%s:%s' % (t.thing_char, name))
+                self.addstr(y, 0, '@%s:%s' % (t.thing_char, name))
                 y += 1
-                if y >= self.screen.size.y:
+                if y >= self.size.y:
                     break
 
         def draw_face_popup():
@@ -1006,21 +1047,20 @@ class TUI:
 
             start_x = self.left_window_width - 10
             def draw_body_part(body_part, end_y):
-                safe_addstr(end_y - 3, start_x, '----------')
-                safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
-                safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
-                safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
+                self.addstr(end_y - 3, start_x, '----------')
+                self.addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
+                self.addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
+                self.addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
 
             if hasattr(t, 'face'):
-                draw_body_part(t.face, self.screen.size.y - 3)
+                draw_body_part(t.face, self.size.y - 3)
             if hasattr(t, 'hat'):
-                draw_body_part(t.hat, self.screen.size.y - 6)
-            safe_addstr(self.screen.size.y - 2, start_x, '----------')
+                draw_body_part(t.hat, self.size.y - 6)
+            self.addstr(self.size.y - 2, start_x, '----------')
             name = t.name[:]
             if len(name) > 7:
                 name = name[:6 - 1] + '…'
-            safe_addstr(self.screen.size.y - 1, start_x,
-                        '@%s:%s' % (t.thing_char, name))
+            self.addstr(self.size.y - 1, start_x, '@%s:%s' % (t.thing_char, name))
 
         def draw_help():
             content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
@@ -1028,8 +1068,8 @@ class TUI:
             if len(self.mode.available_actions) > 0:
                 content += "Available actions:\n"
                 for action in self.mode.available_actions:
-                    if action in action_tasks:
-                        if action_tasks[action] not in self.game.tasks:
+                    if action in self.action_tasks:
+                        if self.action_tasks[action] not in self.game.tasks:
                             continue
                     if action == 'move_explorer':
                         action = 'move'
@@ -1037,26 +1077,26 @@ class TUI:
                         key = ','.join(self.movement_keys)
                     else:
                         key = self.keys[action]
-                    content += '[%s] – %s\n' % (key, action_descriptions[action])
+                    content += '[%s] – %s\n' % (key, self.action_descriptions[action])
                 content += '\n'
             content += self.mode.list_available_modes(self)
-            for i in range(self.screen.size.y):
-                safe_addstr(i,
+            for i in range(self.size.y):
+                self.addstr(i,
                             self.left_window_width * (not self.mode.has_input_prompt),
                             ' ' * self.left_window_width)
             lines = []
             for line in content.split('\n'):
                 lines += msg_into_lines_of_width(line, self.right_window_width)
             for i in range(len(lines)):
-                if i >= self.screen.size.y:
+                if i >= self.size.y:
                     break
-                safe_addstr(i,
+                self.addstr(i,
                             self.left_window_width * (not self.mode.has_input_prompt),
                             lines[i])
 
         def draw_screen():
-            self.screen.stdscr.clear()
-            self.screen.stdscr.bkgd(' ', curses.color_pair(1))
+            self.stdscr.clear()
+            self.stdscr.bkgd(' ', curses.color_pair(1))
             recalc_input_lines()
             if self.mode.has_input_prompt:
                 draw_input()
@@ -1079,11 +1119,11 @@ class TUI:
             try:
                 i = int(self.input_)
                 if i < 0 or i >= len(self.selectables):
-                    self.log_msg('? invalid index, aborted')
+                    self.log('? invalid index, aborted')
                 else:
                     self.send('TASK:%s %s' % (task_name, self.selectables[i]))
             except ValueError:
-                self.log_msg('? invalid index, aborted')
+                self.log('? invalid index, aborted')
             self.input_ = ''
             self.switch_mode('play')
 
@@ -1092,7 +1132,7 @@ class TUI:
             if with_size and self.ascii_draw_stage == 0:
                 width = len(self.input_)
                 if width > 36:
-                    self.log_msg('? input too long, must be max 36; try again')
+                    self.log('? input too long, must be max 36; try again')
                     # TODO: move max width mechanism server-side
                     return
                 old_size = self.game.player.carrying.design[0]
@@ -1101,10 +1141,10 @@ class TUI:
                     self.game.player.carrying.design[1] = ''
                     self.game.player.carrying.design[0] = YX(old_size.y, width)
             elif len(self.input_) > width:
-                self.log_msg('? input too long, '
+                self.log('? input too long, '
                              'must be max %s; try again' % width)
                 return
-            self.log_msg('  ' + self.input_)
+            self.log('  ' + self.input_)
             if with_size and self.input_ in {'', ' '}\
                and self.ascii_draw_stage > 0:
                 height = self.ascii_draw_stage
@@ -1134,253 +1174,210 @@ class TUI:
                 self.input_ = ""
                 self.switch_mode('edit')
 
-        action_descriptions = {
-            'move': 'move',
-            'flatten': 'flatten surroundings',
-            'teleport': 'teleport',
-            'take_thing': 'pick up thing',
-            'drop_thing': 'drop thing',
-            'toggle_map_mode': 'toggle map view',
-            'toggle_tile_draw': 'toggle protection character drawing',
-            'install': '(un-)install',
-            'wear': '(un-)wear',
-            'door': 'open/close',
-            'consume': 'consume',
-            'spin': 'spin',
-            'dance': 'dance',
-        }
-
-        action_tasks = {
-            'flatten': 'FLATTEN_SURROUNDINGS',
-            'take_thing': 'PICK_UP',
-            'drop_thing': 'DROP',
-            'door': 'DOOR',
-            'install': 'INSTALL',
-            'wear': 'WEAR',
-            'move': 'MOVE',
-            'command': 'COMMAND',
-            'consume': 'INTOXICATE',
-            'spin': 'SPIN',
-            'dance': 'DANCE',
-        }
-
-        curses.start_color()
-        self.set_default_colors()
-        curses.init_pair(1, 7, 0)
-        if not curses.can_change_color():
-            self.log_msg('@ unfortunately, your terminal does not seem to '
-                         'support re-definition of colors; you might miss out '
-                         'on some color effects')
-        reset_screen_size()
-        self.explorer = YX(0, 0)
-        self.input_ = ''
-        store_widechar = False
-        input_prompt = '> '
-        while True:
-            prev_disconnected = self.socket.disconnected
-            self.socket.keep_connection_alive()
-            if prev_disconnected and not self.socket.disconnected:
-                self.update_on_connect()
-            if self.flash:
-                curses.flash()
-                self.flash = False
-            if self.do_refresh:
-                draw_screen()
-                self.do_refresh = False
-            for msg in self.socket.get_message():
-                handle_input(msg)
-            try:
-                key = self.screen.stdscr.getkey()
-                self.do_refresh = True
-            except curses.error:
-                continue
-            keycode = None
-            if len(key) == 1:
-                keycode = ord(key)
-                # workaround for <https://stackoverflow.com/a/56390915>
-                if store_widechar:
-                    store_widechar = False
-                    key = bytes([195, keycode]).decode()
-                if keycode == 195:
-                    store_widechar = True
-                    continue
-            self.show_help = False
-            self.draw_face = False
-            if key == 'KEY_RESIZE':
-                reset_screen_size()
-            elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
-                self.input_ = self.input_[:-1]
-            elif (((not self.mode.is_intro) and keycode == 27)  # Escape
-                  or (self.mode.has_input_prompt and key == '\n'
-                      and self.input_ == ''\
-                      and self.mode.name in {'chat', 'command_thing',
-                                             'take_thing', 'drop_thing',
-                                             'admin_enter'})):
-                if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
-                    self.log_msg('@ aborted')
-                self.switch_mode('play')
-            elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
-                self.show_help = True
-                self.input_ = ""
-                self.restore_input_values()
-            elif self.mode.has_input_prompt and key != '\n':  # Return key
-                self.input_ += key
-                max_length = self.right_window_width * self.screen.size.y - len(input_prompt) - 1
-                if len(self.input_) > max_length:
-                    self.input_ = self.input_[:max_length]
-            elif key == self.keys['help'] and not self.mode.is_single_char_entry:
-                self.show_help = True
-            elif self.mode.name == 'login' and key == '\n':
-                self.login_name = self.input_
-                self.send('LOGIN ' + quote(self.input_))
-                self.input_ = ""
-            elif self.mode.name == 'enter_face' and key == '\n':
-                enter_ascii_art('PLAYER_FACE', 3, 6)
-            elif self.mode.name == 'enter_design' and key == '\n':
-                if self.game.player.carrying.type_ == 'Hat':
-                    enter_ascii_art('THING_DESIGN',
-                                    self.game.player.carrying.design[0].y,
-                                    self.game.player.carrying.design[0].x, True)
-                else:
-                    enter_ascii_art('THING_DESIGN',
-                                    self.game.player.carrying.design[0].y,
-                                    self.game.player.carrying.design[0].x,
-                                    True, True)
-            elif self.mode.name == 'take_thing' and key == '\n':
-                pick_selectable('PICK_UP')
-            elif self.mode.name == 'drop_thing' and key == '\n':
-                pick_selectable('DROP')
-            elif self.mode.name == 'command_thing' and key == '\n':
-                self.send('TASK:COMMAND ' + quote(self.input_))
-                self.input_ = ""
-            elif self.mode.name == 'control_pw_pw' and key == '\n':
-                if self.input_ == '':
-                    self.log_msg('@ aborted')
-                else:
-                    self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
-                    self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
+        prev_disconnected = self.socket.disconnected
+        self.socket.keep_connection_alive()
+        if prev_disconnected and not self.socket.disconnected:
+            self.update_on_connect()
+        if self.flash:
+            curses.flash()
+            self.flash = False
+        if self.do_refresh:
+            draw_screen()
+            self.do_refresh = False
+        for msg in self.socket.get_message():
+            handle_input(msg)
+        try:
+            key = self.stdscr.getkey()
+            self.do_refresh = True
+        except curses.error:
+            return
+        keycode = None
+        if len(key) == 1:
+            keycode = ord(key)
+            # workaround for <https://stackoverflow.com/a/56390915>
+            if self.store_widechar:
+                self.store_widechar = False
+                key = bytes([195, keycode]).decode()
+            if keycode == 195:
+                self.store_widechar = True
+                return
+        self.show_help = False
+        self.draw_face = False
+        if key == 'KEY_RESIZE':
+            self.reset_size()
+        elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
+            self.input_ = self.input_[:-1]
+        elif (((not self.mode.is_intro) and keycode == 27)  # Escape
+              or (self.mode.has_input_prompt and key == '\n'
+                  and self.input_ == ''\
+                  and self.mode.name in {'chat', 'command_thing',
+                                         'take_thing', 'drop_thing',
+                                         'admin_enter'})):
+            if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
+                self.log('@ aborted')
+            self.switch_mode('play')
+        elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
+            self.show_help = True
+            self.input_ = ""
+            self.restore_input_values()
+        elif self.mode.has_input_prompt and key != '\n':  # Return key
+            self.input_ += key
+            max_length = self.right_window_width * self.size.y - len(self.input_prompt) - 1
+            if len(self.input_) > max_length:
+                self.input_ = self.input_[:max_length]
+        elif key == self.keys['help'] and not self.mode.is_single_char_entry:
+            self.show_help = True
+        elif self.mode.name == 'login' and key == '\n':
+            self.login_name = self.input_
+            self.send('LOGIN ' + quote(self.input_))
+            self.input_ = ""
+        elif self.mode.name == 'enter_face' and key == '\n':
+            enter_ascii_art('PLAYER_FACE', 3, 6)
+        elif self.mode.name == 'enter_design' and key == '\n':
+            if self.game.player.carrying.type_ == 'Hat':
+                enter_ascii_art('THING_DESIGN',
+                                self.game.player.carrying.design[0].y,
+                                self.game.player.carrying.design[0].x, True)
+            else:
+                enter_ascii_art('THING_DESIGN',
+                                self.game.player.carrying.design[0].y,
+                                self.game.player.carrying.design[0].x,
+                                True, True)
+        elif self.mode.name == 'take_thing' and key == '\n':
+            pick_selectable('PICK_UP')
+        elif self.mode.name == 'drop_thing' and key == '\n':
+            pick_selectable('DROP')
+        elif self.mode.name == 'command_thing' and key == '\n':
+            self.send('TASK:COMMAND ' + quote(self.input_))
+            self.input_ = ""
+        elif self.mode.name == 'control_pw_pw' and key == '\n':
+            if self.input_ == '':
+                self.log('@ aborted')
+            else:
+                self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
+                self.log('@ sent new password for protection character "%s"' % self.tile_control_char)
+            self.switch_mode('admin')
+        elif self.mode.name == 'password' and key == '\n':
+            if self.input_ == '':
+                self.input_ = ' '
+            self.password = self.input_
+            self.switch_mode('edit')
+        elif self.mode.name == 'admin_enter' and key == '\n':
+            self.send('BECOME_ADMIN ' + quote(self.input_))
+            self.switch_mode('play')
+        elif self.mode.name == 'control_pw_type' and key == '\n':
+            if len(self.input_) != 1:
+                self.log('@ entered non-single-char, therefore aborted')
                 self.switch_mode('admin')
-            elif self.mode.name == 'password' and key == '\n':
-                if self.input_ == '':
-                    self.input_ = ' '
-                self.password = self.input_
-                self.switch_mode('edit')
-            elif self.mode.name == 'admin_enter' and key == '\n':
-                self.send('BECOME_ADMIN ' + quote(self.input_))
-                self.switch_mode('play')
-            elif self.mode.name == 'control_pw_type' and key == '\n':
-                if len(self.input_) != 1:
-                    self.log_msg('@ entered non-single-char, therefore aborted')
-                    self.switch_mode('admin')
-                else:
-                    self.tile_control_char = self.input_
-                    self.switch_mode('control_pw_pw')
-            elif self.mode.name == 'admin_thing_protect' and key == '\n':
-                if len(self.input_) != 1:
-                    self.log_msg('@ entered non-single-char, therefore aborted')
-                else:
-                    self.send('THING_PROTECTION %s' % (quote(self.input_)))
-                    self.log_msg('@ sent new protection character for thing')
+            else:
+                self.tile_control_char = self.input_
+                self.switch_mode('control_pw_pw')
+        elif self.mode.name == 'admin_thing_protect' and key == '\n':
+            if len(self.input_) != 1:
+                self.log('@ entered non-single-char, therefore aborted')
+            else:
+                self.send('THING_PROTECTION %s' % (quote(self.input_)))
+                self.log('@ sent new protection character for thing')
+            self.switch_mode('admin')
+        elif self.mode.name == 'control_tile_type' and key == '\n':
+            if len(self.input_) != 1:
+                self.log('@ entered non-single-char, therefore aborted')
                 self.switch_mode('admin')
-            elif self.mode.name == 'control_tile_type' and key == '\n':
-                if len(self.input_) != 1:
-                    self.log_msg('@ entered non-single-char, therefore aborted')
-                    self.switch_mode('admin')
-                else:
-                    self.tile_control_char = self.input_
-                    self.switch_mode('control_tile_draw')
-            elif self.mode.name == 'chat' and key == '\n':
-                if self.input_ == '':
-                    continue
-                if self.input_[0] == '/':
-                    if self.input_.startswith('/nick'):
-                        tokens = self.input_.split(maxsplit=1)
-                        if len(tokens) == 2:
-                            self.send('NICK ' + quote(tokens[1]))
-                        else:
-                            self.log_msg('? need login name')
+            else:
+                self.tile_control_char = self.input_
+                self.switch_mode('control_tile_draw')
+        elif self.mode.name == 'chat' and key == '\n':
+            if self.input_ == '':
+                return
+            if self.input_[0] == '/':
+                if self.input_.startswith('/nick'):
+                    tokens = self.input_.split(maxsplit=1)
+                    if len(tokens) == 2:
+                        self.send('NICK ' + quote(tokens[1]))
                     else:
-                        self.log_msg('? unknown command')
+                        self.log('? need login name')
                 else:
-                    self.send('ALL ' + quote(self.input_))
-                self.input_ = ""
-            elif self.mode.name == 'name_thing' and key == '\n':
-                if self.input_ == '':
-                    self.input_ = ' '
-                self.send('THING_NAME %s %s' % (quote(self.input_),
-                                                quote(self.password)))
-                self.switch_mode('edit')
-            elif self.mode.name == 'annotate' and key == '\n':
-                if self.input_ == '':
-                    self.input_ = ' '
-                self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
-                                                 quote(self.password)))
-                self.switch_mode('edit')
-            elif self.mode.name == 'portal' and key == '\n':
-                if self.input_ == '':
-                    self.input_ = ' '
-                self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
-                                               quote(self.password)))
-                self.switch_mode('edit')
-            elif self.mode.name == 'study':
-                if self.mode.mode_switch_on_key(self, key):
-                    continue
-                elif key == self.keys['toggle_map_mode']:
-                    self.toggle_map_mode()
-                elif key in self.movement_keys:
-                    move_explorer(self.movement_keys[key])
-            elif self.mode.name == 'play':
-                if self.mode.mode_switch_on_key(self, key):
-                    continue
-                elif key == self.keys['door'] and task_action_on('door'):
-                    self.send('TASK:DOOR')
-                elif key == self.keys['consume'] and task_action_on('consume'):
-                    self.send('TASK:INTOXICATE')
-                elif key == self.keys['wear'] and task_action_on('wear'):
-                    self.send('TASK:WEAR')
-                elif key == self.keys['spin'] and task_action_on('spin'):
-                    self.send('TASK:SPIN')
-                elif key == self.keys['dance'] and task_action_on('dance'):
-                    self.send('TASK:DANCE')
-                elif key == self.keys['teleport']:
-                    if self.game.player.position in self.game.portals:
-                        self.socket.host = self.game.portals[self.game.player.position]
-                        self.reconnect()
-                    else:
-                        self.flash = True
-                        self.log_msg('? not standing on portal')
-                elif key in self.movement_keys and task_action_on('move'):
-                    self.send('TASK:MOVE ' + self.movement_keys[key])
-            elif self.mode.name == 'write':
-                self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
-                self.switch_mode('edit')
-            elif self.mode.name == 'control_tile_draw':
-                if self.mode.mode_switch_on_key(self, key):
-                    continue
-                elif key in self.movement_keys:
-                    move_explorer(self.movement_keys[key])
-                elif key == self.keys['toggle_tile_draw']:
-                    self.tile_draw = False if self.tile_draw else True
-            elif self.mode.name == 'admin':
-                if self.mode.mode_switch_on_key(self, key):
-                    continue
-                elif key == self.keys['toggle_map_mode']:
-                    self.toggle_map_mode()
-                elif key in self.movement_keys and task_action_on('move'):
-                    self.send('TASK:MOVE ' + self.movement_keys[key])
-            elif self.mode.name == 'edit':
-                if self.mode.mode_switch_on_key(self, key):
-                    continue
-                elif key == self.keys['flatten'] and task_action_on('flatten'):
-                    self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
-                elif key == self.keys['install'] and task_action_on('install'):
-                    self.send('TASK:INSTALL %s' % quote(self.password))
-                elif key == self.keys['toggle_map_mode']:
-                    self.toggle_map_mode()
-                elif key in self.movement_keys and task_action_on('move'):
-                    self.send('TASK:MOVE ' + self.movement_keys[key])
+                    self.log('? unknown command')
+            else:
+                self.send('ALL ' + quote(self.input_))
+            self.input_ = ""
+        elif self.mode.name == 'name_thing' and key == '\n':
+            if self.input_ == '':
+                self.input_ = ' '
+            self.send('THING_NAME %s %s' % (quote(self.input_),
+                                            quote(self.password)))
+            self.switch_mode('edit')
+        elif self.mode.name == 'annotate' and key == '\n':
+            if self.input_ == '':
+                self.input_ = ' '
+            self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
+                                             quote(self.password)))
+            self.switch_mode('edit')
+        elif self.mode.name == 'portal' and key == '\n':
+            if self.input_ == '':
+                self.input_ = ' '
+            self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
+                                           quote(self.password)))
+            self.switch_mode('edit')
+        elif self.mode.name == 'study':
+            if self.mode.mode_switch_on_key(self, key):
+                return
+            elif key == self.keys['toggle_map_mode']:
+                self.toggle_map_mode()
+            elif key in self.movement_keys:
+                move_explorer(self.movement_keys[key])
+        elif self.mode.name == 'play':
+            if self.mode.mode_switch_on_key(self, key):
+                return
+            elif key == self.keys['door'] and task_action_on('door'):
+                self.send('TASK:DOOR')
+            elif key == self.keys['consume'] and task_action_on('consume'):
+                self.send('TASK:INTOXICATE')
+            elif key == self.keys['wear'] and task_action_on('wear'):
+                self.send('TASK:WEAR')
+            elif key == self.keys['spin'] and task_action_on('spin'):
+                self.send('TASK:SPIN')
+            elif key == self.keys['dance'] and task_action_on('dance'):
+                self.send('TASK:DANCE')
+            elif key == self.keys['teleport']:
+                if self.game.player.position in self.game.portals:
+                    self.socket.host = self.game.portals[self.game.player.position]
+                    self.reconnect()
+                else:
+                    self.flash = True
+                    self.log('? not standing on portal')
+            elif key in self.movement_keys and task_action_on('move'):
+                self.send('TASK:MOVE ' + self.movement_keys[key])
+        elif self.mode.name == 'write':
+            self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
+            self.switch_mode('edit')
+        elif self.mode.name == 'control_tile_draw':
+            if self.mode.mode_switch_on_key(self, key):
+                return
+            elif key in self.movement_keys:
+                move_explorer(self.movement_keys[key])
+            elif key == self.keys['toggle_tile_draw']:
+                self.tile_draw = False if self.tile_draw else True
+        elif self.mode.name == 'admin':
+            if self.mode.mode_switch_on_key(self, key):
+                return
+            elif key == self.keys['toggle_map_mode']:
+                self.toggle_map_mode()
+            elif key in self.movement_keys and task_action_on('move'):
+                self.send('TASK:MOVE ' + self.movement_keys[key])
+        elif self.mode.name == 'edit':
+            if self.mode.mode_switch_on_key(self, key):
+                return
+            elif key == self.keys['flatten'] and task_action_on('flatten'):
+                self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
+            elif key == self.keys['install'] and task_action_on('install'):
+                self.send('TASK:INSTALL %s' % quote(self.password))
+            elif key == self.keys['toggle_map_mode']:
+                self.toggle_map_mode()
+            elif key in self.movement_keys and task_action_on('move'):
+                self.send('TASK:MOVE ' + self.movement_keys[key])
 
 if len(sys.argv) != 2:
     raise ArgError('wrong number of arguments, need game host')
 host = sys.argv[1]
-TUI(host)
+RogueChatTUI(host)