self.composer = Composer(self)
return self.composer
- def new_composer(self, text):
- self.composer = Composer(self, text)
+ def new_composer(self, text, reply_header, reply_id):
+ self.composer = Composer(self, text, reply_header, reply_id)
return self.composer
def run(self):
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)))
+ initial_content = "".join(f"@{r} " for r in recipients)
+
+ reply_id = self.statuses[self.send_target].get_reply_id()
+ if reply_id is not None:
+ hp = text.HTMLParser()
+ try:
+ reply_status = self.cc.get_status_by_id(reply_id)
+ hp.feed(reply_status['content'])
+ except client.HTTPError as ex:
+ hp.feed(f'[unavailable: {ex.response.status_code}]')
+ except KeyError: # returned 200 with an empty JSON object
+ hp.feed(f'[unavailable]')
+ hp.done()
+ reply_header = text.InReplyToLine(hp.paras)
+ else:
+ reply_header = None
+
+ self.push_to(self.cc.new_composer(
+ initial_content, reply_header, reply_id))
+
def move_to(self, pos):
old_linepos = self.linepos
y += 1
pos = next_nl + 1
- def __init__(self, cc, initial_text=""):
+ def __init__(self, cc, initial_text="", reply_header=None, reply_id=None):
self.cc = cc
- self.header = text.FileHeader("Compose a post")
+ self.reply_header = reply_header
+ self.reply_id = reply_id
+ if self.reply_header is None:
+ assert self.reply_id is None
+ self.header = text.FileHeader("Compose a post")
+ else:
+ assert self.reply_id is not None
+ self.header = text.FileHeader("Compose a reply")
self.text = initial_text
self.point = len(self.text)
self.goal_column = None
+ self.mode = 'normal'
def render(self):
y = 0
self.cc.print_at(y, 0, line)
y += 1
+ if self.reply_header is not None:
+ for line in self.reply_header.render(self.cc.scr_w):
+ self.cc.print_at(y, 0, line)
+ y += 1
+
# FIXME: here the real Mono editor has some keypress help of the form
# [F1]:Options (or [^O]) [F3]:Mark Block [F6]:Goto [F8]:Read File
# [F2]:Finish [F7]:Find [F9]:Write File
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}:
- self.move_to(self.point + 1)
- elif ch in {ctrl('n'), curses.KEY_DOWN}:
- try:
- new_point = util.last(
- (i for i, yx in enumerate(self.dtext.yx[self.point:],
- self.point)
- if yx <= (self.cy + 1, self.goal_column)))
- except ValueError:
- new_point = len(self.text)
-
- 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:
+ if self.mode == 'normal':
+ # 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}:
+ self.move_to(self.point + 1)
+ elif ch in {ctrl('n'), curses.KEY_DOWN}:
+ try:
+ new_point = util.last(
+ (i for i, yx in enumerate(self.dtext.yx[self.point:],
+ self.point)
+ if yx <= (self.cy + 1, self.goal_column)))
+ except ValueError:
+ new_point = len(self.text)
+
+ 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(
+ (i for i, yx in enumerate(self.dtext.yx[:self.point+1])
+ if yx <= (self.cy - 1, self.goal_column)))
+ except ValueError:
+ new_point = 0
+
+ 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])
+ if yx[0] == self.cy)
+ self.move_to(new_point)
+ elif ch in {ctrl('e'), curses.KEY_END}:
new_point = util.last(
- (i for i, yx in enumerate(self.dtext.yx[:self.point+1])
- if yx <= (self.cy - 1, self.goal_column)))
- except ValueError:
- new_point = 0
-
- 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])
- if yx[0] == self.cy)
- self.move_to(new_point)
- elif ch in {ctrl('e'), curses.KEY_END}:
- new_point = util.last(
- i for i, yx in enumerate(self.dtext.yx[self.point:],
- 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 {'\r', '\n'}:
- self.text = (self.text[:self.point] + '\n' +
- self.text[self.point:])
- self.point += 1
- 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
+ i for i, yx in enumerate(self.dtext.yx[self.point:],
+ 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 {'\r', '\n'}:
+ self.text = (self.text[:self.point] + '\n' +
+ self.text[self.point:])
+ self.point += 1
+ 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
+ elif self.mode == 'ctrlo':
+ if ch == ' ':
+ self.post()
+ self.cc.composer = None
+ self.cc.activity_stack.pop()
+ elif ch == 'q':
+ self.cc.composer = None
+ self.cc.activity_stack.pop()
+ else:
+ self.mode = 'normal'
if not is_updown:
self.goal_column = None
+ def post(self):
+ params = {
+ "status": self.text,
+ "visibility": "public",
+ "language": "en", # FIXME
+ }
+ if self.reply_id is not None:
+ params["in_reply_to_id"] = self.reply_id
+ self.cc.post("statuses", **params)
+
class testComposerLayout(unittest.TestCase):
def testLayout(self):
t = Composer.DisplayText("abc")