def print_settings(self) -> None: """\ Print list of settings, their current and default values, and setting descriptions. """ desc_d = {"serial_baudrate": "The speed of serial interface in bauds per second", "serial_error_correction": "Number of byte errors serial datagrams can recover from"} # Columns c1 = ['Serial interface setting'] c2 = ['Current value'] c3 = ['Default value'] c4 = ['Description'] terminal_width = get_terminal_width() description_indent = 64 if terminal_width < description_indent + 1: raise SoftError("Error: Screen width is too small.") # Populate columns with setting data for key in desc_d: c1.append(key) c2.append(str(self.__getattribute__(key))) c3.append(str(self.defaults[key])) description = desc_d[key] wrapper = textwrap.TextWrapper(width=max(1, (terminal_width - description_indent))) desc_lines = wrapper.fill(description).split('\n') desc_string = desc_lines[0] for line in desc_lines[1:]: desc_string += '\n' + description_indent * ' ' + line if len(desc_lines) > 1: desc_string += '\n' c4.append(desc_string) # Calculate column widths c1w, c2w, c3w = [max(len(v) for v in column) + SETTINGS_INDENT for column in [c1, c2, c3]] # Align columns by adding whitespace between fields of each line lines = [f'{f1:{c1w}} {f2:{c2w}} {f3:{c3w}} {f4}' for f1, f2, f3, f4 in zip(c1, c2, c3, c4)] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the settings print('\n' + '\n'.join(lines) + '\n')
def pwd_prompt(message: str, # Prompt message repeat: bool = False # When True, prints corner chars for the second box ) -> str: # Password from user """Prompt the user to enter a password. The getpass library ensures the password is not echoed on screen when it is typed. """ l, r = ('├', '┤') if repeat else ('┌', '┐') terminal_w = get_terminal_width() input_space = len(' c ') # `c` is where the caret sits upper_line = ( l + (len(message) + input_space) * '─' + r ).center(terminal_w) title_line = ('│' + message + input_space * ' ' + '│').center(terminal_w) lower_line = ('└' + (len(message) + input_space) * '─' + '┘').center(terminal_w) terminal_width_check(len(upper_line)) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') user_input = getpass.getpass(indent * ' ' + f'│ {message}') return user_input
def draw_frame( argv: str, # Arguments for the simulator position/orientation message: str, # Status message to print high: bool = False # Determines the signal's state (high/low) ) -> None: """Draw a data diode animation frame.""" l, indicator, arrow, r = { NCDCLR: ('Rx', '<', '←', 'Tx'), SCNCLR: ('Tx', '>', '→', 'Rx'), NCDCRL: ('Tx', '>', '→', 'Rx'), SCNCRL: ('Rx', '<', '←', 'Tx') }[argv] indicator = indicator if high else ' ' arrow = arrow if message != IDLE else ' ' terminal_width = get_terminal_width() def c_print(string: str) -> None: """Print string on the center of the screen.""" print(string.center(terminal_width)) print('\n' * ((get_terminal_height() // 2) - DD_OFFSET_FROM_CENTER)) c_print(message) c_print(arrow) c_print("────╮ " + ' ' + " ╭────") c_print(f" {l} │ " + indicator + f" │ {r} ") c_print("────╯ " + ' ' + " ╰────")
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)
def print_logs(message_list: List[MsgTuple], export: bool, msg_to_load: int, window: Union['TxWindow', 'RxWindow'], contact_list: 'ContactList', group_list: 'GroupList', settings: 'Settings' ) -> None: """Print list of logged messages to screen or export them to file.""" terminal_width = get_terminal_width() system, m_dir = {TX: ("Transmitter", "sent to"), RX: ("Receiver", "to/from")}[settings.software_operation] f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent " title = textwrap.fill(f"Log file of {subset}message(s) {m_dir} {window.type} {window.name}", terminal_width) packet_list = PacketList(settings, contact_list) log_window = RxWindow(window.uid, contact_list, group_list, settings, packet_list) log_window.is_active = True log_window.message_log = message_list if message_list: if not export: clear_screen() print(title, file=f_name) print(terminal_width * '═', file=f_name) log_window.redraw( file=f_name) print("<End of log file>\n", file=f_name) else: raise FunctionReturn(f"No logged messages for {window.type} '{window.name}'.", head_clear=True) if export: f_name.close()
def phase( string: str, # Description of the phase done: bool = False, # When True, uses string as the phase completion message head: int = 0, # Number of inserted new lines before print tail: int = 0, # Number of inserted new lines after print offset: int = 4, # Offset of phase string from center to left delay: float = 0.5 # Duration of phase completion message ) -> None: """Print the name of the next phase. The notification of completion of the phase is printed on the same line as the phase message. """ print_spacing(head) if string == DONE or done: print(string) time.sleep(delay) else: string += '... ' indent = ((get_terminal_width() - (len(string) + offset)) // 2) * ' ' print(indent + string, end='', flush=True) print_spacing(tail)
def ask_confirmation_code(source: str # The system the confirmation code is displayed by ) -> str: # The confirmation code entered by the user """\ Ask the user to input confirmation code from Source Computer to verify local key has been installed. """ title = f"Enter confirmation code (from {source}): " input_space = len(' ff ') upper_line = '┌' + (len(title) + input_space) * '─' + '┐' title_line = '│' + title + input_space * ' ' + '│' lower_line = '└' + (len(title) + input_space) * '─' + '┘' terminal_w = get_terminal_width() upper_line = upper_line.center(terminal_w) title_line = title_line.center(terminal_w) lower_line = lower_line.center(terminal_w) terminal_width_check(len(upper_line)) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') return input(indent * ' ' + f'│ {title}')
def ask_confirmation_code() -> str: """\ Ask user to input confirmation code from RxM to verify that local key has been installed. Input box accommodates room for the 'resend' command. """ title = "Enter confirmation code (from RxM): " space = len(' resend ') upper_line = ('┌' + (len(title) + space) * '─' + '┐') title_line = ('│' + title + space * ' ' + '│') lower_line = ('└' + (len(title) + space) * '─' + '┘') terminal_w = get_terminal_width() upper_line = upper_line.center(terminal_w) title_line = title_line.center(terminal_w) lower_line = lower_line.center(terminal_w) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') return input(indent * ' ' + '│ {}'.format(title))
def phase(string: str, done: bool = False, head: int = 0, offset: int = 2) -> None: """Print name of next phase. Message about completion will be printed on same line. :param string: String to be printed :param done: When True, allows custom string to notify completion :param head: Number of inserted new lines before print :param offset: Offset of message from center to left :return: None """ for _ in range(head): print('') if string == DONE or done: print(string) time.sleep(0.5) else: string = '{}... '.format(string) indent = ((get_terminal_width() - (len(string) + offset)) // 2) * ' ' print(indent + string, end='', flush=True)
def print_contacts(self) -> None: """Print list of contacts.""" # Columns c1 = ['Contact'] c2 = ['Logging'] c3 = ['Notify'] c4 = ['Files '] c5 = ['Key Ex'] c6 = ['Account'] for c in self.get_list_of_contacts(): c1.append(c.nick) c2.append('Yes' if c.log_messages else 'No') c3.append('Yes' if c.notifications else 'No') c4.append('Accept' if c.file_reception else 'Reject') c5.append('PSK' if c.tx_fingerprint == bytes(FINGERPRINT_LEN) else 'X25519') c6.append(c.rx_account) lst = [] for nick, log_setting, notify_setting, file_rcv_setting, key_exchange, account in zip(c1, c2, c3, c4, c5, c6): lst.append('{0:{1}} {2:{3}} {4:{5}} {6:{7}} {8:{9}} {10}'.format( nick, max(len(v) for v in c1) + CONTACT_LIST_INDENT, log_setting, max(len(v) for v in c2) + CONTACT_LIST_INDENT, notify_setting, max(len(v) for v in c3) + CONTACT_LIST_INDENT, file_rcv_setting, max(len(v) for v in c4) + CONTACT_LIST_INDENT, key_exchange, max(len(v) for v in c5) + CONTACT_LIST_INDENT, account, max(len(v) for v in c6) + CONTACT_LIST_INDENT)) lst.insert(1, get_terminal_width() * '─') clear_screen() print('\n' + '\n'.join(lst) + '\n\n')
def pwd_prompt(message: str, second: bool = False) -> str: """Prompt user to enter a password. :param message: Prompt message :param second: When True, prints corner chars for second box :return: Password from user """ l, r = {False: ('┌', '┐'), True: ('├', '┤')}[second] upper_line = (l + (len(message) + 3) * '─' + r) title_line = ('│' + message + 3 * ' ' + '│') lower_line = ('└' + (len(message) + 3) * '─' + '┘') terminal_w = get_terminal_width() upper_line = upper_line.center(terminal_w) title_line = title_line.center(terminal_w) lower_line = lower_line.center(terminal_w) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') user_input = getpass.getpass(indent * ' ' + '│ {}'.format(message)) return user_input
def draw_frame(argv: str, # Arguments for simulator position/orientation message: str, # Status message to print high: bool = False # Determines the signal's state (high/low) ) -> None: """Draw a data diode animation frame.""" l, r, blink, arrow = dict(scnclr=('Tx', 'Rx', '>', '→'), scncrl=('Rx', 'Tx', '<', '←'), ncdclr=('Rx', 'Tx', '<', '←'), ncdcrl=('Tx', 'Rx', '>', '→'))[argv] arrow = arrow if message != 'Idle' else ' ' blink = blink if high else ' ' offset_from_center = 4 print(((get_terminal_height() // 2) - offset_from_center) * '\n') terminal_width = get_terminal_width() def c_print(msg: str) -> None: """Print string in the center of the screen.""" print(msg.center(terminal_width)) c_print(message) c_print(arrow) c_print( "────╮ " + ' ' + " ╭────" ) c_print(f" {l} │ " + blink + f" │ {r} ") c_print( "────╯ " + ' ' + " ╰────" )
def m_print( msg_list: Union[str, List[str]], # List of lines to print manual_proceed: bool = False, # Wait for user input before continuing bold: bool = False, # When True, prints the message in bold style center: bool = True, # When False, does not center message box: bool = False, # When True, prints a box around the message head_clear: bool = False, # When True, clears screen before printing message tail_clear: bool = False, # When True, clears screen after printing message (requires delay) delay: float = 0, # Delay before continuing max_width: int = 0, # Maximum width of message head: int = 0, # Number of new lines to print before the message tail: int = 0, # Number of new lines to print after the message ) -> None: """Print message to screen. The message automatically wraps if the terminal is too narrow to display the message. """ if isinstance(msg_list, str): msg_list = [msg_list] terminal_width = get_terminal_width() len_widest_msg, msg_list = split_too_wide_messages(box, max_width, msg_list, terminal_width) if box or center: # Insert whitespace around every line to make them equally long msg_list = [f'{m:^{len_widest_msg}}' for m in msg_list] if box: # Add box chars around the message msg_list = [f'│ {m} │' for m in msg_list] msg_list.insert(0, '┌' + (len_widest_msg + 2) * '─' + '┐') msg_list.append('└' + (len_widest_msg + 2) * '─' + '┘') # Print the message if head_clear: clear_screen() print_spacing(head) for message in msg_list: if center: message = message.center(terminal_width) if bold: message = BOLD_ON + message + NORMAL_TEXT print(message) print_spacing(tail) time.sleep(delay) if tail_clear: clear_screen() # Check if message needs to be manually dismissed if manual_proceed: input('') print_on_previous_line()
def print(self, msg_tuple: MsgTuple, file: Any = None) -> None: """Print a new message to the window.""" # Unpack tuple ts, message, onion_pub_key, origin, whisper, event_msg = msg_tuple # Determine handle handle = self.get_handle(ts, onion_pub_key, origin, whisper, event_msg) # Check if message content needs to be changed to privacy-preserving notification if not self.is_active and not self.settings.new_message_notify_preview and self.uid != WIN_UID_COMMAND: trailer = 's' if self.unread_messages > 0 else '' message = BOLD_ON + f"{self.unread_messages + 1} unread message{trailer}" + NORMAL_TEXT # Wrap message wrapper = textwrap.TextWrapper(width=get_terminal_width(), initial_indent=handle, subsequent_indent=len(handle) * ' ') wrapped = wrapper.fill(message) if wrapped == '': wrapped = handle # Add bolding unless export file is provided bold_on, bold_off, f_name = (BOLD_ON, NORMAL_TEXT, sys.stdout) if file is None else ('', '', file) 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: if onion_pub_key != WIN_UID_COMMAND: self.unread_messages += 1 if (self.type == WIN_TYPE_CONTACT and self.contact is not None and self.contact.notifications) \ or (self.type == WIN_TYPE_GROUP and self.group is not None and self.group.notifications) \ or (self.type == WIN_TYPE_COMMAND): lines = wrapped.split('\n') if len(lines) > 1: print(lines[0][:-1] + '…') # Preview only first line of the long message else: print(wrapped) print_on_previous_line( delay=self.settings.new_message_notify_duration, flush=True) self.previous_msg_ts = ts
def print_contacts(self) -> None: """Print the list of contacts. Neatly printed contact list allows easy contact management: It allows the user to check active logging, file reception and notification settings, as well as what key exchange was used and what is the state of that key exchange. The contact list also shows and what the account displayed by the Relay Program corresponds to what nick etc. """ # Initialize columns c1 = ['Contact'] c2 = ['Account'] c3 = ['Logging'] c4 = ['Notify'] c5 = ['Files '] c6 = ['Key Ex'] # Key exchange status dictionary kex_dict = { KEX_STATUS_PENDING: f"{ECDHE} (Pending)", KEX_STATUS_UNVERIFIED: f"{ECDHE} (Unverified)", KEX_STATUS_VERIFIED: f"{ECDHE} (Verified)", KEX_STATUS_NO_RX_PSK: f"{PSK} (No contact key)", KEX_STATUS_HAS_RX_PSK: PSK } # Populate columns with contact data for c in self.get_list_of_contacts(): c1.append(c.nick) c2.append(c.short_address) c3.append('Yes' if c.log_messages else 'No') c4.append('Yes' if c.notifications else 'No') c5.append('Accept' if c.file_reception else 'Reject') c6.append(kex_dict[c.kex_status]) # Calculate column widths c1w, c2w, c3w, c4w, c5w, = [ max(len(v) for v in column) + CONTACT_LIST_INDENT for column in [c1, c2, c3, c4, c5] ] # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}}{f2:{c2w}}{f3:{c3w}}{f4:{c4w}}{f5:{c5w}}{f6}' for f1, f2, f3, f4, f5, f6 in zip(c1, c2, c3, c4, c5, c6) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the contact list clear_screen() print('\n' + '\n'.join(lines) + '\n\n')
def yes( prompt: str, # Question to be asked abort: Optional[bool] = None, # Determines the return value of ^C and ^D head: int = 0, # Number of new lines to print before prompt tail: int = 0 # Number of new lines to print after prompt ) -> bool: # True/False depending on input """Prompt the user a question that is answered with yes/no.""" print_spacing(head) prompt = f"{prompt} (y/n): " input_space = len(' yes ') upper_line = '┌' + (len(prompt) + input_space) * '─' + '┐' title_line = '│' + prompt + input_space * ' ' + '│' lower_line = '└' + (len(prompt) + input_space) * '─' + '┘' terminal_w = get_terminal_width() upper_line = upper_line.center(terminal_w) title_line = title_line.center(terminal_w) lower_line = lower_line.center(terminal_w) indent = title_line.find('│') terminal_width_check(len(upper_line)) print(upper_line) while True: print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) try: user_input = input(indent * ' ' + f'│ {prompt}') except (EOFError, KeyboardInterrupt): if abort is None: raise print('') user_input = 'y' if abort else 'n' print_on_previous_line() if user_input == '': continue if user_input.lower() in ['y', 'yes']: print(indent * ' ' + f'│ {prompt}Yes │\n') print_spacing(tail) return True if user_input.lower() in ['n', 'no']: print(indent * ' ' + f'│ {prompt}No │\n') print_spacing(tail) return False
def print_groups(self) -> None: """Print list of groups.""" # Columns c1 = ['Group '] c2 = ['Logging'] c3 = ['Notify'] c4 = ['Members'] for g in self.groups: c1.append(g.name) c2.append('Yes' if g.log_messages else 'No') c3.append('Yes' if g.notifications else 'No') if g.has_members(): m_indent = max(len(g.name) for g in self.groups) + 28 m_string = ', '.join(sorted([m.nick for m in g.members])) wrapper = textwrap.TextWrapper( width=max(1, (get_terminal_width() - m_indent))) mem_lines = wrapper.fill(m_string).split('\n') f_string = mem_lines[0] + '\n' for l in mem_lines[1:]: f_string += m_indent * ' ' + l + '\n' c4.append(f_string) else: c4.append("<Empty group>\n") lst = [] for name, log_setting, notify_setting, members in zip(c1, c2, c3, c4): lst.append('{0:{1}} {2:{3}} {4:{5}} {6}'.format( name, max(len(v) for v in c1) + CONTACT_LIST_INDENT, log_setting, max(len(v) for v in c2) + CONTACT_LIST_INDENT, notify_setting, max(len(v) for v in c3) + CONTACT_LIST_INDENT, members)) lst.insert(1, get_terminal_width() * '─') print('\n'.join(lst) + '\n')
def c_print(string: str, head: int = 0, tail: int = 0) -> None: """Print string to center of screen. :param string: String to print :param head: Number of new lines to print before string :param tail: Number of new lines to print after string :return: None """ for _ in range(head): print('') print(string.center(get_terminal_width())) for _ in range(tail): print('')
def redraw_file_win(self) -> None: """Draw file transmission window progress bars.""" # Initialize columns c1 = ['File name'] c2 = ['Size'] c3 = ['Sender'] c4 = ['Complete'] # Populate columns with file transmission status data for p in self.packet_list: # type: Packet if p.type == FILE and len(p.assembly_pt_list) > 0: if (p.name is not None and p.assembly_pt_list is not None and p.size is not None and p.packets is not None): 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 len(c1) <= 1: m_print("No file transmissions currently in progress.", bold=True, head=1, tail=1) print_on_previous_line(reps=3, delay=0.1) return None # Calculate column widths c1w, c2w, c3w, c4w = [ max(len(v) for v in column) + FILE_TRANSFER_INDENT for column in [c1, c2, c3, c4] ] # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}}{f2:{c2w}}{f3:{c3w}}{f4:{c4w}}' for f1, f2, f3, f4 in zip(c1, c2, c3, c4) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the file transfer list print('\n' + '\n'.join(lines) + '\n') print_on_previous_line(reps=len(lines) + 2, delay=0.1)
def yes(prompt: str, head: int = 0, tail: int = 0) -> bool: """Prompt user a question that is answered with yes / no. :param prompt: Question to be asked :param head: Number of new lines to print before prompt :param tail: Number of new lines to print after prompt :return: True if user types 'y' or 'yes' False if user types 'n' or 'no' """ for _ in range(head): print('') prompt = "{} (y/n): ".format(prompt) upper_line = ('┌' + (len(prompt) + 5) * '─' + '┐') title_line = ('│' + prompt + 5 * ' ' + '│') lower_line = ('└' + (len(prompt) + 5) * '─' + '┘') terminal_w = get_terminal_width() upper_line = upper_line.center(terminal_w) title_line = title_line.center(terminal_w) lower_line = lower_line.center(terminal_w) indent = title_line.find('│') print(upper_line) while True: print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) user_input = input(indent * ' ' + '│ {}'.format(prompt)) print_on_previous_line() if user_input == '': continue if user_input.lower() in ['y', 'yes']: print(indent * ' ' + '│ {}Yes │\n'.format(prompt)) for _ in range(tail): print('') return True elif user_input.lower() in ['n', 'no']: print(indent * ' ' + '│ {}No │\n'.format(prompt)) for _ in range(tail): print('') return False
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 message_printer(message: str, head: int = 0, tail: int = 0) -> None: """Print long message in the middle of the screen. :param message: Message to print :param head: Number of new lines to print before message :param tail: Number of new lines to print after message :return: None """ for _ in range(head): print('') line_list = (textwrap.fill(message, min(49, (get_terminal_width() - 6))).split('\n')) for l in line_list: c_print(l) for _ in range(tail): print('')
def box_print(msg_list: Union[str, list], manual_proceed: bool = False, head: int = 0, tail: int = 0) -> None: """Print message inside a box. :param msg_list: List of lines to print :param manual_proceed: Wait for user input before continuing :param head: Number of new lines to print before box :param tail: Number of new lines to print after box :return: None """ for _ in range(head): print('') if isinstance(msg_list, str): msg_list = [msg_list] len_widest = max(len(m) for m in msg_list) msg_list = ['{:^{}}'.format(m, len_widest) for m in msg_list] top_line = '┌' + (len(msg_list[0]) + 2) * '─' + '┐' bot_line = '└' + (len(msg_list[0]) + 2) * '─' + '┘' msg_list = ['│ {} │'.format(m) for m in msg_list] terminal_w = get_terminal_width() top_line = top_line.center(terminal_w) msg_list = [m.center(terminal_w) for m in msg_list] bot_line = bot_line.center(terminal_w) print(top_line) for m in msg_list: print(m) print(bot_line) for _ in range(tail): print('') if manual_proceed: input('') print_on_previous_line()
def redraw(self, file: Any = None) -> None: """Print all messages received to the window.""" old_messages = len(self.message_log) - self.unread_messages self.unread_messages = 0 if file is None: clear_screen() if self.message_log: self.previous_msg_ts = self.message_log[-1][0] self.create_handle_dict(self.message_log) for i, msg_tuple in enumerate(self.message_log): if i == old_messages: print( '\n' + ' Unread Messages '.center(get_terminal_width(), '-') + '\n') self.print(msg_tuple, file) else: m_print(f"This window for {self.name} is currently empty.", bold=True, head=1, tail=1)
def test_get_terminal_width(self): self.assertIsInstance(get_terminal_width(), int)
def box_input(message: str, # Input prompt message default: str = '', # Default return value head: int = 0, # Number of new lines to print before the input tail: int = 1, # Number of new lines to print after input expected_len: int = 0, # Expected length of the input key_type: str = '', # When specified, sets input width guide: bool = False, # When True, prints the guide for key validator: Optional[Validator] = None, # Input validator function validator_args: Optional[Any] = None # Arguments required by the validator ) -> str: # Input from user """Display boxed input prompt with a message.""" print_spacing(head) terminal_width = get_terminal_width() if key_type: key_guide = {B58_LOCAL_KEY: B58_LOCAL_KEY_GUIDE, B58_PUBLIC_KEY: B58_PUBLIC_KEY_GUIDE}.get(key_type, '') if guide: inner_spc = len(key_guide) + 2 else: inner_spc = ENCODED_B58_PUB_KEY_LENGTH if key_type == B58_PUBLIC_KEY else ENCODED_B58_KDK_LENGTH inner_spc += 2 # Spacing around input space else: key_guide = '' inner_spc = terminal_width - 2 if expected_len == 0 else expected_len + 2 upper_line = '┌' + inner_spc * '─' + '┐' guide_line = '│ ' + key_guide + ' │' input_line = '│' + inner_spc * ' ' + '│' lower_line = '└' + inner_spc * '─' + '┘' box_indent = (terminal_width - len(upper_line)) // 2 * ' ' terminal_width_check(len(upper_line)) print(box_indent + upper_line) if guide: print(box_indent + guide_line) print(box_indent + input_line) print(box_indent + lower_line) print((5 if guide else 4) * CURSOR_UP_ONE_LINE) print(box_indent + '┌─┤' + message + '├') if guide: print('') user_input = input(box_indent + '│ ') if user_input == '': print(2 * CURSOR_UP_ONE_LINE) print(box_indent + '│ ' + default) user_input = default if validator is not None: error_msg = validator(user_input, validator_args) if error_msg: m_print(error_msg, head=1) print_on_previous_line(reps=4, delay=1) return box_input(message, default, head, tail, expected_len, key_type, guide, validator, validator_args) print_spacing(tail) return user_input
def print_settings(self) -> None: """\ Print list of settings, their current and default values, and setting descriptions. """ desc_d = { # Common settings "disable_gui_dialog": "True replaces GUI dialogs with CLI prompts", "max_number_of_group_members": "Maximum number of members in a group", "max_number_of_groups": "Maximum number of groups", "max_number_of_contacts": "Maximum number of contacts", "log_messages_by_default": "Default logging setting for new contacts/groups", "accept_files_by_default": "Default file reception setting for new contacts", "show_notifications_by_default": "Default message notification setting for new contacts/groups", "log_file_masking": "True hides real size of log file during traffic masking", "ask_password_for_log_access": "False disables password prompt when viewing/exporting logs", # Transmitter settings "nc_bypass_messages": "False removes Networked Computer bypass interrupt messages", "confirm_sent_files": "False sends files without asking for confirmation", "double_space_exits": "True exits, False clears screen with double space command", "traffic_masking": "True enables traffic masking to hide metadata", "tm_static_delay": "The static delay between traffic masking packets", "tm_random_delay": "Max random delay for traffic masking timing obfuscation", # Relay settings "allow_contact_requests": "When False, does not show TFC contact requests", # Receiver settings "new_message_notify_preview": "When True, shows a preview of the received message", "new_message_notify_duration": "Number of seconds new message notification appears", "max_decompress_size": "Max size Receiver accepts when decompressing file" } # Columns c1 = ['Setting name'] c2 = ['Current value'] c3 = ['Default value'] c4 = ['Description'] terminal_width = get_terminal_width() description_indent = 64 if terminal_width < description_indent + 1: raise FunctionReturn("Error: Screen width is too small.", head_clear=True) # Populate columns with setting data for key in self.defaults: c1.append(key) c2.append(str(self.__getattribute__(key))) c3.append(str(self.defaults[key])) description = desc_d[key] wrapper = textwrap.TextWrapper( width=max(1, (terminal_width - description_indent))) desc_lines = wrapper.fill(description).split('\n') desc_string = desc_lines[0] for line in desc_lines[1:]: desc_string += '\n' + description_indent * ' ' + line if len(desc_lines) > 1: desc_string += '\n' c4.append(desc_string) # Calculate column widths c1w, c2w, c3w = [ max(len(v) for v in column) + SETTINGS_INDENT for column in [c1, c2, c3] ] # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}} {f2:{c2w}} {f3:{c3w}} {f4}' for f1, f2, f3, f4 in zip(c1, c2, c3, c4) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the settings clear_screen() print('\n' + '\n'.join(lines))
def box_input(message: str, default: str = '', head: int = 0, tail: int = 1, expected_len: int = 0, validator: Callable = None, validator_args: Any = None, key_input: bool = False) -> str: """Display boxed input prompt with title. :param message: Input prompt message :param default: Default return value :param head: Number of new lines to print before input :param tail: Number of new lines to print after input :param expected_len Expected length of input :param validator: Input validator function :param validator_args: Arguments required by the validator :param key_input: When True, prints key input position guide :return: Input from user """ for _ in range(head): print('') terminal_w = get_terminal_width() input_len = terminal_w - 2 if expected_len == 0 else expected_len + 2 if key_input: input_len += 2 input_top_line = '┌' + input_len * '─' + '┐' key_pos_guide = '│ ' + ' '.join('ABCDEFGHIJKLMNOPQ') + ' │' input_line = '│' + input_len * ' ' + '│' input_bot_line = '└' + input_len * '─' + '┘' input_line_indent = (terminal_w - len(input_line)) // 2 input_box_indent = input_line_indent * ' ' print(input_box_indent + input_top_line) if key_input: print(input_box_indent + key_pos_guide) print(input_box_indent + input_line) print(input_box_indent + input_bot_line) print((5 if key_input else 4) * CURSOR_UP_ONE_LINE) print(input_box_indent + '┌─┤' + message + '├') if key_input: print('') user_input = input(input_box_indent + '│ ') if user_input == '': print(2 * CURSOR_UP_ONE_LINE) print(input_box_indent + '│ {}'.format(default)) user_input = default if validator is not None: error_msg = validator(user_input, validator_args) if error_msg: c_print("Error: {}".format(error_msg), head=1) print_on_previous_line(reps=4, delay=1.5) return box_input(message, default, head, tail, expected_len, validator, validator_args) for _ in range(tail): print('') return user_input
def print_groups(self) -> None: """Print list of groups. Neatly printed group list allows easy group management and it also allows the user to check active logging and notification setting, as well as what group ID Relay Program shows corresponds to what group, and which contacts are in the group. """ # Initialize columns c1 = ['Group'] c2 = ['Group ID'] c3 = ['Logging '] c4 = ['Notify'] c5 = ['Members'] # Populate columns with group data that has only a single line for g in self.groups: c1.append(g.name) c2.append(b58encode(g.group_id)) c3.append('Yes' if g.log_messages else 'No') c4.append('Yes' if g.notifications else 'No') # Calculate the width of single-line columns c1w, c2w, c3w, c4w = [ max(len(v) for v in column) + CONTACT_LIST_INDENT for column in [c1, c2, c3, c4] ] # Create a wrapper for Members-column wrapped_members_line_indent = c1w + c2w + c3w + c4w members_column_width = max( 1, get_terminal_width() - wrapped_members_line_indent) wrapper = textwrap.TextWrapper(width=members_column_width) # Populate the Members-column for g in self.groups: if g.empty(): c5.append("<Empty group>\n") else: comma_separated_nicks = ', '.join( sorted([m.nick for m in g.members])) members_column_lines = wrapper.fill( comma_separated_nicks).split('\n') final_str = members_column_lines[0] + '\n' for line in members_column_lines[1:]: final_str += wrapped_members_line_indent * ' ' + line + '\n' c5.append(final_str) # Align columns by adding whitespace between fields of each line lines = [ f'{f1:{c1w}}{f2:{c2w}}{f3:{c3w}}{f4:{c4w}}{f5}' for f1, f2, f3, f4, f5 in zip(c1, c2, c3, c4, c5) ] # Add a terminal-wide line between the column names and the data lines.insert(1, get_terminal_width() * '─') # Print the group list print('\n'.join(lines) + '\n')
def print_help(settings: 'Settings') -> None: """Print the list of commands.""" def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None: """Print list of commands and their descriptions. Style in which commands are printed depends on terminal width. Depending on whether traffic masking is enabled, some commands are either displayed or hidden. """ len_longest_command = max( len(t[0]) for t in tuple_list) + 1 # Add one for spacing wrapper = textwrap.TextWrapper( width=max(1, terminal_width - len_longest_command)) for help_cmd, description, display in tuple_list: if not display: continue desc_lines = wrapper.fill(description).split('\n') desc_indent = (len_longest_command - len(help_cmd)) * ' ' print(help_cmd + desc_indent + desc_lines[0]) # Print wrapped description lines with indent if len(desc_lines) > 1: for line in desc_lines[1:]: print(len_longest_command * ' ' + line) print('') # ------------------------------------------------------------------------------------------------------------------ y_tm = settings.traffic_masking n_tm = not y_tm common_commands = [ ("/about", "Show links to project resources", True), ("/add", "Add new contact", n_tm), ("/cf", "Cancel file transmission to active contact/group", y_tm), ("/cm", "Cancel message transmission to active contact/group", True), ("/clear, ' '", "Clear TFC screens", True), ("/cmd, '//'", "Display command window on Receiver", True), ("/connect", "Resend Onion Service data to Relay", True), ("/exit", "Exit TFC on all three computers", True), ("/export (n)", "Export (n) messages from recipient's log file", True), ("/file", "Send file to active contact/group", True), ("/fw", "Display file reception window on Receiver", y_tm), ("/help", "Display this list of commands", True), ("/history (n)", "Print (n) messages from recipient's log file", True), ("/localkey", "Generate new local key pair", n_tm), ("/logging {on,off}(' all')", "Change message log setting (for all contacts)", True), ("/msg {A,N,G}", "Change recipient to Account, Nick, or Group", n_tm), ("/names", "List contacts and groups", True), ("/nick N", "Change nickname of active recipient/group to N", True), ("/notify {on,off} (' all')", "Change notification settings (for all contacts)", True), ("/passwd {tx,rx}", "Change master password on target system", n_tm), ("/psk", "Open PSK import dialog on Receiver", n_tm), ("/reset", "Reset ephemeral session log for active window", True), ("/rm {A,N}", "Remove contact specified by account A or nick N", n_tm), ("/rmlogs {A,N}", "Remove log entries for account A or nick N", True), ("/set S V", "Change setting S to value V", True), ("/settings", "List setting names, values and descriptions", True), ("/store {on,off} (' all')", "Change file reception (for all contacts)", True), ("/unread, ' '", "List windows with unread messages on Receiver", True), ("/verify", "Verify fingerprints with active contact", True), ("/whisper M", "Send message M, asking it not to be logged", True), ("/whois {A,N}", "Check which A corresponds to N or vice versa", True), ("/wipe", "Wipe all TFC user data and power off systems", True), ("Shift + PgUp/PgDn", "Scroll terminal up/down", True) ] group_commands = [ ("/group create G A₁..Aₙ", "Create group G and add accounts A₁..Aₙ", n_tm), ("/group join ID G A₁..Aₙ", "Join group ID, call it G and add accounts A₁..Aₙ", n_tm), ("/group add G A₁..Aₙ", "Add accounts A₁..Aₙ to group G", n_tm), ("/group rm G A₁..Aₙ", "Remove accounts A₁..Aₙ from group G", n_tm), ("/group rm G", "Remove group G", n_tm) ] terminal_width = get_terminal_width() clear_screen() print(textwrap.fill("List of commands:", width=terminal_width)) print('') help_printer(common_commands) print(terminal_width * '─') if settings.traffic_masking: print('') else: print(textwrap.fill("Group management:", width=terminal_width)) print('') help_printer(group_commands) print(terminal_width * '─' + '\n')