Beispiel #1
0
    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')
Beispiel #2
0
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
Beispiel #3
0
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("────╯ " + ' ' + " ╰────")
Beispiel #4
0
    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)
Beispiel #5
0
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()
Beispiel #6
0
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)
Beispiel #7
0
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}')
Beispiel #8
0
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))
Beispiel #9
0
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)
Beispiel #10
0
    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')
Beispiel #11
0
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
Beispiel #12
0
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(  "────╯ " +  ' '  +  " ╰────" )
Beispiel #13
0
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()
Beispiel #14
0
    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
Beispiel #15
0
    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')
Beispiel #16
0
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
Beispiel #17
0
    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')
Beispiel #18
0
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('')
Beispiel #19
0
    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)
Beispiel #20
0
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
Beispiel #21
0
    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
Beispiel #22
0
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('')
Beispiel #23
0
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()
Beispiel #24
0
    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)
Beispiel #25
0
 def test_get_terminal_width(self):
     self.assertIsInstance(get_terminal_width(), int)
Beispiel #26
0
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
Beispiel #27
0
    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))
Beispiel #28
0
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
Beispiel #29
0
    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')
Beispiel #30
0
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')