home · contact · privacy
Refactor client curses code.
[plomrogue2] / rogue_chat_curses.py
index d2cc704173f33243e52ee37ab08cbc371021e30a..11fe94868c72ee8891877eec4a487c123ac41368 100755 (executable)
@@ -1,15 +1,16 @@
 #!/usr/bin/env python3
 import curses
-import queue
-import threading
-import time
 import sys
 from plomrogue.game import GameBase
 from plomrogue.parser import Parser
 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
 from plomrogue.things import ThingBase
 from plomrogue.misc import quote
-from plomrogue.errors import BrokenSocketConnection, ArgError
+from plomrogue.errors import ArgError
+from plomrogue_client.socket import ClientSocket
+from plomrogue_client.tui import msg_into_lines_of_width, CursesScreen
+
+
 
 mode_helps = {
     'play': {
@@ -133,46 +134,6 @@ mode_helps = {
     }
 }
 
-from ws4py.client import WebSocketBaseClient
-class WebSocketClient(WebSocketBaseClient):
-
-    def __init__(self, recv_handler, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        self.recv_handler = recv_handler
-        self.connect()
-
-    def received_message(self, message):
-        if message.is_text:
-            message = str(message)
-            self.recv_handler(message)
-
-    @property
-    def plom_closed(self):
-        return self.client_terminated
-
-from plomrogue.io_tcp import PlomSocket
-class PlomSocketClient(PlomSocket):
-
-    def __init__(self, recv_handler, url):
-        import socket
-        self.recv_handler = recv_handler
-        host, port = url.split(':')
-        super().__init__(socket.create_connection((host, port)))
-
-    def close(self):
-        self.socket.close()
-
-    def run(self):
-        import ssl
-        try:
-            for msg in self.recv():
-                if msg == 'NEED_SSL':
-                    self.socket = ssl.wrap_socket(self.socket)
-                    continue
-                self.recv_handler(msg)
-        except BrokenSocketConnection:
-            pass  # we assume socket will be known as dead by now
-
 def cmd_TURN(game, n):
     game.turn_complete = False
 cmd_TURN.argtypes = 'int:nonneg'
@@ -533,13 +494,12 @@ class TUI:
         self.mode_edit.available_actions = ["move", "flatten", "install",
                                             "toggle_map_mode"]
         self.mode = None
-        self.host = host
+        self.socket = ClientSocket(host, self.socket_log)
         self.game = Game()
         self.game.tui = self
         self.parser = Parser(self.game)
         self.log = []
         self.do_refresh = True
-        self.queue = queue.Queue()
         self.login_name = None
         self.map_mode = 'terrain + things'
         self.password = 'foo'
@@ -591,8 +551,6 @@ class TUI:
             for k in keys_conf:
                 self.keys[k] = keys_conf[k]
         self.show_help = False
-        self.disconnected = True
-        self.force_instant_connect = True
         self.input_lines = []
         self.fov = ''
         self.flash = False
@@ -600,58 +558,33 @@ class TUI:
         self.ascii_draw_stage = 0
         self.full_ascii_draw = ''
         self.offset = YX(0,0)
-        curses.wrapper(self.loop)
-
-    def connect(self):
+        self.screen = CursesScreen()
+        self.screen.wrap_loop(self.loop)
 
-        def handle_recv(msg):
-            if msg == 'BYE':
-                self.socket.close()
-            else:
-                self.queue.put(msg)
-
-        self.log_msg('@ attempting connect')
-        socket_client_class = PlomSocketClient
-        if self.host.startswith('ws://') or self.host.startswith('wss://'):
-            socket_client_class = WebSocketClient
-        try:
-            self.socket = socket_client_class(handle_recv, self.host)
-            self.socket_thread = threading.Thread(target=self.socket.run)
-            self.socket_thread.start()
-            self.disconnected = False
-            self.game.thing_types = {}
-            self.game.terrains = {}
-            self.is_admin = False
-            time.sleep(0.1)  # give potential SSL negotation some time …
-            self.socket.send('TASKS')
-            self.socket.send('TERRAINS')
-            self.socket.send('THING_TYPES')
-            self.switch_mode('login')
-        except ConnectionRefusedError:
-            self.log_msg('@ server connect failure')
-            self.disconnected = True
-            self.switch_mode('waiting_for_server')
-        self.do_refresh = True
+    def update_on_connect(self):
+        self.socket.send('TASKS')
+        self.socket.send('TERRAINS')
+        self.socket.send('THING_TYPES')
+        self.switch_mode('login')
 
     def reconnect(self):
+        import time
         self.log_msg('@ attempting reconnect')
-        self.send('QUIT')
+        self.socket.send('QUIT')
         # necessitated by some strange SSL race conditions with ws4py
         time.sleep(0.1)  # FIXME find out why exactly necessary
         self.switch_mode('waiting_for_server')
-        self.connect()
+        self.socket.connect()
+        self.update_on_connect()
 
     def send(self, msg):
-        try:
-            if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
-                raise BrokenSocketConnection
-            self.socket.send(msg)
-        except (BrokenPipeError, BrokenSocketConnection):
-            self.log_msg('@ server disconnected :(')
-            self.disconnected = True
-            self.force_instant_connect = True
+        self.socket.send(msg)
+        if self.socket.disconnected:
             self.do_refresh = True
 
+    def socket_log(self, msg):
+        self.log_msg('@ ' + msg)
+
     def log_msg(self, msg):
         self.log += [msg]
         if len(self.log) > 100:
@@ -901,19 +834,10 @@ class TUI:
             info += ')'
         return info
 
-    def loop(self, stdscr):
-        import datetime
+    def loop(self):
 
         def safe_addstr(y, x, line):
-            if y < self.size.y - 1 or x + len(line) < self.size.x:
-                stdscr.addstr(y, x, line, curses.color_pair(1))
-            else:  # workaround to <https://stackoverflow.com/q/7063128>
-                cut_i = self.size.x - x - 1
-                cut = line[:cut_i]
-                last_char = line[cut_i]
-                stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
-                stdscr.insstr(y, self.size.x - 2, ' ')
-                stdscr.addstr(y, x, cut, curses.color_pair(1))
+            self.screen.safe_addstr(y, x, line, curses.color_pair(1))
 
         def handle_input(msg):
             command, args = self.parser.parse(msg)
@@ -922,29 +846,10 @@ class TUI:
         def task_action_on(action):
             return action_tasks[action] in self.game.tasks
 
-        def msg_into_lines_of_width(msg, width):
-            chunk = ''
-            lines = []
-            x = 0
-            for i in range(len(msg)):
-                if x >= width or msg[i] == "\n":
-                    lines += [chunk]
-                    chunk = ''
-                    x = 0
-                    if msg[i] == "\n":
-                        x -= 1
-                if msg[i] != "\n":
-                    chunk += msg[i]
-                x += 1
-            lines += [chunk]
-            return lines
-
         def reset_screen_size():
-            self.size = YX(*stdscr.getmaxyx())
-            self.size = self.size - YX(self.size.y % 4, 0)
-            self.size = self.size - YX(0, self.size.x % 4)
-            self.left_window_width = min(52, int(self.size.x / 2))
-            self.right_window_width = self.size.x - self.left_window_width
+            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
 
         def recalc_input_lines():
             if not self.mode.has_input_prompt:
@@ -970,7 +875,7 @@ class TUI:
                 lines += msg_into_lines_of_width(line, self.right_window_width)
             lines.reverse()
             height_header = 2
-            max_y = self.size.y - len(self.input_lines)
+            max_y = self.screen.size.y - len(self.input_lines)
             for i in range(len(lines)):
                 if (i >= max_y - height_header):
                     break
@@ -982,12 +887,12 @@ class TUI:
             height_header = 2
             for i in range(len(lines)):
                 y = height_header + i
-                if y >= self.size.y - len(self.input_lines):
+                if y >= self.screen.size.y - len(self.input_lines):
                     break
                 safe_addstr(y, self.left_window_width, lines[i])
 
         def draw_input():
-            y = self.size.y - len(self.input_lines)
+            y = self.screen.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])
                 y += 1
@@ -1057,7 +962,7 @@ class TUI:
                 else:
                     for line in map_lines_split:
                         self.map_lines += [''.join(line)]
-                window_center = YX(int(self.size.y / 2),
+                window_center = YX(int(self.screen.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':
@@ -1070,7 +975,7 @@ 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.size.y and map_y < len(self.map_lines):
+            while term_y < self.screen.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)
                 term_y += 1
@@ -1080,7 +985,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.size.y - self.left_window_width // 2) // 2)
+            shrink_offset = max(0, (self.screen.size.y - self.left_window_width // 2) // 2)
             y = 0
             for t in players:
                 offset_y = y - shrink_offset
@@ -1090,7 +995,7 @@ class TUI:
                     name = name[:max_len - 1] + '…'
                 safe_addstr(y, 0, '@%s:%s' % (t.thing_char, name))
                 y += 1
-                if y >= self.size.y:
+                if y >= self.screen.size.y:
                     break
 
         def draw_face_popup():
@@ -1107,14 +1012,14 @@ class TUI:
                 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
 
             if hasattr(t, 'face'):
-                draw_body_part(t.face, self.size.y - 3)
+                draw_body_part(t.face, self.screen.size.y - 3)
             if hasattr(t, 'hat'):
-                draw_body_part(t.hat, self.size.y - 6)
-            safe_addstr(self.size.y - 2, start_x, '----------')
+                draw_body_part(t.hat, self.screen.size.y - 6)
+            safe_addstr(self.screen.size.y - 2, start_x, '----------')
             name = t.name[:]
             if len(name) > 7:
                 name = name[:6 - 1] + '…'
-            safe_addstr(self.size.y - 1, start_x,
+            safe_addstr(self.screen.size.y - 1, start_x,
                         '@%s:%s' % (t.thing_char, name))
 
         def draw_help():
@@ -1135,7 +1040,7 @@ class TUI:
                     content += '[%s] – %s\n' % (key, action_descriptions[action])
                 content += '\n'
             content += self.mode.list_available_modes(self)
-            for i in range(self.size.y):
+            for i in range(self.screen.size.y):
                 safe_addstr(i,
                             self.left_window_width * (not self.mode.has_input_prompt),
                             ' ' * self.left_window_width)
@@ -1143,15 +1048,15 @@ class TUI:
             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.size.y:
+                if i >= self.screen.size.y:
                     break
                 safe_addstr(i,
                             self.left_window_width * (not self.mode.has_input_prompt),
                             lines[i])
 
         def draw_screen():
-            stdscr.clear()
-            stdscr.bkgd(' ', curses.color_pair(1))
+            self.screen.stdscr.clear()
+            self.screen.stdscr.bkgd(' ', curses.color_pair(1))
             recalc_input_lines()
             if self.mode.has_input_prompt:
                 draw_input()
@@ -1259,7 +1164,6 @@ class TUI:
             'dance': 'DANCE',
         }
 
-        curses.curs_set(0)  # hide cursor
         curses.start_color()
         self.set_default_colors()
         curses.init_pair(1, 7, 0)
@@ -1267,39 +1171,26 @@ class TUI:
             self.log_msg('@ unfortunately, your terminal does not seem to '
                          'support re-definition of colors; you might miss out '
                          'on some color effects')
-        stdscr.timeout(10)
         reset_screen_size()
         self.explorer = YX(0, 0)
         self.input_ = ''
         store_widechar = False
         input_prompt = '> '
-        interval = datetime.timedelta(seconds=5)
-        last_ping = datetime.datetime.now() - interval
         while True:
-            if self.disconnected and self.force_instant_connect:
-                self.force_instant_connect = False
-                self.connect()
-            now = datetime.datetime.now()
-            if now - last_ping > interval:
-                if self.disconnected:
-                    self.connect()
-                else:
-                    self.send('PING')
-                last_ping = now
+            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
-            while True:
-                try:
-                    msg = self.queue.get(block=False)
-                    handle_input(msg)
-                except queue.Empty:
-                    break
+            for msg in self.socket.get_message():
+                handle_input(msg)
             try:
-                key = stdscr.getkey()
+                key = self.screen.stdscr.getkey()
                 self.do_refresh = True
             except curses.error:
                 continue
@@ -1334,7 +1225,7 @@ class TUI:
                 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(input_prompt) - 1
+                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:
@@ -1453,7 +1344,7 @@ class TUI:
                     self.send('TASK:DANCE')
                 elif key == self.keys['teleport']:
                     if self.game.player.position in self.game.portals:
-                        self.host = self.game.portals[self.game.player.position]
+                        self.socket.host = self.game.portals[self.game.player.position]
                         self.reconnect()
                     else:
                         self.flash = True