1 Commits

Author SHA1 Message Date
Dominic Zimmer
556b771078 Add inital work on cursor movement, bugs everywhere! 2019-08-08 16:06:33 +02:00
5 changed files with 122 additions and 176 deletions

View File

@@ -15,23 +15,3 @@ Once you obtained your own api key and hash, you need to set them as your enviro
`TTTC_API_ID` and `TTTC_API_HASH`, respectively.
The client can be run with `python3 tttc.py`.
## Keybindings
Currently, there is no way of changing the keybindings in a config. This is subject to change in a future update.
The default key bindings are
Key | Function
--|--
i| Enter insert mode (to compose a message)
y, Return | Send message
Esc | Cancel, Exit current mode
c/C | Previous/Next Dialog
E | Toggle emoji ASCII display
`n` e | Edit message `n` (ESC to open prompt to save changes)
`n` r | Reply to message `n` (submit draft)
`n` d | Delete message `n`
/ | enter search mode
n/N | Previous/Next search result
Q | exit TTTC
q `r` | Record macro into register `r`

View File

@@ -22,7 +22,6 @@ class AuthView():
self.inputs = ""
self.w, self.h = curses.COLS, curses.LINES
self.fin = False
self.showinput = True
async def textinput(self):
@@ -48,44 +47,36 @@ class AuthView():
self.stdscr.refresh()
self.phone = await self.textinput()
try:
response = await self.client.send_code_request(self.phone.replace("+","00").replace(" ",""))
response = await self.client.send_code_request(self.phone)
if not response.phone_registered:
self.stdscr.addstr("This phone number is not registered in telegram. ")
self.stdscr.refresh()
else:
break
except telethon.errors.rpcerrorlist.FloodWaitError as err:
self.stdscr.addstr(f"The telegram servers blocked you for too many retries. ({err.seconds}s remaining).")
self.stdscr.addstr(f"The telegram servers blocked you for too many retries ({err.seconds}s remaining). ")
self.stdscr.refresh()
except Exception as e:
self.stdscr.addstr(f"An error occured: {str(e)}")
self.stdscr.addstr("Incorrect phone number. ")
self.stdscr.refresh()
self.stdscr.addstr("Now authentificate with the code telegram sent to you.")
self.stdscr.addstr("auth with code now.")
self.stdscr.refresh()
while True:
done = False
self.code = await self.textinput()
try:
await self.client.sign_in(self.phone, self.code)
except telethon.errors.SessionPasswordNeededError:
self.stdscr.addstr("Password required to log in")
self.stdscr.refresh()
self.passwd = await self.textinput()
try:
self.code = await self.textinput()
await self.client.sign_in(self.phone.replace("+","00").replace(" ",""), self.code)
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
self.stdscr.addstr("The authentification code was wrong. Please try again.")
self.stdscr.refresh()
except telethon.errors.SessionPasswordNeededError:
self.showinput = False
self.stdscr.addstr("A 2FA password is required to log in.")
self.stdscr.refresh()
while True:
self.passwd = await self.textinput()
try:
await self.client.sign_in(password=self.passwd)
done = True
break
except telethon.errors.PasswordHashInvalidError:
self.stdscr.addstr("Incorrect password. Try again.")
self.stdscr.refresh()
if done:
break
self.stdscr.addstr("Authentification successfull. Please wait until the client has finished loading.")
await self.client.sign_in(password=self.passwd)
# TODO: debug me
except:
show_stacktrace()
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
pass
self.stdscr.addstr(f"auth successful. ")
self.stdscr.refresh()
async def handle_key(self, key):
@@ -96,10 +87,7 @@ class AuthView():
self.inputs = self.inputs[0:-1]
else:
self.inputs += key
if self.showinput:
self.stdscr.addstr(20, 50, self.inputs)
else:
self.stdscr.addstr(20, 50, "*"*len(self.inputs))
self.stdscr.addstr(20, 50, self.inputs)
self.stdscr.clrtoeol()
self.stdscr.refresh()

View File

@@ -26,6 +26,7 @@ def handle():
messaging.add_argument("--me", action="store_true", help="Send the message to yourself")
messaging.add_argument("--target", "-t", metavar="chatid", type=int, help="Send the message to the given chat")
messaging.add_argument("--message", "--msg", "-m", help="Provide a message.")
messaging.add_argument("--stdin", "-i", action="store_true", help="Read a message from stdin.")
parsed = parser.parse_args()
global debug
if parsed.verbose:
@@ -57,8 +58,6 @@ def handle():
debug("Fetching chats...", file=sys.stderr)
chats = client.get_dialogs()
stdinput = "".join([line for line in sys.stdin])
filtered = chats if parsed.list else filter_chats((parsed.startswith, parsed.contains, parsed.matches))
unique = None
if filtered:
@@ -68,12 +67,12 @@ def handle():
for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name)
else:
if not (parsed.message or stdinput):
if not (parsed.message or parsed.stdin):
for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name)
exit()
unique = filtered[0].id
if not (parsed.message or stdinput):
if not (parsed.message or parsed.stdin):
exit() # we are done here
recipient = unique or parsed.target
@@ -93,11 +92,11 @@ def handle():
# print("Illegal entity id. Aborting.", file=sys.stderr)
# exit(1)
#if parsed.message or parsed.stdin:
send_message(client, recipient, message=parsed.message, stdinput=stdinput)
if parsed.message or parsed.stdin:
send_message(client, recipient, message=parsed.message)
return True
def send_message(client, chat_id, message, stdinput):
def send_message(client, chat_id, message):
#print(f"call to {client} {chat_id} {message}")
try:
recipient = client.get_input_entity(chat_id)
@@ -105,14 +104,13 @@ def send_message(client, chat_id, message, stdinput):
print("Could not find the entity for this entity id. Aborting.", file=sys.stderr)
exit(1)
debug("Chat exists. Sending message.", file=sys.stderr)
if message and stdinput:
out = f"{message}\n{stdinput}"
else:
out = message or stdinput
if not out.strip():
if message is None:
debug("No message specified. Reading from stdin.", file=sys.stderr)
message = "".join([line for line in sys.stdin])
if message.strip() == "":
print("The message must not be empty.", file=sys.stderr)
exit()
client.send_message(chat_id, out)
client.send_message(chat_id, message)
def filter_chats(filt):
if filt == (None, None, None):

View File

@@ -60,12 +60,51 @@ class Drawtool():
return newlines
#return textwrap.wrap(s, width = width)
def move_cursor_home(self):
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
self.main_view.cursor -= x
#len(lines[y])
def move_cursor_end(self):
try:
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
self.main_view.cursor -= x - len(lines[y])
except:
show_stacktrace()
#len(lines[y])
def move_cursor_up(self):
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
lines = self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]
debug(lines)
debug(f"x:{x} y:{y}")
if y > 0:
self.main_view.cursor -= x
self.main_view.cursor -= 1
def move_cursor_down(self):
pass
def _get_cursor_position(self, s, width = 50):
lines = self._get_input_lines(s, width = width)[-self.input_lines:]
alllines = self._get_input_lines(s, width = width)
if not lines:
return (0, 0)
x = len(lines[-1])
y = len(lines) - 1
curs = self.main_view.cursor
y = 0
x = 0
for line in alllines:
if curs > len(line):
curs -= len(line)
y += 1
else:
x = curs
break
curs -= 1
if y >= self.input_lines:
y = self.input_lines - 1
return y, x
async def redraw(self):
@@ -79,7 +118,7 @@ class Drawtool():
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"])
else:
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box)
elif self.main_view.mode in ["popup", "popupmessage"]:
elif self.main_view.mode == "popup":
_, question = self.main_view.popup
self.stdscr.addstr(self.H - 1, 0, question)
elif self.main_view.mode == "vimmode":
@@ -91,7 +130,7 @@ class Drawtool():
for index, line in enumerate(self._get_input_lines(self.main_view.inputs, width = self.W - 4)[-self.input_lines:]):
self.stdscr.addstr(self.H - self.input_lines - 2 + index, 2, f"{line}")
if self.main_view.mode in ["insert", "edit"]:
if self.main_view.mode == "insert":
curses.curs_set(1)
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4)
self.stdscr.move(self.H - self.input_lines - 2 + y, 2 + x)

View File

@@ -27,14 +27,8 @@ class MainView():
# self.client.add_event_handler(self.on_read, events.MessageRead)
self.text_emojis = True
self.macros = {}
self.macro_recording = None
self.macro_sequence = []
self.inputs = ""
self.inputs_cursor = 0
self.edit_message = None
self.cursor = 0
self.popup = None
@@ -58,7 +52,6 @@ class MainView():
self.selected_message = None
self.mode = "normal"
self.modestack = []
async def quit(self):
self.fin = True
@@ -308,29 +301,21 @@ class MainView():
subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
def popup_message(self, question):
self.modestack.append(self.mode)
self.mode = "popupmessage"
self.mode = "popup"
async def action_handler(self, key):
pass
self.mode = "normal"
self.popup = (action_handler, question)
def spawn_popup(self, action_handler, question):
# on q press
self.modestack.append(self.mode)
self.mode = "popup"
self.popup = (action_handler, question)
async def handle_key(self, key, redraw = True):
if self.mode == "popupmessage":
self.mode = self.modestack.pop()
async def handle_key(self, key):
if not self.ready:
return
if key == "RESIZE":
await self.drawtool.resize()
return
if self.macro_recording:
if key != "q":
self.macro_sequence.append(key)
if self.mode == "search":
if key == "ESCAPE" or key == "RETURN":
self.mode = "normal"
@@ -374,41 +359,19 @@ class MainView():
self.vimline_box = ""
elif key == "RETURN" or key == "y":
await self.send_message()
elif key == "Q":
await self.quit()
elif key == "q":
if self.macro_recording == None:
# start macro recording
async def record_macro(self, key):
if "a" < key.lower() < "z":
self.macro_recording = key
self.popup_message(f"recording into {key}")
else:
self.popup_message(f"Register must be [a-zA-Z]")
self.spawn_popup(record_macro, "Record into which register?")
else:
# end macro recording
self.macros[self.macro_recording] = self.macro_sequence
self.macro_recording = None
self.macro_sequence = []
elif key == "@":
# execute macro
async def ask_macro(self, key):
if key in self.macros.keys():
macro = self.macros[key]
debug(macro)
for k in macro:
await self.handle_key(k, redraw = False)
else:
self.popup_message(f"No such macro @{key}")
self.spawn_popup(ask_macro, "Execute which macro?")
await self.quit()
#elif key == "D":
# for i in range(10):
# self.select_prev_chat()
#elif key == "d":
# for i in range(10):
# self.select_next_chat()
elif key == "C":
self.select_prev_chat()
elif key == "c":
self.select_next_chat()
elif key == "E":
elif key == "e":
self.text_emojis ^= True
elif key == "R":
await self.mark_read()
@@ -434,16 +397,6 @@ class MainView():
self.spawn_popup(action_handler, question)
await self.drawtool.redraw()
elif key == "e":
if self.command_box:
try:
n = int(self.command_box)
except:
return
self.edit_message = self.dialogs[self.selected_chat]["messages"][n]
self.mode = "edit"
self.inputs = emojis.decode(self.edit_message.text)
self.command_box = ""
elif key == "r":
if self.command_box:
try:
@@ -483,65 +436,53 @@ class MainView():
self.drawtool.show_indices ^= True
elif self.mode == "popup":
action, _ = self.popup
# I think this could break
self.mode = self.modestack.pop()
await action(self, key)
elif self.mode == "edit":
if key == "ESCAPE":
async def ah(self, key):
if key in ["Y", "y", "RETURN"]:
edit = await self.edit_message.edit(self.inputs)
await self.on_message(edit)
# TODO: update message in chat
# this on_message call does not work reliably
self.mode = "normal"
else:
self.popup_message("Edit discarded.")
self.mode = "normal"
self.spawn_popup(ah, "Do you want to save the edit? [Y/n]")
elif key == "LEFT":
self.insert_move_left()
elif key == "RIGHT":
self.insert_move_right()
elif key == "BACKSPACE":
self.inputs = self.inputs[0:-1]
elif key == "RETURN":
self.inputs += "\n"
else:
self.inputs += key
elif self.mode == "insert":
if key == "ESCAPE":
self.mode = "normal"
elif key == "LEFT":
self.insert_move_left()
elif key == "UP":
self.drawtool.move_cursor_up()
elif key == "RIGHT":
self.insert_move_right()
elif key == "HOME":
self.drawtool.move_cursor_home()
#self.cursor = 0
elif key == "END":
self.drawtool.move_cursor_end()
#self.cursor = len(self.inputs)
elif key == "DEL":
try:
if len(self.inputs) > self.cursor:
inp = list(self.inputs)
inp.pop(self.cursor)
self.inputs = "".join(inp)
except:
debug(f"{self.cursor}, {self.inputs}")
show_stacktrace()
elif key == "BACKSPACE":
self.inputs = self.inputs[0:-1]
elif key == "RETURN":
self.inputs += "\n"
try:
if len(self.inputs) > 0:
inp = list(self.inputs)
inp.pop(self.cursor - 1)
self.inputs = "".join(inp)
self.insert_move_left()
except:
debug(f"{self.cursor}, {self.inputs}")
show_stacktrace()
else:
self.inputs += key
if key == "RETURN":
key = "\n"
inp = list(self.inputs)
inp.insert(self.cursor, key)
self.inputs = "".join(inp)
self.cursor += 1
self.command_box = ""
if redraw:
await self.drawtool.redraw()
await self.drawtool.redraw()
def insert_move_left(self):
self.inputs_cursor = max(0, self.cursor - 1)
self.cursor = max(0, self.cursor - 1)
def insert_move_right(self):
self.inputs_cursor = min(len(self.inputs), self.cursor + 1)
async def handle_key_old(self, key):
if key == "RETURN":
with await self.inputevent:
self.inputevent.notify()
elif key == "":
chat = self.dialogs[self.selected_chat]["dialog"]
last_message = self.dialogs[self.selected_chat]["messages"][0]
await self.client.send_read_acknowledge(chat, max_id=last_message.id)
self.dialogs[self.selected_chat]["unread_count"] = 0
else:
self.inputs += key
self.drawtool.redraw()
self.cursor = min(len(self.inputs), self.cursor + 1)