chiark / gitweb /
Ability to prompt on the bottom screen line.
authorSimon Tatham <anakin@pobox.com>
Sat, 9 Dec 2023 16:16:14 +0000 (16:16 +0000)
committerSimon Tatham <anakin@pobox.com>
Sat, 9 Dec 2023 17:21:45 +0000 (17:21 +0000)
cursesclient.py

index 64b6cf13123df2754bce89dc1db344c1feb1bffd..0dbf77c083e0b695e6875f072db74ec9e615c744 100644 (file)
@@ -48,7 +48,7 @@ class CursesUI(client.Client):
             default_fg = default_bg = None
 
         self.scr.keypad(1)
-        self.scr.scrollok(1)
+        self.scr.scrollok(0)
         self.scr.nodelay(1) # so we can use get_wch() via a select loop
         curses.noecho()
         self.scr_h, self.scr_w = self.scr.getmaxyx()
@@ -98,9 +98,15 @@ class CursesUI(client.Client):
     def move_cursor_to(self, y, x):
         self.scr.move(y, x)
 
+    def clear_line(self, y):
+        try:
+            self.print_at(y, 0, text.ColouredString(' ' * self.scr_w))
+        except curses.error:
+            pass # this can happen if clearing the bottom right screen corner
+
     def clear(self):
         for y in range(self.scr_h):
-            self.print_at(y, 0, text.ColouredString(' ' * self.scr_w))
+            self.clear_line(y)
 
     def get_wch(self):
         try:
@@ -791,7 +797,99 @@ class StatusInfoFile(ObjectFile):
         super().__init__(
             cc, lambda x,cc:x, client.StatusInfoFeed(cc, self.data), title)
 
-class Composer(Activity):
+class EditorCommon:
+    def move_to(self, pos):
+        self.point = max(0, min(len(self.text), pos))
+
+    def handle_common_editing_keys(self, ch):
+        if ch in {ctrl('b'), curses.KEY_LEFT}:
+            self.move_to(self.point - 1)
+        elif ch in {ctrl('f'), curses.KEY_RIGHT}:
+            self.move_to(self.point + 1)
+        elif ch in {ctrl('h'), '\x7F', curses.KEY_BACKSPACE}:
+            if self.point > 0:
+                self.text = (self.text[:self.point - 1] +
+                             self.text[self.point:])
+                self.point -= 1
+        elif ch in {ctrl('d'), curses.KEY_DC}:
+            if self.point < len(self.text):
+                self.text = (self.text[:self.point] +
+                             self.text[self.point + 1:])
+        elif ch in {ctrl('w')}:
+            if self.point > 0:
+                while True:
+                    self.point -= 1
+                    if self.word_boundary(self.point):
+                        break
+        elif ch in {ctrl('t')}:
+            if self.point < len(self.text):
+                while True:
+                    self.point += 1
+                    if self.word_boundary(self.point):
+                        break
+        elif ch in {ctrl('k')}:
+            if self.point < len(self.text):
+                if self.text[self.point] == '\n':
+                    self.text = (self.text[:self.point] +
+                                 self.text[self.point+1:])
+                else:
+                    try:
+                        endpos = self.text.index('\n', self.point)
+                    except ValueError:
+                        endpos = len(self.text)
+                    self.ctrl_k_paste_buffer = self.text[self.point:endpos]
+                    self.text = (self.text[:self.point] +
+                                 self.text[endpos:])
+        elif ch in {ctrl('y')}:
+            self.text = (
+                self.text[:self.point] + self.ctrl_k_paste_buffer +
+                self.text[self.point:])
+            self.point += len(self.ctrl_k_paste_buffer)
+        elif isinstance(ch, str) and (' ' <= ch < '\x7F' or '\xA0' <= ch):
+            # TODO: overwrite mode
+            self.text = (self.text[:self.point] + ch +
+                         self.text[self.point:])
+            self.point += 1
+
+class BottomLinePrompt(Activity, EditorCommon):
+    def __init__(self, cc, callback, prompt, text=""):
+        self.cc = cc
+        self.prompt = prompt
+        self.text = text
+        self.point = len(self.text)
+        self.callback = callback
+
+    @property
+    def parent_activity(self):
+        assert len(self.cc.activity_stack) >= 2
+        assert self.cc.activity_stack[-1] is self
+        return self.cc.activity_stack[-2]
+
+    def render(self):
+        self.parent_activity.render()
+
+        y = self.cc.scr_h-1
+        self.cc.clear_line(y)
+
+        prompt_string = text.ColouredString(self.prompt)
+        text_string = text.ColouredString(self.text)
+        # FIXME: prevent overflow
+        self.cc.print_at(y, 0, prompt_string + text_string)
+        self.cc.move_cursor_to(
+            y, prompt_string.width + text_string[:self.point].width)
+
+    def handle_key(self, ch):
+        if ch in {ctrl('a'), curses.KEY_HOME}:
+            self.move_to(0)
+        elif ch in {ctrl('e'), curses.KEY_END}:
+            self.move_to(len(self.text))
+        elif ch in {'\r', '\n'}:
+            self.callback(self.text)
+            self.chain_to(self.parent_activity)
+        else:
+            self.handle_common_editing_keys(ch)
+
+class Composer(Activity, EditorCommon):
     class DisplayText:
         def __init__(self, text):
             self.text = text
@@ -970,9 +1068,6 @@ class Composer(Activity):
         self.cy, self.cx = self.dtext.yx[self.point]
         self.cc.move_cursor_to(self.cy + ytop, self.cx)
 
-    def move_to(self, pos):
-        self.point = max(0, min(len(self.text), pos))
-
     def word_boundary(self, pos):
         if pos == 0 or pos == len(self.text):
             return True
@@ -998,11 +1093,7 @@ class Composer(Activity):
             # selection start/end points? Probably don't want to
             # replicate Mono's underpowered [^O][B] business.
 
-            if ch in {ctrl('b'), curses.KEY_LEFT}:
-                self.move_to(self.point - 1)
-            elif ch in {ctrl('f'), curses.KEY_RIGHT}:
-                self.move_to(self.point + 1)
-            elif ch in {ctrl('n'), curses.KEY_DOWN}:
+            if ch in {ctrl('n'), curses.KEY_DOWN}:
                 try:
                     new_point = util.last(
                         (i for i, yx in enumerate(self.dtext.yx[self.point:],
@@ -1040,45 +1131,6 @@ class Composer(Activity):
                                              self.point)
                     if yx[0] == self.cy)
                 self.move_to(new_point)
-            elif ch in {ctrl('h'), '\x7F', curses.KEY_BACKSPACE}:
-                if self.point > 0:
-                    self.text = (self.text[:self.point - 1] +
-                                 self.text[self.point:])
-                    self.point -= 1
-            elif ch in {ctrl('d'), curses.KEY_DC}:
-                if self.point < len(self.text):
-                    self.text = (self.text[:self.point] +
-                                 self.text[self.point + 1:])
-            elif ch in {ctrl('w')}:
-                if self.point > 0:
-                    while True:
-                        self.point -= 1
-                        if self.word_boundary(self.point):
-                            break
-            elif ch in {ctrl('t')}:
-                if self.point < len(self.text):
-                    while True:
-                        self.point += 1
-                        if self.word_boundary(self.point):
-                            break
-            elif ch in {ctrl('k')}:
-                if self.point < len(self.text):
-                    if self.text[self.point] == '\n':
-                        self.text = (self.text[:self.point] +
-                                     self.text[self.point+1:])
-                    else:
-                        try:
-                            endpos = self.text.index('\n', self.point)
-                        except ValueError:
-                            endpos = len(self.text)
-                        self.ctrl_k_paste_buffer = self.text[self.point:endpos]
-                        self.text = (self.text[:self.point] +
-                                     self.text[endpos:])
-            elif ch in {ctrl('y')}:
-                self.text = (
-                    self.text[:self.point] + self.ctrl_k_paste_buffer +
-                    self.text[self.point:])
-                self.point += len(self.ctrl_k_paste_buffer)
             elif ch in {'\r', '\n'}:
                 self.text = (self.text[:self.point] + '\n' +
                              self.text[self.point:])
@@ -1090,11 +1142,8 @@ class Composer(Activity):
                     self.chain_to(PostMenu(self))
             elif ch in {ctrl('o')}:
                 self.mode = 'ctrlo'
-            elif isinstance(ch, str) and (' ' <= ch < '\x7F' or '\xA0' <= ch):
-                # TODO: overwrite mode
-                self.text = (self.text[:self.point] + ch +
-                             self.text[self.point:])
-                self.point += 1
+            else:
+                self.handle_common_editing_keys(ch)
         elif self.mode == 'ctrlo':
             if ch == ' ':
                 self.chain_to(PostMenu(self))