tfc-mirror/src/rx/windows.py

328 lines
13 KiB
Python

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
"""
Copyright (C) 2013-2017 Markus Ottela
This file is part of TFC.
TFC is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.
TFC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with TFC. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import sys
import textwrap
import typing
from datetime import datetime
from typing import Dict, Generator, Iterable, List, Tuple
from src.common.exceptions import FunctionReturn
from src.common.misc import get_terminal_width
from src.common.output import c_print, clear_screen, print_on_previous_line
from src.common.statics import *
if typing.TYPE_CHECKING:
from src.common.db_contacts import Contact, ContactList
from src.common.db_groups import GroupList
from src.common.db_settings import Settings
from src.rx.packet import PacketList
class RxWindow(Iterable):
"""RxWindow is an ephemeral message log for contact or group.
In addition, command history and file transfers have
their own windows, accessible with separate commands.
"""
def __init__(self,
uid: str,
contact_list: 'ContactList',
group_list: 'GroupList',
settings: 'Settings',
packet_list: 'PacketList' = None) -> None:
"""Create a new RxWindow object."""
self.uid = uid
self.contact_list = contact_list
self.group_list = group_list
self.settings = settings
self.packet_list = packet_list
self.is_active = False
self.group_msg_id = os.urandom(GROUP_MSG_ID_LEN)
self.window_contacts = [] # type: List[Contact]
self.message_log = [] # type: List[Tuple[datetime, str, str, bytes, bool]]
self.handle_dict = dict() # type: Dict[str, str]
self.previous_msg_ts = datetime.now()
self.unread_messages = 0
if self.uid == LOCAL_ID:
self.type = WIN_TYPE_COMMAND
self.type_print = 'system messages'
self.window_contacts = [self.contact_list.get_contact(LOCAL_ID)]
self.name = self.type_print
elif self.uid == WIN_TYPE_FILE:
self.type = WIN_TYPE_FILE
self.packet_list = packet_list
elif self.uid in self.contact_list.get_list_of_accounts():
self.type = WIN_TYPE_CONTACT
self.type_print = 'contact'
self.window_contacts = [self.contact_list.get_contact(uid)]
self.name = self.contact_list.get_contact(uid).nick
elif self.uid in self.group_list.get_list_of_group_names():
self.type = WIN_TYPE_GROUP
self.type_print = 'group'
self.window_contacts = self.group_list.get_group_members(self.uid)
self.name = self.group_list.get_group(self.uid).name
else:
raise FunctionReturn(f"Invalid window '{uid}'")
def __len__(self) -> int:
"""Return number of message tuples in message log."""
return len(self.message_log)
def __iter__(self) -> Generator:
"""Iterate over window's message log."""
yield from self.message_log
def add_contacts(self, accounts: List[str]) -> None:
"""Add contact objects to window."""
self.window_contacts += [self.contact_list.get_contact(a) for a in accounts
if not self.has_contact(a) and self.contact_list.has_contact(a)]
def remove_contacts(self, accounts: List[str]) -> None:
"""Remove contact objects from window."""
to_remove = set(accounts) & set([m.rx_account for m in self.window_contacts])
if to_remove:
self.window_contacts = [c for c in self.window_contacts if c.rx_account not in to_remove]
def reset_window(self) -> None:
"""Reset window."""
self.message_log = []
def has_contact(self, account: str) -> bool:
"""Return True if contact with specified account is in window, else False."""
return any(c.rx_account == account for c in self.window_contacts)
def create_handle_dict(self, message_log: List[Tuple['datetime', str, str, bytes, bool]] = None) -> None:
"""Pre-generate {account: handle} dictionary.
This allows `self.print()` to indent accounts and nicks without
having to loop over entire message list for every message.
"""
accounts = set(c.rx_account for c in self.window_contacts)
if message_log is not None:
accounts |= set(a for ts, ma, a, o, w in message_log)
for a in accounts:
self.handle_dict[a] = self.contact_list.get_contact(a).nick if self.contact_list.has_contact(a) else a
def get_handle(self, time_stamp: 'datetime', account: str, origin: bytes, whisper: bool=False) -> str:
"""Returns indented handle complete with headers and trailers."""
if self.type == WIN_TYPE_COMMAND:
handle = "-!- "
else:
handle = self.handle_dict[account] if origin == ORIGIN_CONTACT_HEADER else "Me"
handles = list(self.handle_dict.values()) + ["Me"]
indent = len(max(handles, key=len)) - len(handle) if self.is_active else 0
handle = indent * ' ' + handle
handle = time_stamp.strftime('%H:%M') + ' ' + handle
if not self.is_active:
handle += {WIN_TYPE_GROUP: f" (group {self.name})",
WIN_TYPE_CONTACT: f" (private message)" }.get(self.type, '')
if self.type != WIN_TYPE_COMMAND:
if whisper:
handle += " (whisper)"
handle += ": "
return handle
def print(self, msg_tuple: Tuple['datetime', str, str, bytes, bool], file=None) -> None:
"""Print new message to window."""
bold_on, bold_off, f_name = (BOLD_ON, NORMAL_TEXT, sys.stdout) if file is None else ('', '', file)
ts, message, account, origin, whisper = msg_tuple
if not self.is_active and not self.settings.new_message_notify_preview and self.type != WIN_TYPE_COMMAND:
message = BOLD_ON + f"{self.unread_messages + 1} unread message{'s' if self.unread_messages > 1 else ''}" + NORMAL_TEXT
handle = self.get_handle(ts, account, origin, whisper)
wrapper = textwrap.TextWrapper(get_terminal_width(), initial_indent=handle, subsequent_indent=len(handle)*' ')
wrapped = wrapper.fill(message)
if wrapped == '':
wrapped = handle
wrapped = bold_on + wrapped[:len(handle)] + bold_off + wrapped[len(handle):]
if self.is_active:
if self.previous_msg_ts.date() != ts.date():
print(bold_on + f"00:00 -!- Day changed to {str(ts.date())}" + bold_off, file=f_name)
print(wrapped, file=f_name)
else:
self.unread_messages += 1
if (self.type == WIN_TYPE_CONTACT and self.contact_list.get_contact(account).notifications) \
or (self.type == WIN_TYPE_GROUP and self.group_list.get_group(self.uid).notifications) \
or (self.type == WIN_TYPE_COMMAND):
if len(wrapped.split('\n')) > 1:
# Preview only first line of long message
print(wrapped.split('\n')[0][:-3] + "...")
else:
print(wrapped)
print_on_previous_line(delay=self.settings.new_message_notify_duration, flush=True)
self.previous_msg_ts = ts
def add_new(self,
timestamp: 'datetime',
message: str,
account: str = LOCAL_ID,
origin: bytes = ORIGIN_USER_HEADER,
output: bool = False,
whisper: bool = False) -> None:
"""Add message tuple to message log and optionally print it."""
msg_tuple = (timestamp, message, account, origin, whisper)
self.message_log.append(msg_tuple)
self.handle_dict[account] = (self.contact_list.get_contact(account).nick
if self.contact_list.has_contact(account) else account)
if output:
self.print(msg_tuple)
def redraw(self, file=None) -> None:
"""Print all messages received to window."""
self.unread_messages = 0
if file is None:
clear_screen()
if self.message_log:
self.previous_msg_ts = self.message_log[0][0]
self.create_handle_dict(self.message_log)
for msg_tuple in self.message_log:
self.print(msg_tuple, file)
else:
c_print(f"This window for {self.name} is currently empty.", head=1, tail=1)
def redraw_file_win(self) -> None:
"""Draw file transmission window progress bars."""
# Columns
c1 = ['File name']
c2 = ['Size']
c3 = ['Sender']
c4 = ['Complete']
for i, p in enumerate(self.packet_list):
if p.type == FILE and len(p.assembly_pt_list) > 0:
c1.append(p.name)
c2.append(p.size)
c3.append(p.contact.nick)
c4.append(f"{len(p.assembly_pt_list) / p.packets * 100:.2f}%")
if not len(c1) > 1:
c_print("No file transmissions currently in progress.", head=1, tail=1)
print_on_previous_line(reps=3, delay=0.1)
return None
lst = []
for name, size, sender, percent, in zip(c1, c2, c3, c4):
lst.append('{0:{1}} {2:{3}} {4:{5}} {6:{7}}'.format(
name, max(len(v) for v in c1) + CONTACT_LIST_INDENT,
size, max(len(v) for v in c2) + CONTACT_LIST_INDENT,
sender, max(len(v) for v in c3) + CONTACT_LIST_INDENT,
percent, max(len(v) for v in c4) + CONTACT_LIST_INDENT))
lst.insert(1, get_terminal_width() * '')
print('\n' + '\n'.join(lst) + '\n')
print_on_previous_line(reps=len(lst)+2, delay=0.1)
class WindowList(Iterable):
"""WindowList manages a list of Window objects."""
def __init__(self,
settings: 'Settings',
contact_list: 'ContactList',
group_list: 'GroupList',
packet_list: 'PacketList') -> None:
"""Create a new WindowList object."""
self.settings = settings
self.contact_list = contact_list
self.group_list = group_list
self.packet_list = packet_list
self.active_win = None # type: RxWindow
self.windows = [RxWindow(uid, self.contact_list, self.group_list, self.settings, self.packet_list)
for uid in ([WIN_TYPE_FILE]
+ self.contact_list.get_list_of_accounts()
+ self.group_list.get_list_of_group_names())]
if self.contact_list.has_local_contact():
self.select_rx_window(LOCAL_ID)
def __len__(self) -> int:
"""Return number of windows in window list."""
return len(self.windows)
def __iter__(self) -> Generator:
"""Iterate over window list."""
yield from self.windows
def get_group_windows(self) -> List[RxWindow]:
"""Return list of group windows."""
return [w for w in self.windows if w.type == WIN_TYPE_GROUP]
def has_window(self, uid: str) -> bool:
"""Return True if window with matching UID exists, else False."""
return uid in [w.uid for w in self.windows]
def remove_window(self, uid: str) -> None:
"""Remove window based on it's UID."""
for i, w in enumerate(self.windows):
if uid == w.uid:
del self.windows[i]
break
def select_rx_window(self, uid: str) -> None:
"""Select new active window."""
if self.active_win is not None:
self.active_win.is_active = False
self.active_win = self.get_window(uid)
self.active_win.is_active = True
if self.active_win.type == WIN_TYPE_FILE:
self.active_win.redraw_file_win()
else:
self.active_win.redraw()
def get_local_window(self) -> 'RxWindow':
"""Return command window."""
return self.get_window(LOCAL_ID)
def get_window(self, uid: str) -> 'RxWindow':
"""Return window that matches the specified UID.
Create window if it does not exist.
"""
if not self.has_window(uid):
self.windows.append(RxWindow(uid, self.contact_list, self.group_list, self.settings, self.packet_list))
return next(w for w in self.windows if w.uid == uid)