home · contact · privacy
Refactor client curses code.
[plomrogue2] / rogue_chat_curses.py
index 9db7b445b8040bd92166d554d4d3f02b32cbe8b7..11fe94868c72ee8891877eec4a487c123ac41368 100755 (executable)
@@ -1,15 +1,16 @@
 #!/usr/bin/env python3
 import curses
 #!/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
 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': {
 
 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'
 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.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.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'
         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
             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
         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)
         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):
 
     def reconnect(self):
+        import time
         self.log_msg('@ attempting reconnect')
         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')
         # 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):
 
     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
 
             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:
     def log_msg(self, msg):
         self.log += [msg]
         if len(self.log) > 100:
@@ -901,19 +834,10 @@ class TUI:
             info += ')'
         return info
 
             info += ')'
         return info
 
-    def loop(self, stdscr):
-        import datetime
+    def loop(self):
 
         def safe_addstr(y, x, line):
 
         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)
 
         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 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():
         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:
 
         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
                 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
             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
             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():
                     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
             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)]
                 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':
                                    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)
             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
                 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,17 +985,17 @@ class TUI:
             players = [t for t in self.game.things if t.type_ == 'Player']
             players.sort(key=lambda t: len(t.name))
             players.reverse()
             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
             y = 0
             for t in players:
                 offset_y = y - shrink_offset
-                max_len = max(4, (self.left_window_width // 2) - (offset_y * 2) - 8)
+                max_len = max(5, (self.left_window_width // 2) - (offset_y * 2) - 8)
                 name = t.name[:]
                 if len(name) > max_len:
                 name = t.name[:]
                 if len(name) > max_len:
-                    name = name[:max_len] + '…'
+                    name = name[:max_len - 1] + '…'
                 safe_addstr(y, 0, '@%s:%s' % (t.thing_char, name))
                 y += 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():
                     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'):
                 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'):
             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[:]
             name = t.name[:]
-            if len(name) > 6:
-                name = name[:6] + '…'
-            safe_addstr(self.size.y - 1, start_x,
+            if len(name) > 7:
+                name = name[:6 - 1] + '…'
+            safe_addstr(self.screen.size.y - 1, start_x,
                         '@%s:%s' % (t.thing_char, name))
 
         def draw_help():
                         '@%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)
                     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)
                 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)):
             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():
                     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()
             recalc_input_lines()
             if self.mode.has_input_prompt:
                 draw_input()
@@ -1259,7 +1164,6 @@ class TUI:
             'dance': 'DANCE',
         }
 
             'dance': 'DANCE',
         }
 
-        curses.curs_set(0)  # hide cursor
         curses.start_color()
         self.set_default_colors()
         curses.init_pair(1, 7, 0)
         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')
             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 = '> '
         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:
         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
             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:
             try:
-                key = stdscr.getkey()
+                key = self.screen.stdscr.getkey()
                 self.do_refresh = True
             except curses.error:
                 continue
                 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
                 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:
                 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.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
                         self.reconnect()
                     else:
                         self.flash = True