339 lines
15 KiB
Python
339 lines
15 KiB
Python
import curses
|
|
import math
|
|
from telethon.utils import get_display_name
|
|
import emojis
|
|
import os
|
|
import datetime
|
|
import time
|
|
import config
|
|
import textwrap
|
|
from tttcutils import show_stacktrace, debug
|
|
|
|
class Drawtool():
|
|
def __init__(self, main_view):
|
|
self.client = main_view.client
|
|
self.main_view = main_view
|
|
self.stdscr = main_view.stdscr
|
|
|
|
self.chat_ratio = 0.3
|
|
|
|
self.H, self.W = self.stdscr.getmaxyx()
|
|
self.recompute_dimensions()
|
|
|
|
self.chat_rows = 5
|
|
self.chat_offset_fraction = 0.3
|
|
self.single_chat_fraction = 0.3
|
|
self.dialog_fraction = 0.25
|
|
self.show_indices = False
|
|
|
|
def recompute_dimensions(self):
|
|
self.min_input_lines = int(0.1 * self.H)
|
|
self.max_input_lines = int(0.3 * self.H)
|
|
try:
|
|
self.input_lines = min(self.max_input_lines, max(len(self._get_input_lines(self.main_view.inputs, width = self.W - 4)), self.min_input_lines))
|
|
except:
|
|
show_stacktrace()
|
|
|
|
self.chats_height = self.H - self.input_lines - 3
|
|
self.chats_width = int(self.W * self.chat_ratio)
|
|
self.chats_num = self.chats_height // 3
|
|
|
|
async def resize(self):
|
|
self.H, self.W = self.stdscr.getmaxyx()
|
|
self.recompute_dimensions()
|
|
await self.redraw()
|
|
|
|
def _get_input_lines(self, s, width = 50):
|
|
# in order to preserve user made linebreaks:
|
|
wrapper = textwrap.TextWrapper()
|
|
wrapper.width = width
|
|
wrapper.replace_whitespace = False
|
|
wrapper.drop_whitespace = False
|
|
|
|
lines = s.split("\n")
|
|
newlines = []
|
|
for line in lines:
|
|
if line:
|
|
newlines += wrapper.wrap(line)
|
|
else:
|
|
newlines += [""]
|
|
return newlines
|
|
#return textwrap.wrap(s, width = width)
|
|
|
|
def _get_cursor_position(self, s, width = 50):
|
|
lines = self._get_input_lines(s, width = width)[-self.input_lines:]
|
|
if not lines:
|
|
return (0, 0)
|
|
x = len(lines[-1])
|
|
y = len(lines) - 1
|
|
return y, x
|
|
|
|
async def redraw(self):
|
|
self.recompute_dimensions()
|
|
|
|
self.stdscr.clear()
|
|
self.draw_chats()
|
|
await self.draw_messages()
|
|
if self.main_view.mode == "search":
|
|
if self.main_view.search_result == []:
|
|
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"]:
|
|
_, question = self.main_view.popup
|
|
self.stdscr.addstr(self.H - 1, 0, question)
|
|
elif self.main_view.mode == "vimmode":
|
|
self.stdscr.addstr(self.H - 1, 0, ":" + self.main_view.vimline_box)
|
|
else:
|
|
self.stdscr.addstr(self.H - 1, 0, "--" + self.main_view.mode.upper() + "--")
|
|
self.stdscr.addstr(self.H - 1, int(self.W * 2/3), self.main_view.command_box[:8])
|
|
|
|
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"]:
|
|
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)
|
|
elif self.main_view.mode == "search":
|
|
curses.curs_set(1)
|
|
self.stdscr.move(self.H - 1, 1 + len(self.main_view.search_box))
|
|
elif self.main_view.mode == "vimmode":
|
|
curses.curs_set(1)
|
|
self.stdscr.move(self.H - 1, 1 + len(self.main_view.vimline_box))
|
|
else:
|
|
curses.curs_set(0)
|
|
self.stdscr.refresh()
|
|
|
|
def format(self, text, attributes = None, width = None, alignment = "left", inner_alignment = "left", truncation = "..."):
|
|
if attributes == None:
|
|
attributes = self.main_view.colors["default"]
|
|
if width == None:
|
|
width = len(text)
|
|
return {
|
|
"text": text,
|
|
"attributes": attributes,
|
|
"width": width,
|
|
"alignment": alignment,
|
|
"inner_alignment": inner_alignment,
|
|
"truncation": truncation
|
|
}
|
|
|
|
def _datestring(self, date):
|
|
now = datetime.datetime.now().astimezone()
|
|
today = datetime.date.today()
|
|
if (now - date).total_seconds() < 20*3600:
|
|
out = date.strftime(f"%I:%M %p")
|
|
return out
|
|
if (now - date).total_seconds() < 6*86400:
|
|
return date.strftime("%A")
|
|
return date.strftime("%d.%m.%y")
|
|
|
|
def draw_frame(self, yoff, xoff, h, w, chars = "││──┌┐└┘", attributes = 0):
|
|
for i in range(h):
|
|
self.stdscr.addstr(yoff + i, xoff, chars[0], attributes)
|
|
for i in range(h):
|
|
self.stdscr.addstr(yoff + i, xoff + w, chars[1], attributes)
|
|
for i in range(w):
|
|
self.stdscr.addstr(yoff, xoff + i, chars[2], attributes)
|
|
for i in range(w):
|
|
self.stdscr.addstr(yoff + h, xoff + i, chars[3], attributes)
|
|
self.stdscr.addstr(yoff, xoff, chars[4], attributes)
|
|
self.stdscr.addstr(yoff, xoff + w, chars[5], attributes)
|
|
self.stdscr.addstr(yoff + h, xoff, chars[6], attributes)
|
|
self.stdscr.addstr(yoff + h, xoff + w, chars[7], attributes)
|
|
|
|
def draw_chats(self):
|
|
selected_chat_index = self.main_view.selected_chat - self.main_view.selected_chat_offset
|
|
offset = self.main_view.selected_chat_offset
|
|
try:
|
|
self.draw_frame(0,0, self.chats_height , self.chats_width)
|
|
index = 0
|
|
y = 1
|
|
for index in range(self.chats_num):
|
|
dialog = self.main_view.dialogs[index + offset]
|
|
message = dialog["messages"][0] if "messages" in dialog else dialog["dialog"].message
|
|
message_string = message.text if message.text else "[Non-text object]"
|
|
if self.main_view.text_emojis:
|
|
message_string = emojis.decode(message_string)
|
|
chat_name = get_display_name(dialog["dialog"].entity)
|
|
from_string = get_display_name(message.sender)
|
|
unread = dialog["unread_count"]
|
|
unread_string = f"({unread} new)" if unread else ""
|
|
date = dialog["dialog"].date
|
|
date = date.astimezone()
|
|
date_string = self._datestring(date)
|
|
pinned = "* " if dialog["dialog"].pinned else " "
|
|
selected = selected_chat_index == index
|
|
|
|
self.draw_text(
|
|
[
|
|
self.format("o" if dialog["online"] else " ", attributes = self.main_view.colors["secondary"]),
|
|
self.format(chat_name, attributes = self.main_view.colors["primary"] | curses.A_STANDOUT if selected else curses.A_BOLD, width = int(0.5 * self.chats_width)),
|
|
self.format(f" {str(index)} " if self.show_indices else "", attributes = self.main_view.colors["standout"]),
|
|
self.format(unread_string, attributes = self.main_view.colors["error"], alignment = "right"),
|
|
self.format(date_string, alignment = "right", attributes = self.main_view.colors["primary"]),
|
|
],
|
|
y, 2, maxwidth = self.chats_width - 2)
|
|
self.draw_text(
|
|
[
|
|
self.format(f"{from_string}:"),
|
|
self.format(message_string, width = self.chats_width - len(f"{from_string}: ") - 3)
|
|
],
|
|
y + 1, 2, maxwidth = self.chats_width - 2)
|
|
y += 3
|
|
index += 1
|
|
except Exception:
|
|
show_stacktrace()
|
|
|
|
def draw_text(self, format_dicts, y_off = 0, x_off = 0, screen = None, maxwidth = None, separator = " "):
|
|
if maxwidth == None:
|
|
maxwidth = sum(format_dict["width"] for format_dict in format_dicts)
|
|
if screen == None:
|
|
screen = self.stdscr
|
|
left = [ x for x in format_dicts if x["alignment"] == "left" ]
|
|
right = list(reversed([ x for x in format_dicts if x["alignment"] == "right" ]))
|
|
center = [ x for x in format_dicts if x["alignment"] == "center" ]
|
|
entries = [ (x, "left") for x in left ] + [ (x, "right") for x in right ] + [ (x, "center") for x in center ]
|
|
x_left = 0
|
|
x_right = maxwidth -1
|
|
for (format_dict, alignment) in entries:
|
|
text = format_dict["text"]
|
|
text = text.replace("\n", "")
|
|
attributes = format_dict["attributes"]
|
|
width = format_dict["width"]
|
|
inner_alignment = format_dict["inner_alignment"]
|
|
truncation = format_dict["truncation"]
|
|
# TODO: make this split preferrably at spaces and not show linebreaks
|
|
display_text = text.split("\n")[0]
|
|
if len(display_text) > width:
|
|
if truncation:
|
|
# TODO: make this split preferrably at spaces and not show linebreaks
|
|
display_text = display_text[:width - len(truncation)] + truncation
|
|
else:
|
|
display_text = display_text[:width]
|
|
rljust = " " * (width - len(display_text))
|
|
|
|
if alignment == "left":
|
|
if inner_alignment == "left":
|
|
screen.addstr(y_off, x_off + x_left, display_text, attributes)
|
|
x_left += len(display_text)
|
|
if rljust:
|
|
screen.addstr(y_off, x_off + x_left, rljust)
|
|
x_left += len(rljust)
|
|
elif inner_alignment == "right":
|
|
if rljust:
|
|
screen.addstr(y_off, x_off + x_left, rljust)
|
|
x_left += len(rljust)
|
|
screen.addstr(y_off, x_off + x_left, display_text, attributes)
|
|
x_left += len(display_text)
|
|
if left and format_dict != left[-1]:
|
|
screen.addstr(y_off, x_off + x_left, separator)
|
|
x_left += len(separator)
|
|
elif alignment == "right":
|
|
if inner_alignment == "left":
|
|
x_right -= len(text)
|
|
self.stdscr.addstr(y_off, x_off + x_right, text, attributes)
|
|
x_right -= len(rljust)
|
|
self.stdscr.addstr(y_off, x_off + x_right, rljust)
|
|
elif inner_alignment == "right":
|
|
x_right -= len(rljust)
|
|
self.stdscr.addstr(y_off, x_off + x_right, rljust)
|
|
x_right -= len(text)
|
|
self.stdscr.addstr(y_off, x_off + x_right, text, attributes)
|
|
if right and format_dict != right[-1]:
|
|
x_right -= len(separator)
|
|
self.stdscr.addstr(y_off, x_off + x_right, separator)
|
|
elif alignment == "center":
|
|
self.stdscr.addstr(y_off, maxwidth // 2 - len(display_text) // 2, display_text, attributes)
|
|
|
|
|
|
|
|
|
|
def draw_message(self, main_view, message, chat_idx):
|
|
maxtextwidth = int(self.single_chat_fraction * self.W) - 2
|
|
lines = []
|
|
if message.text:
|
|
message_lines = message.text.split("\n")
|
|
for message_line in message_lines:
|
|
if main_view.text_emojis:
|
|
message_line = emojis.decode(message_line)
|
|
if message_line == "":
|
|
lines += [""]
|
|
else:
|
|
lines += [
|
|
message_line[maxtextwidth * i: maxtextwidth*i+maxtextwidth]
|
|
for i in range(int(math.ceil(len(message_line)/maxtextwidth)))
|
|
]
|
|
if message.media:
|
|
media_type = message.media.to_dict()["_"]
|
|
if media_type == "MessageMediaPhoto":
|
|
media_type = "Photo"
|
|
elif media_type == "MessageMediaDocument":
|
|
atts = message.media.document.attributes
|
|
filename = [ x for x in atts if x.to_dict()["_"] == "DocumentAttributeFilename" ]
|
|
if filename:
|
|
filename = filename[0].to_dict()["file_name"]
|
|
media_type = f"Document ({filename})"
|
|
else:
|
|
media_type = f"Document ({message.media.document.mime_type})"
|
|
lines += [ f"[{media_type}]" ]
|
|
|
|
reply = ""
|
|
if message.is_reply:
|
|
reply_id = message.reply_to_msg_id
|
|
reply = " r?? "
|
|
for idx2, message2 in enumerate(main_view.dialogs[main_view.selected_chat]["messages"]):
|
|
if message2.id == reply_id:
|
|
reply = f"r{idx2:02d}"
|
|
break
|
|
|
|
from_message = message
|
|
from_user = "You" if message.out else get_display_name(from_message.sender)
|
|
via_user = f" via {get_display_name(from_message.forward.sender)}" if message.forward else ""
|
|
user_string = f"{from_user}{via_user} "
|
|
out = []
|
|
if message.out:
|
|
out.append(f"{chat_idx} {user_string}{self._datestring(message.date.astimezone())}".rjust(maxtextwidth))
|
|
for idx, text in enumerate(lines):
|
|
out.append(text.rjust(maxtextwidth - 4))
|
|
#out.append(f"{chat_idx} {message.date.hour}:{message.date.minute:02d}".rjust(maxtextwidth) + ".")
|
|
if message.is_reply:
|
|
out.append(reply)
|
|
else:
|
|
out.append(f"{chat_idx} {user_string}{self._datestring(message.date.astimezone())}")
|
|
for idx, text in enumerate(lines):
|
|
out.append(" " + text)
|
|
if message.is_reply:
|
|
out.append(reply)
|
|
return (out, message)
|
|
|
|
async def load_messages(self, chat_index):
|
|
main_view = self.main_view
|
|
index = chat_index
|
|
if not "messages" in main_view.dialogs[index]:
|
|
temp = await main_view.client.get_messages(main_view.dialogs[index]["dialog"], 50)
|
|
main_view.dialogs[index]["messages"] = [ message for message in temp ]
|
|
|
|
async def draw_messages(self, offset = 0):
|
|
main_view = self.main_view
|
|
await self.load_messages(main_view.selected_chat)
|
|
messages = main_view.dialogs[main_view.selected_chat]["messages"]
|
|
max_rows = self.H - self.input_lines - 3 - 1
|
|
lines = []
|
|
chat_count = 0
|
|
while len(lines) < max_rows + offset and chat_count < len(messages):
|
|
text, message = self.draw_message(main_view, messages[chat_count], chat_count)
|
|
for line in reversed(text):
|
|
lines.append((line, message))
|
|
lines.append(("",message))
|
|
chat_count += 1
|
|
|
|
for i in range(min(len(lines)-offset, max_rows)):
|
|
text, message = lines[i + offset]
|
|
if message.out:
|
|
self.stdscr.addstr(max_rows - i, int(self.W * (1-self.single_chat_fraction) - 4) + 2, text)
|
|
else:
|
|
self.stdscr.addstr(max_rows - i, int(self.W * self.chat_offset_fraction) + 2, text)
|
|
self.draw_frame(0, self.chats_width + 1, self.chats_height, self.W - self.chats_width - 2)
|