home · contact · privacy
Allow prompt cursor to move horizontally. master
authorChristian Heller <c.heller@plomlompom.de>
Mon, 9 Jun 2025 23:42:36 +0000 (01:42 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 9 Jun 2025 23:42:36 +0000 (01:42 +0200)
ircplom.py

index 67d8ae22870b63e12205256e95871311583f903c..0e1381d10255e34d14bc0d9cba0a7e75183d2c87 100755 (executable)
@@ -24,6 +24,8 @@ CONN_RECV_BUFSIZE = 1024
 KEYBINDINGS = {
     'KEY_BACKSPACE': ('window.prompt.backspace',),
     'KEY_ENTER': ('prompt_enter',),
 KEYBINDINGS = {
     'KEY_BACKSPACE': ('window.prompt.backspace',),
     'KEY_ENTER': ('prompt_enter',),
+    'KEY_LEFT': ('window.prompt.move_cursor', 'left'),
+    'KEY_RIGHT': ('window.prompt.move_cursor', 'right'),
     'KEY_UP': ('window.prompt.scroll', 'up'),
     'KEY_DOWN': ('window.prompt.scroll', 'down'),
     'KEY_PGUP': ('window.log.scroll', 'up'),
     'KEY_UP': ('window.prompt.scroll', 'up'),
     'KEY_DOWN': ('window.prompt.scroll', 'down'),
     'KEY_PGUP': ('window.log.scroll', 'up'),
@@ -40,6 +42,10 @@ B64_PREFIX = 'b64:'
 OSC52_PREFIX = ']52;c;'
 PASTE_DELIMITER = '\007'
 
 OSC52_PREFIX = ']52;c;'
 PASTE_DELIMITER = '\007'
 
+PROMPT_TEMPLATE = '> '
+PROMPT_ELL_IN = '<…'
+PROMPT_ELL_OUT = '…>'
+
 IRCSPEC_LINE_SEPARATOR = b'\r\n'
 IRCSPEC_TAG_ESCAPES = ((r'\:', ';'),
                        (r'\s', ' '),
 IRCSPEC_LINE_SEPARATOR = b'\r\n'
 IRCSPEC_TAG_ESCAPES = ((r'\:', ';'),
                        (r'\s', ' '),
@@ -537,32 +543,43 @@ class PromptWidget(ScrollableWidget):
 
     def __init__(self, *args, **kwargs) -> None:
         super().__init__(*args, **kwargs)
 
     def __init__(self, *args, **kwargs) -> None:
         super().__init__(*args, **kwargs)
+        self._clear()
         self._input_buffer = ''
         self._input_buffer = ''
-        self._history_idx = 0
 
     def set_geometry(self, measurements: YX) -> None:
         self._y, self._width = measurements
 
     def append(self, to_append: str) -> None:
 
     def set_geometry(self, measurements: YX) -> None:
         self._y, self._width = measurements
 
     def append(self, to_append: str) -> None:
-        self._input_buffer += to_append
+        self._cursor_x += len(to_append)
+        self._input_buffer = (self._input_buffer[:self._cursor_x - 2]
+                              + to_append
+                              + self._input_buffer[self._cursor_x - 2:])
         self._history_idx = 0
         self.draw()
 
     def draw(self) -> None:
         self._history_idx = 0
         self.draw()
 
     def draw(self) -> None:
-        cursor = '_'
-        prompt_template = '> '
-        prompt = f'{prompt_template}'
+        prompt = PROMPT_TEMPLATE[:]
+        content = self._input_buffer[:]
+        if self._cursor_x > len(self._input_buffer):
+            content += ' '
+        half_width = self._width // 2
+        to_write = f'{prompt}{content}'
         offset = 0
         offset = 0
-        while True:
-            to_write = f'{prompt}{self._input_buffer[offset:]}{cursor}'
-            len_too_much = len(to_write) - self._width
-            if len_too_much <= 0:
-                break
-            offset += len_too_much
-            prompt = f'<{offset}|{prompt_template}…'
-        self._write(to_write[:-1], self._y, padding=False)
-        self._write(to_write[-1], attribute='reverse', padding=False)
-        self._write()
+        if len(to_write) > self._width and self._cursor_x > half_width:
+            prompt = f'{PROMPT_TEMPLATE}{PROMPT_ELL_IN}'
+            if self._cursor_x > len(content) - half_width:
+                offset = len(content) - self._width + len(prompt)
+            else:
+                offset = self._cursor_x - half_width + len(prompt) // 2
+        cursor_x_to_write = len(prompt) - 1 + self._cursor_x - offset
+        to_write = f'{prompt}{content[offset:]}'
+        if len(to_write) > self._width:
+            to_write = (to_write[:self._width - len(PROMPT_ELL_OUT)]
+                        + PROMPT_ELL_OUT)
+        self._write(to_write[:cursor_x_to_write], self._y, padding=False)
+        self._write(to_write[cursor_x_to_write], attribute='reverse',
+                    padding=False)
+        self._write(to_write[cursor_x_to_write + 1:])
 
     def _scroll(self, up: bool = True) -> None:
         if up and -(self._history_idx) < len(self._history):
 
     def _scroll(self, up: bool = True) -> None:
         if up and -(self._history_idx) < len(self._history):
@@ -579,18 +596,30 @@ class PromptWidget(ScrollableWidget):
         else:
             return
         self._input_buffer = self._history[self._history_idx][:]
         else:
             return
         self._input_buffer = self._history[self._history_idx][:]
-        self.draw()
 
     def cmd__backspace(self) -> None:
         'Truncate current content by one character, if possible.'
 
     def cmd__backspace(self) -> None:
         'Truncate current content by one character, if possible.'
-        self._input_buffer = self._input_buffer[:-1]
-        self._history_idx = 0
+        if self._cursor_x > 1:
+            self._cursor_x -= 1
+            self._input_buffer = (self._input_buffer[:self._cursor_x - 1]
+                                  + self._input_buffer[self._cursor_x:])
+            self._history_idx = 0
+            self.draw()
+
+    def cmd__move_cursor(self, direction: str) -> None:
+        'Move cursor one space into direction ("left" or "right") if possible.'
+        if direction == 'left' and self._cursor_x > 1:
+            self._cursor_x -= 1
+        elif direction == 'right'\
+                and self._cursor_x <= len(self._input_buffer):
+            self._cursor_x += 1
+        else:
+            return
         self.draw()
 
     def _clear(self) -> None:
         self.draw()
 
     def _clear(self) -> None:
-        'Empty current content.'
         self._input_buffer = ''
         self._input_buffer = ''
-        self.draw()
+        self._cursor_x = len(self._input_buffer) + 1
 
     def enter(self) -> str:
         'Return current content while also clearing and then redrawing.'
 
     def enter(self) -> str:
         'Return current content while also clearing and then redrawing.'
@@ -903,7 +932,7 @@ class KeyboardLoop(Loop):
             self.broadcast(EventType.PROMPT_ADD, yielded)
         else:
             self.broadcast(EventType.ALERT,
             self.broadcast(EventType.PROMPT_ADD, yielded)
         else:
             self.broadcast(EventType.ALERT,
-                           'unknown keyboard input: {yielded}')
+                           f'unknown keyboard input: {yielded}')
 
 
 def run() -> None:
 
 
 def run() -> None: