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 131 additions and 187 deletions

View File

@@ -14,24 +14,4 @@ You can read more about how to get them [here](https://core.telegram.org/api/obt
Once you obtained your own api key and hash, you need to set them as your environment variables Once you obtained your own api key and hash, you need to set them as your environment variables
`TTTC_API_ID` and `TTTC_API_HASH`, respectively. `TTTC_API_ID` and `TTTC_API_HASH`, respectively.
The client can be run with `python3 tttc.py`. 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.inputs = ""
self.w, self.h = curses.COLS, curses.LINES self.w, self.h = curses.COLS, curses.LINES
self.fin = False self.fin = False
self.showinput = True
async def textinput(self): async def textinput(self):
@@ -48,7 +47,7 @@ class AuthView():
self.stdscr.refresh() self.stdscr.refresh()
self.phone = await self.textinput() self.phone = await self.textinput()
try: 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: if not response.phone_registered:
self.stdscr.addstr("This phone number is not registered in telegram. ") self.stdscr.addstr("This phone number is not registered in telegram. ")
self.stdscr.refresh() self.stdscr.refresh()
@@ -60,32 +59,24 @@ class AuthView():
except Exception as e: except Exception as e:
self.stdscr.addstr("Incorrect phone number. ") self.stdscr.addstr("Incorrect phone number. ")
self.stdscr.refresh() 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() self.stdscr.refresh()
while True: self.code = await self.textinput()
done = False 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: try:
self.code = await self.textinput() await self.client.sign_in(password=self.passwd)
await self.client.sign_in(self.phone.replace("+","00").replace(" ",""), self.code) # TODO: debug me
except telethon.errors.rpcerrorlist.PhoneCodeInvalidError: except:
self.stdscr.addstr("The authentification code was wrong. Please try again.") show_stacktrace()
self.stdscr.refresh() except telethon.errors.rpcerrorlist.PhoneCodeInvalidError:
except telethon.errors.SessionPasswordNeededError: pass
self.showinput = False
self.stdscr.addstr("A 2FA password is required to log in.") self.stdscr.addstr(f"auth successful. ")
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.")
self.stdscr.refresh() self.stdscr.refresh()
async def handle_key(self, key): async def handle_key(self, key):
@@ -96,10 +87,7 @@ class AuthView():
self.inputs = self.inputs[0:-1] self.inputs = self.inputs[0:-1]
else: else:
self.inputs += key self.inputs += key
if self.showinput: self.stdscr.addstr(20, 50, self.inputs)
self.stdscr.addstr(20, 50, self.inputs)
else:
self.stdscr.addstr(20, 50, "*"*len(self.inputs))
self.stdscr.clrtoeol() self.stdscr.clrtoeol()
self.stdscr.refresh() 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("--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("--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("--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() parsed = parser.parse_args()
global debug global debug
if parsed.verbose: if parsed.verbose:
@@ -57,8 +58,6 @@ def handle():
debug("Fetching chats...", file=sys.stderr) debug("Fetching chats...", file=sys.stderr)
chats = client.get_dialogs() 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)) filtered = chats if parsed.list else filter_chats((parsed.startswith, parsed.contains, parsed.matches))
unique = None unique = None
if filtered: if filtered:
@@ -68,12 +67,12 @@ def handle():
for result in reversed(filtered): for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name) print(str(result.id).rjust(16) + " "*4 + result.name)
else: else:
if not (parsed.message or stdinput): if not (parsed.message or parsed.stdin):
for result in reversed(filtered): for result in reversed(filtered):
print(str(result.id).rjust(16) + " "*4 + result.name) print(str(result.id).rjust(16) + " "*4 + result.name)
exit() exit()
unique = filtered[0].id unique = filtered[0].id
if not (parsed.message or stdinput): if not (parsed.message or parsed.stdin):
exit() # we are done here exit() # we are done here
recipient = unique or parsed.target recipient = unique or parsed.target
@@ -93,11 +92,11 @@ def handle():
# print("Illegal entity id. Aborting.", file=sys.stderr) # print("Illegal entity id. Aborting.", file=sys.stderr)
# exit(1) # exit(1)
#if parsed.message or parsed.stdin: if parsed.message or parsed.stdin:
send_message(client, recipient, message=parsed.message, stdinput=stdinput) send_message(client, recipient, message=parsed.message)
return True 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}") #print(f"call to {client} {chat_id} {message}")
try: try:
recipient = client.get_input_entity(chat_id) 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) print("Could not find the entity for this entity id. Aborting.", file=sys.stderr)
exit(1) exit(1)
debug("Chat exists. Sending message.", file=sys.stderr) debug("Chat exists. Sending message.", file=sys.stderr)
if message and stdinput: if message is None:
out = f"{message}\n{stdinput}" debug("No message specified. Reading from stdin.", file=sys.stderr)
else: message = "".join([line for line in sys.stdin])
out = message or stdinput if message.strip() == "":
if not out.strip():
print("The message must not be empty.", file=sys.stderr) print("The message must not be empty.", file=sys.stderr)
exit() exit()
client.send_message(chat_id, out) client.send_message(chat_id, message)
def filter_chats(filt): def filter_chats(filt):
if filt == (None, None, None): if filt == (None, None, None):

View File

@@ -60,12 +60,51 @@ class Drawtool():
return newlines return newlines
#return textwrap.wrap(s, width = width) #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): def _get_cursor_position(self, s, width = 50):
lines = self._get_input_lines(s, width = width)[-self.input_lines:] lines = self._get_input_lines(s, width = width)[-self.input_lines:]
alllines = self._get_input_lines(s, width = width)
if not lines: if not lines:
return (0, 0) return (0, 0)
x = len(lines[-1]) curs = self.main_view.cursor
y = len(lines) - 1 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 return y, x
async def redraw(self): 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"]) self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box, self.main_view.colors["error"])
else: else:
self.stdscr.addstr(self.H - 1, 0, "/" + self.main_view.search_box) 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 _, question = self.main_view.popup
self.stdscr.addstr(self.H - 1, 0, question) self.stdscr.addstr(self.H - 1, 0, question)
elif self.main_view.mode == "vimmode": 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:]): 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}") 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) curses.curs_set(1)
y, x = self._get_cursor_position(self.main_view.inputs, width = self.W - 4) 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) 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.client.add_event_handler(self.on_read, events.MessageRead)
self.text_emojis = True self.text_emojis = True
self.macros = {}
self.macro_recording = None
self.macro_sequence = []
self.inputs = "" self.inputs = ""
self.inputs_cursor = 0 self.cursor = 0
self.edit_message = None
self.popup = None self.popup = None
@@ -58,7 +52,6 @@ class MainView():
self.selected_message = None self.selected_message = None
self.mode = "normal" self.mode = "normal"
self.modestack = []
async def quit(self): async def quit(self):
self.fin = True self.fin = True
@@ -89,22 +82,20 @@ class MainView():
dialog["messages"].insert(0, newmessage[0]) dialog["messages"].insert(0, newmessage[0])
if not event.out: if not event.out:
dialog["unread_count"] += 1 dialog["unread_count"] += 1
os.system(f"notify-send -i apps/telegram \"{dialog['dialog'].name}\" \"{newmessage[0].message}\"")
front = self.dialogs.pop(idx) front = self.dialogs.pop(idx)
self.dialogs = [front] + self.dialogs self.dialogs = [front] + self.dialogs
break break
#old dead code # auto adjust relative replys to match shifted message offsets
# # auto adjust relative replys to match shifted message offsets if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id:
# if event.chat_id == self.dialogs[self.selected_chat]["dialog"].id: if self.inputs.startswith("r"):
# if self.inputs.startswith("r"): num = self.inputs[1:].split()[0]
# num = self.inputs[1:].split()[0] try:
# try: # num = int(s[1:].split()[0])
## num = int(s[1:].split()[0]) number = int(num)
# number = int(num) msg = self.inputs.replace("r" + num, "r" + str(number+1))
# msg = self.inputs.replace("r" + num, "r" + str(number+1)) self.inputs = msg
# self.inputs = msg except:
# except: pass
# pass
# dont switch the dialoge upon arriving messages # dont switch the dialoge upon arriving messages
if idx == self.selected_chat: if idx == self.selected_chat:
self.selected_chat = 0 self.selected_chat = 0
@@ -310,29 +301,21 @@ class MainView():
subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL) subprocess.Popen(["xdg-open", f"{path}"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL)
def popup_message(self, question): def popup_message(self, question):
self.modestack.append(self.mode) self.mode = "popup"
self.mode = "popupmessage"
async def action_handler(self, key): async def action_handler(self, key):
pass self.mode = "normal"
self.popup = (action_handler, question) self.popup = (action_handler, question)
def spawn_popup(self, action_handler, question): def spawn_popup(self, action_handler, question):
# on q press
self.modestack.append(self.mode)
self.mode = "popup" self.mode = "popup"
self.popup = (action_handler, question) self.popup = (action_handler, question)
async def handle_key(self, key, redraw = True): async def handle_key(self, key):
if self.mode == "popupmessage":
self.mode = self.modestack.pop()
if not self.ready: if not self.ready:
return return
if key == "RESIZE": if key == "RESIZE":
await self.drawtool.resize() await self.drawtool.resize()
return return
if self.macro_recording:
if key != "q":
self.macro_sequence.append(key)
if self.mode == "search": if self.mode == "search":
if key == "ESCAPE" or key == "RETURN": if key == "ESCAPE" or key == "RETURN":
self.mode = "normal" self.mode = "normal"
@@ -376,41 +359,19 @@ class MainView():
self.vimline_box = "" self.vimline_box = ""
elif key == "RETURN" or key == "y": elif key == "RETURN" or key == "y":
await self.send_message() await self.send_message()
elif key == "Q":
await self.quit()
elif key == "q": elif key == "q":
if self.macro_recording == None: await self.quit()
# start macro recording #elif key == "D":
async def record_macro(self, key): # for i in range(10):
if "a" < key.lower() < "z": # self.select_prev_chat()
self.macro_recording = key #elif key == "d":
self.popup_message(f"recording into {key}") # for i in range(10):
else: # self.select_next_chat()
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?")
elif key == "C": elif key == "C":
self.select_prev_chat() self.select_prev_chat()
elif key == "c": elif key == "c":
self.select_next_chat() self.select_next_chat()
elif key == "E": elif key == "e":
self.text_emojis ^= True self.text_emojis ^= True
elif key == "R": elif key == "R":
await self.mark_read() await self.mark_read()
@@ -436,16 +397,6 @@ class MainView():
self.spawn_popup(action_handler, question) self.spawn_popup(action_handler, question)
await self.drawtool.redraw() 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": elif key == "r":
if self.command_box: if self.command_box:
try: try:
@@ -485,65 +436,53 @@ class MainView():
self.drawtool.show_indices ^= True self.drawtool.show_indices ^= True
elif self.mode == "popup": elif self.mode == "popup":
action, _ = self.popup action, _ = self.popup
# I think this could break
self.mode = self.modestack.pop()
await action(self, key) 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": elif self.mode == "insert":
if key == "ESCAPE": if key == "ESCAPE":
self.mode = "normal" self.mode = "normal"
elif key == "LEFT": elif key == "LEFT":
self.insert_move_left() self.insert_move_left()
elif key == "UP":
self.drawtool.move_cursor_up()
elif key == "RIGHT": elif key == "RIGHT":
self.insert_move_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": elif key == "BACKSPACE":
self.inputs = self.inputs[0:-1] try:
elif key == "RETURN": if len(self.inputs) > 0:
self.inputs += "\n" 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: 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 = "" self.command_box = ""
if redraw: await self.drawtool.redraw()
await self.drawtool.redraw()
def insert_move_left(self): 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): def insert_move_right(self):
self.inputs_cursor = min(len(self.inputs), self.cursor + 1) self.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()