+import collections
import curses
import itertools
import os
self.composer = Composer(self)
return self.composer
+ def new_composer(self, text):
+ self.composer = Composer(self, text)
+ return self.composer
+
def run(self):
home_feed = self.home_timeline_feed()
mentions_feed = self.mentions_feed()
finally:
self.curses_shutdown()
-class Menu:
+class Activity:
+ def chain_to(self, activity):
+ assert self.cc.activity_stack[-1] is self
+ if (len(self.cc.activity_stack) > 1 and
+ self.cc.activity_stack[-2] == activity):
+ self.cc.activity_stack.pop()
+ else:
+ self.cc.activity_stack[-1] = activity
+
+ def push_to(self, activity):
+ self.cc.activity_stack.append(activity)
+
+class Menu(Activity):
status_extra_text = None
def __init__(self, cc):
def add_keys(self, sl):
sl.keys.append(('RET', 'Back'))
- def chain_to(self, activity):
- assert self.cc.activity_stack[-1] is self
- if (len(self.cc.activity_stack) > 1 and
- self.cc.activity_stack[-2] == activity):
- self.cc.activity_stack.pop()
- else:
- self.cc.activity_stack[-1] = activity
-
- def push_to(self, activity):
- self.cc.activity_stack.append(activity)
-
def handle_key(self, ch):
if ch in {'q', 'Q', '\n', '\r'}:
return 'quit'
else:
return super().handle_key(ch)
-class File:
+class File(Activity):
# Base class for anything where you page up and down.
def __init__(self, cc):
self.cc = cc
+ self.mode = 'normal'
def handle_key(self, ch):
- if ch in {' ', curses.KEY_NPAGE}:
- self.down_screen()
- elif ch in {'-', 'b', 'B', curses.KEY_PPAGE}:
- self.up_screen()
- elif ch == curses.KEY_DOWN:
- self.down_line()
- elif ch in {'\n', '\r'}:
- if not self.down_line():
+ if self.mode == 'normal':
+ if ch in {' ', curses.KEY_NPAGE}:
+ self.down_screen()
+ elif ch in {'-', 'b', 'B', curses.KEY_PPAGE}:
+ self.up_screen()
+ elif ch == curses.KEY_DOWN:
+ self.down_line()
+ elif ch in {'\n', '\r'}:
+ if not self.down_line():
+ return 'quit'
+ elif ch in {curses.KEY_UP}:
+ self.up_line()
+ elif ch in {'0', curses.KEY_HOME}:
+ self.goto_top()
+ elif ch in {'z', 'Z', curses.KEY_END}:
+ self.goto_bottom()
+ elif ch in {'q', 'Q'}:
return 'quit'
- elif ch in {curses.KEY_UP}:
- self.up_line()
- elif ch in {'0', curses.KEY_HOME}:
- self.goto_top()
- elif ch in {'z', 'Z', curses.KEY_END}:
- self.goto_bottom()
- elif ch in {'q', 'Q'}:
- return 'quit'
+ elif ch in {'s', 'S'}:
+ self.send_mode()
+ elif self.mode == 'send':
+ if ch in {'q', 'Q'}:
+ self.mode = 'normal'
+ elif ch in {'-', 'b', 'B', curses.KEY_UP}:
+ self.prev_send_target()
+ elif ch in {'+', curses.KEY_DOWN}:
+ self.next_send_target()
+ elif ch in {' ', 'i', 'I', 'a', 'A', 'l', 'L'}:
+ self.send_complete()
+
+ def send_mode(self):
+ pass # not supported
class ObjectFile(File):
def __init__(self, cc, constructor, feed, title):
self.lines = None
self.linepos = None
self.width = None
+ self.send_target = None
+ self.old_display_state = None
+ self.index_by_line = []
def iter_text_indexed(self):
yield self.header, None
return got_any
def regenerate_lines(self, width):
- if self.width == width and not self.fetch_new():
+ display_state = (self.mode, self.send_target)
+ if (self.width == width and display_state == self.old_display_state and
+ not self.fetch_new()):
return
+ self.old_display_state = display_state
self.lines = []
+ self.index_by_line = []
pos = 0
for thing, itemindex in self.iter_text_indexed():
- for line in thing.render(width):
+ params = {}
+ if (self.mode == 'send' and itemindex == self.send_target and
+ isinstance(thing, text.FromLine)):
+ params['target'] = True
+ for line in thing.render(width, **params):
for s in line.split(width):
self.lines.append(s)
+ self.index_by_line.append(itemindex)
if itemindex == self.itempos:
pos = len(self.lines)
if self.width != width:
self.cc.print_at(y, 0, line)
sl = text.FileStatusLine()
- if self.linepos >= len(self.lines):
- sl.keys.append(('-', 'Up'))
+ if self.mode == 'send':
+ sl.keys.append(('SPACE', 'Reply'))
+ sl.keys.append(('-', None))
+ sl.keys.append(('+', None))
+ sl.keys.append(('Q', 'Quit'))
else:
- sl.keys.append(('SPACE', 'More'))
- sl.keys.append(('Q', 'Exit'))
- sl.proportion = self.linepos / len(self.lines)
+ if self.linepos >= len(self.lines):
+ sl.keys.append(('-', 'Up'))
+ else:
+ sl.keys.append(('SPACE', 'More'))
+ sl.keys.append(('Q', 'Exit'))
+ sl.proportion = self.linepos / len(self.lines)
sl_rendered = util.exactly_one(sl.render(self.cc.scr_w))
self.cc.print_at(self.cc.scr_h - 1, 0, sl_rendered)
+ def send_mode(self):
+ self.mode = 'send'
+ self.send_target = self.index_by_line[self.linepos-1]
+ def prev_send_target(self):
+ self.send_target = max(self.minpos, self.send_target-1)
+ print("prev ->", self.send_target, file=sys.stderr)
+ def next_send_target(self):
+ self.send_target = min(self.maxpos-1, self.send_target+1)
+ print("next ->", self.send_target, file=sys.stderr)
+ def send_complete(self):
+ self.mode = 'normal'
+ recipients = collections.OrderedDict()
+ for r in self.statuses[self.send_target].get_reply_recipients():
+ if r == self.cc.fq_username or r in recipients:
+ continue
+ recipients[r] = 1
+ self.push_to(self.cc.new_composer("".join(
+ f"@{r} " for r in recipients)))
+
def move_to(self, pos):
old_linepos = self.linepos
self.linepos = pos
def layout(self, width):
self.colourise()
self.yx = [None] * (len(self.text) + 1)
+ self.yx[0] = 0, 0 # in case there's no text at all
self.lines = []
y = 0
pos = 0
y += 1
pos = next_nl + 1
- def __init__(self, cc):
+ def __init__(self, cc, initial_text=""):
self.cc = cc
self.header = text.FileHeader("Compose a post")
- self.text = "@Lorem #ipsum dolor sit amet, @consectetur@adipisicing.elit, sed do eiusmod tempor https://incididunt.ut.com/labore/et/ dolore magna\naliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
- self.point = 95
+ self.text = initial_text
+ self.point = len(self.text)
self.goal_column = None
def render(self):
if self.goal_column is None or not is_updown:
self.goal_column = self.cx
+ # TODO:
+ #
+ # ^W,^T are Mono's keys for moving back/forward a word.
+ #
+ # Not sure what to do about ^K/^Y for one-line cut-paste, given
+ # that the autowrap makes the semantics a bit weird compared to
+ # the original setup.
+ #
+ # Probably don't want to exactly replicate the block
+ # operations either. Perhaps I should invent my own more sensible
+ # approach to cut, copy and paste.
+
if ch in {ctrl('b'), curses.KEY_LEFT}:
self.move_to(self.point - 1)
elif ch in {ctrl('f'), curses.KEY_RIGHT}:
if yx <= (self.cy + 1, self.goal_column)))
except ValueError:
new_point = len(self.text)
- self.goal_column = None
self.move_to(new_point)
+ if self.dtext.yx[self.point][0] != self.cy + 1:
+ # Failed to go down; probably went to the end; reset
+ # the goal column.
+ self.goal_column = None
elif ch in {ctrl('p'), curses.KEY_UP}:
try:
new_point = util.last(
if yx <= (self.cy - 1, self.goal_column)))
except ValueError:
new_point = 0
- self.goal_column = None
self.move_to(new_point)
+ if self.dtext.yx[self.point][0] != self.cy - 1:
+ # Failed to go down; probably went to the start; reset
+ # the goal column.
+ self.goal_column = None
elif ch in {ctrl('a'), curses.KEY_HOME}:
new_point = next(
i for i, yx in enumerate(self.dtext.yx[:self.point+1])
self.text = (self.text[:self.point] + '\n' +
self.text[self.point:])
self.point += 1
- elif isinstance(ch, str) and ' ' <= ch < '\x7F' or '\xA0' <= ch:
+ 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:
- pass # print("?", repr(ch), file=sys.stderr)
if not is_updown:
self.goal_column = None
text.ColouredString("ixjkl")])
self.assertEqual(t.yx, ([(0,i) for i in range(10)] +
[(1,i) for i in range(6)]))
+
+ t = Composer.DisplayText("")
+ t.layout(10)
+ self.assertEqual(t.lines, [])
+ self.assertEqual(t.yx, [(0,0)])