Пример #1
0
def pwd_prompt(message: str, lc: str, rc: str) -> str:
    """Prompt user to enter a password.

    :param message: Prompt message
    :param lc:      Upper-left corner box character
    :param rc:      Upper-right corner box character
    :return:        Password from user
    """
    upper_line = (lc + (len(message) + 3) * '─' + rc)
    title_line = ('│' + message + 3 * ' ' + '│')
    lower_line = ('└' + (len(message) + 3) * '─' + '┘')

    upper_line = upper_line.center(get_tty_w())
    title_line = title_line.center(get_tty_w())
    lower_line = lower_line.center(get_tty_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 * ' ' + f'│ {message}')

    return user_input
Пример #2
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')

            m_indent = 40
            m_string = ', '.join(sorted([m.nick for m in g.members]))
            wrapper = textwrap.TextWrapper(
                width=max(1, (get_tty_w() - 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)

        lst = []
        for name, log_setting, notify_setting, members in zip(c1, c2, c3, c4):
            lst.append('{0:{4}} {1:{5}} {2:{6}} {3}'.format(
                name, log_setting, notify_setting, members,
                len(max(c1, key=len)) + 4,
                len(max(c2, key=len)) + 4,
                len(max(c3, key=len)) + 4))

        print(lst[0] + '\n' + get_tty_w() * '─')
        print('\n'.join(str(l) for l in lst[1:]) + '\n')
Пример #3
0
    def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None:
        """Print help menu, style depending on terminal width and display conditions.

        :param tuple_list: List of command-description-display tuples
        """
        longest_command = ''
        for t in tuple_list:
            longest_command = max(t[0], longest_command, key=len)
        longest_command += ' '  # Add spacing

        for help_cmd, description, display_condition in tuple_list:

            if not display_condition:
                continue

            wrapper    = textwrap.TextWrapper(width=max(1, (get_tty_w() - len(longest_command))))
            desc_lines = wrapper.fill(description).split('\n')
            spacing    = (len(longest_command) - len(help_cmd)) * ' '

            print(help_cmd + spacing + 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('')
Пример #4
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:   Notify with custom message
    :param head:   N.o. 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_tty_w() - (len(string) + offset)) // 2) * ' '

        print(indent + string, end='', flush=True)
Пример #5
0
    def redraw(self):
        """Draw file window frame."""
        ft_found = False
        line_ctr = 0
        longest_title = 0
        tty_w = get_tty_w()

        for p in self.packet_list:
            if p.type == 'file' and len(p.assembly_pt_list) > 0:
                title = "{} ({}) from {} ".format(p.f_name, p.f_size,
                                                  p.contact.nick)
                longest_title = max(longest_title, len(title))

        for p in self.packet_list:
            if p.type == 'file' and len(p.assembly_pt_list) > 0:
                line_ctr += 1
                ft_found = True
                title = "{} ({}) from {} ".format(p.f_name, p.f_size,
                                                  p.contact.nick)
                title += (longest_title - len(title)) * ' '

                bar_len = max(tty_w - (4 + len(title)), 1)
                ready = int((len(p.assembly_pt_list) / p.f_packets) * bar_len)
                missing = bar_len - ready
                bar = title + '[' + (ready -
                                     1) * '=' + '>' + missing * ' ' + ']'
                print(bar)

        print_on_previous_line(reps=line_ctr)

        if not ft_found:
            c_print("No file transmissions currently in progress.",
                    head=1,
                    tail=1)
            print_on_previous_line(reps=3)
Пример #6
0
def box_input(title: str,
              default: str = '',
              head: int = 0,
              tail: int = 0,
              expected_len: int = 0,
              validator: Callable = None,
              validator_args: Any = None) -> str:
    """Display boxed prompt for user with title.

    :param title:          Title for data to prompt
    :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
    :return:               Input from user
    """
    for _ in range(head):
        print('')

    tty_w = get_tty_w()
    input_len = tty_w - 2 if expected_len == 0 else expected_len + 2

    input_top_line = '┌' + input_len * '─' + '┐'
    input_line = '│' + input_len * ' ' + '│'
    input_bot_line = '└' + input_len * '─' + '┘'

    input_line_indent = (tty_w - len(input_line)) // 2
    input_box_indent = input_line_indent * ' '

    print(input_box_indent + input_top_line)
    print(input_box_indent + input_line)
    print(input_box_indent + input_bot_line)
    print(4 * CURSOR_UP_ONE_LINE)
    print(input_box_indent + '┌─┤' + title + '├')

    user_input = input(input_box_indent + '│ ')

    if user_input == '':
        print(2 * CURSOR_UP_ONE_LINE)
        print(input_box_indent + f'│ {default}')
        user_input = default

    if validator is not None:
        success, error_msg = validator(user_input, validator_args)
        if not success:
            c_print("Error: {}".format(error_msg), head=1)
            print_on_previous_line(reps=4, delay=1.5)
            return box_input(title, default, head, tail, expected_len,
                             validator, validator_args)

    for _ in range(tail):
        print('')

    return user_input
Пример #7
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)
    tty_w = get_tty_w()
    upper_line = ('┌' + (len(prompt) + 5) * '─' + '┐')
    title_line = ('│' + prompt + 5 * ' ' + '│')
    lower_line = ('└' + (len(prompt) + 5) * '─' + '┘')

    upper_line = upper_line.center(tty_w)
    title_line = title_line.center(tty_w)
    lower_line = lower_line.center(tty_w)

    indent = title_line.find('│')
    print(upper_line)
    print(title_line)
    print(lower_line)
    print(3 * CURSOR_UP_ONE_LINE)

    while True:
        print(title_line)
        print(lower_line)
        print(3 * CURSOR_UP_ONE_LINE)
        answer = input(indent * ' ' + f'│ {prompt}')
        print_on_previous_line()

        if answer == '':
            continue

        if answer.lower() in 'yes':
            print(indent * ' ' + f'│ {prompt}Yes │\n')
            for _ in range(tail):
                print('')
            return True

        elif answer.lower() in 'no':
            print(indent * ' ' + f'│ {prompt}No  │\n')
            for _ in range(tail):
                print('')
            return False

        else:
            continue
Пример #8
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_tty_w()))

    for _ in range(tail):
        print('')
Пример #9
0
    def print(self, msg_tuple: Tuple['datetime.datetime', str, str,
                                     bytes]) -> None:
        """Print new message to window."""
        ts, message, account, origin = msg_tuple

        if self.type == 'command':
            nick = '-!-'
        else:
            window_nicks = [c.nick for c in self.window_contacts] + ['Me']
            len_of_longest = len(max(window_nicks, key=len))
            nick = 'Me' if origin == ORIGIN_USER_HEADER else self.contact_list.get_contact(
                account).nick
            indent = len_of_longest - len(nick)
            nick = indent * ' ' + nick + ':'

        if self.previous_msg_ts.date() != ts.date():
            print(f"00:00 -!- Day changed to {str(ts.date())}")
        self.previous_msg_ts = ts

        timestamp = ts.strftime('%H:%M')
        ts_nick = f"{timestamp} {nick} "

        if not self.is_active and self.type == 'group':
            ts_nick += f"(group {self.name}) "

        wrapper = textwrap.TextWrapper(initial_indent=ts_nick,
                                       subsequent_indent=(len(ts_nick)) * ' ',
                                       width=get_tty_w())
        wrapped = wrapper.fill(message)

        # Add bold-effect after wrapping so length of injected VT100 codes does not affect wrapping.
        wrapped = BOLD_ON + wrapped[:len(ts_nick
                                         )] + BOLD_OFF + wrapped[len(ts_nick):]

        if self.is_active:
            print(wrapped)
        else:
            self.unread_messages += 1
            if self.contact_list.get_contact(account).notifications:
                # Preview only first line of long message
                if len(wrapped.split('\n')) > 1:
                    print(wrapped.split('\n')[0][:-3] + '...')
                else:
                    print(wrapped)
                print_on_previous_line(delay=self.settings.new_msg_notify_dur,
                                       flush=True)
Пример #10
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_tty_w() - 6))).split('\n'))
    for l in line_list:
        c_print(l)

    for _ in range(tail):
        print('')
Пример #11
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]

    tty_w = get_tty_w()
    widest = max(msg_list, key=len)

    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]

    top_line = top_line.center(tty_w)
    msg_list = [m.center(tty_w) for m in msg_list]
    bot_line = bot_line.center(tty_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()
Пример #12
0
def ask_confirmation_code() -> str:
    """Ask user to input confirmation code from RxM to verify local key has been installed."""
    title = "Enter confirmation code (from RxM): "

    upper_line = ('┌' + (len(title) + 8) * '─' + '┐')
    title_line = ('│' + title + 8 * ' ' + '│')
    lower_line = ('└' + (len(title) + 8) * '─' + '┘')

    ttyw = get_tty_w()

    upper_line = upper_line.center(ttyw)
    title_line = title_line.center(ttyw)
    lower_line = lower_line.center(ttyw)

    print(upper_line)
    print(title_line)
    print(lower_line)
    print(3 * CURSOR_UP_ONE_LINE)

    indent = title_line.find('│')
    return input(indent * ' ' + f'│ {title}')
Пример #13
0
    def print_contacts(self, spacing: bool = True) -> None:
        """Print list of contacts."""
        # Columns
        c1 = ['Contact']
        c2 = ['Logging']
        c3 = ['Notify']
        c4 = ['Files ']
        c5 = ['Key Ex']
        c6 = ['Account']

        for c in self.contacts:
            if c.rx_account == 'local':
                continue

            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(32) else 'X25519')
            c6.append(c.rx_account)

        lst = []
        for nick, log_setting, notify_setting, file_reception_setting, key_exchange, account in zip(c1, c2, c3, c4, c5, c6):
            lst.append('{0:{6}} {1:{7}} {2:{8}} {3:{9}} {4:{10}} {5}'.format(
                nick, log_setting, notify_setting, file_reception_setting, key_exchange, account,
                len(max(c1, key=len)) + 4,
                len(max(c2, key=len)) + 4,
                len(max(c3, key=len)) + 4,
                len(max(c4, key=len)) + 4,
                len(max(c5, key=len)) + 4,
                len(max(c6, key=len)) + 4))

        if spacing:
            clear_screen()
            print('')

        lst.insert(1, get_tty_w() * '─')
        print('\n'.join(str(l) for l in lst))
        print('\n')
Пример #14
0
def access_history(window:       Union['Window', 'Window_'],
                   contact_list: 'ContactList',
                   settings:     'Settings',
                   master_key:   'MasterKey',
                   msg_to_load:  int = 0,
                   export:       bool = False) -> None:
    """Decrypt 'msg_to_load' last messages from log database and display/export it.

    :param window:       Window object
    :param contact_list: ContactList object
    :param settings:     Settings object
    :param master_key:   Master key object
    :param msg_to_load:  Number of messages to load
    :param export:       When True, write logged messages into
                         plaintext file instead of printing them.
    :return:             None
    """

    def read_entry():
        """Read encrypted log entry.

        Length  |  Data type
        --------|--------------------------------
             24 |  XSalsa20 nonce
              4 |  Timestamp
              4 |  UTF-32 BOM
          4*255 |  Padded account (UTF-32)
              1 |  Origin header
              1 |  Assembly packet header
            255 |  Padded assembly packet (UTF-8)
             16 |  Poly1305 tag
        """
        return log_file.read(1325)

    ensure_dir(f'{DIR_USER_DATA}/')
    file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs'
    if not os.path.isfile(file_name):
        raise FunctionReturn(f"Error: Could not find '{file_name}'.")

    log_file          = open(file_name, 'rb')
    ts_message_list   = []  # type: List[Tuple[str, str, bytes, str]]
    assembly_p_buffer = dict()
    group_timestamp   = b''

    for ct in iter(read_entry, b''):
        pt      = auth_and_decrypt(ct, key=master_key.master_key)
        account = bytes_to_str(pt[5:1029])

        if window.type == 'contact' and window.uid != account:
            continue

        t_stamp         = parse_ts_bytes(pt[0:4], settings)
        origin_byte     = pt[4:5]
        origin          = origin_byte.decode()
        assembly_header = pt[1029:1030]
        assembly_pt     = pt[1030:]

        if assembly_header == M_S_HEADER:
            depadded     = rm_padding_bytes(assembly_pt)
            decompressed = zlib.decompress(depadded)
            if decompressed[:1] == PRIVATE_MESSAGE_HEADER:
                if window.type == 'group':
                    continue
                decoded = decompressed[1:].decode()

            elif decompressed[:1] == GROUP_MESSAGE_HEADER:

                group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)]
                if group_name != window.name:
                    continue
                if group_timestamp == decompressed[1:9]:
                    continue
                else:
                    group_timestamp = decompressed[1:9]

            ts_message_list.append((t_stamp, account, origin_byte, decoded))

        elif assembly_header == M_L_HEADER:
            assembly_p_buffer[origin + account] = assembly_pt

        elif assembly_header == M_A_HEADER:
            if (origin + account) in assembly_p_buffer:
                assembly_p_buffer[origin + account] += assembly_pt

        elif assembly_header == M_E_HEADER:
            if (origin + account) in assembly_p_buffer:
                assembly_p_buffer[origin + account] += assembly_pt

                pt_buf       = assembly_p_buffer.pop(origin + account)
                inner_l      = rm_padding_bytes(pt_buf)
                msg_key      = inner_l[-32:]
                enc_msg      = inner_l[:-32]
                decrypted    = auth_and_decrypt(enc_msg, key=msg_key)
                decompressed = zlib.decompress(decrypted)

                if decompressed[:1] == PRIVATE_MESSAGE_HEADER:
                    if window.type == 'group':
                        continue
                    decoded = decompressed[1:].decode()

                elif decompressed[:1] == GROUP_MESSAGE_HEADER:
                    group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)]
                    if group_name != window.name:
                        continue
                    if group_timestamp == decompressed[1:9]:  # Skip duplicates of outgoing messages
                        continue
                    else:
                        group_timestamp = decompressed[1:9]

                ts_message_list.append((t_stamp, account, origin_byte, decoded))

        elif assembly_header == M_C_HEADER:
            assembly_p_buffer.pop(origin + account, None)

    log_file.close()

    if not export:
        clear_screen()
        print('')

    tty_w  = get_tty_w()

    system = dict(tx="TxM",     rx="RxM",     ut="Unittest")[settings.software_operation]
    m_dir  = dict(tx="sent to", rx="to/from", ut="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.name}", tty_w)

    print(title,       file=f_name)
    print(tty_w * '═', file=f_name)

    for timestamp, account, origin_, message in ts_message_list[-msg_to_load:]:

        nick = "Me" if origin_ == ORIGIN_USER_HEADER else contact_list.get_contact(account).nick

        print(textwrap.fill(f"{timestamp} {nick}:", tty_w), file=f_name)
        print('',                                           file=f_name)
        print(textwrap.fill(message, tty_w),                file=f_name)
        print('',                                           file=f_name)
        print(tty_w * '─',                                  file=f_name)

    if export:
        f_name.close()
    else:
        print('')
Пример #15
0
 def test_get_tty_w(self):
     self.assertIsInstance(get_tty_w(), int)
Пример #16
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 help menu, style depending on terminal width and display conditions.

        :param tuple_list: List of command-description-display tuples
        """
        longest_command = ''
        for t in tuple_list:
            longest_command = max(t[0], longest_command, key=len)
        longest_command += ' '  # Add spacing

        for help_cmd, description, display_condition in tuple_list:

            if not display_condition:
                continue

            wrapper    = textwrap.TextWrapper(width=max(1, (get_tty_w() - len(longest_command))))
            desc_lines = wrapper.fill(description).split('\n')
            spacing    = (len(longest_command) - len(help_cmd)) * ' '

            print(help_cmd + spacing + 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('')

    common = [("/about",                    "Show links to project resources",                   True),
              ("/add",                      "Add new contact",                                   not settings.session_trickle),
              ("/cf",                       "Cancel file transmission to recipients",            True),
              ("/cm",                       "Cancel message transmission to recipients",         True),
              ("/clear, '  '",              "Clear screens from TxM, RxM and IM client",         True),
              ("/cmd, '//'",                "Display command window on RxM",                     True),
              ("/exit",                     "Exit TFC on TxM, NH and RxM",                       True),
              ("/export (n)",               "Export (n) messages from recipient's logfile",      True),
              ("/file",                     "Send file to active contact/group",                 True),
              ("/fingerprints",             "Print public key fingerprints of user and contact", True),
              ("/fe",                       "Encrypt and export file to NH",                     not settings.session_trickle),
              ("/fi",                       "Import file from NH to RxM",                        not settings.session_trickle),
              ("/fw",                       "Display file reception window on RxM",              True),
              ("/help",                     "Display this list of commands",                     True),
              ("/history (n)",              "Print (n) messages from recipient's logfile",       True),
              ("/localkey",                 "Generate new local key pair",                       not settings.session_trickle),
              ("/logging {on,off}(' all')", "Change log_messages setting (for all contacts)",    True),
              ("/msg",                      "Change active recipient",                           not settings.session_trickle),
              ("/names",                    "List contacts and groups",                          True),
              ("/nick N",                   "Change nickname of active recipient to N",          True),
              ("/notify {on,off} (' all')", "Change notification settings (for all contacts)",   True),
              ("/passwd {tx,rx}",           "Change master password on TxM/RxM",                 not settings.session_trickle),
              ("/psk",                      "Open PSK import dialog on RxM",                     True),
              ("/reset",                    "Reset ephemeral session log on TxM/RxM/IM client",  not settings.session_trickle),
              ("/rm A",                     "Remove account A from TxM and RxM",                 not settings.session_trickle),
              ("/set S V",                  "Change setting S to value V on TxM/RxM",            not settings.session_trickle),
              ("/settings",                 "List settings, default values and descriptions",    not settings.session_trickle),
              ("/store {on,off} (' all')",  "Change file reception (for all contacts)",          True),
              ("/unread, ' '",              "List windows with unread messages on RxM",          True),
              ("Shift + PgUp/PgDn",         "Scroll terminal up/down",                           True)]

    groupc = [("/group create G A1 .. An",  "Create group G and add accounts A1 .. An",          not settings.session_trickle),
              ("/group add G A1 .. An",     "Add accounts A1 .. An to group G",                  not settings.session_trickle),
              ("/group rm G A1 .. An",      "Remove accounts A1 .. An from group G",             not settings.session_trickle),
              ("/group rm G",               "Remove group G",                                    not settings.session_trickle)]

    terminal_width = get_tty_w()

    clear_screen()

    print(textwrap.fill("List of commands:", width=terminal_width))
    print('')
    help_printer(common)
    print(terminal_width * '-')

    if settings.session_trickle:
        print('')
    else:
        print("Group management:\n")
        help_printer(groupc)
        print(terminal_width * '-' + '\n')
Пример #17
0
    def print_settings(self) -> None:
        """Print list of settings, their current and default values and setting descriptions."""

        # Common
        desc_d = {
            "format_of_logfiles": "Timestamp format of logged messages",
            "disable_gui_dialog":
            "True replaces Tkinter dialogs with CLI prompts",
            "m_members_in_group":
            "Max members in group (Must be same on TxM/RxM)",
            "m_number_of_groups":
            "Max number of groups (Must be same on TxM/RxM)",
            "m_number_of_accnts":
            "Max number of accounts (Must be same on TxM/RxM)",
            "serial_iface_speed":
            "The speed of serial interface in bauds per sec",
            "e_correction_ratio":
            "N/o byte errors serial datagrams can recover from",
            "log_msg_by_default": "Default logging setting for new contacts",
            "store_file_default":
            "True accepts files from new contacts by default",
            "n_m_notify_privacy":
            "Default message notification setting for new contacts",
            "log_dummy_file_a_p":
            "False disables storage of placeholder data for files",

            # TxM
            "txm_serial_adapter":
            "False uses system's integrated serial interface",
            "nh_bypass_messages": "False removes NH bypass interrupt messages",
            "confirm_sent_files":
            "False sends files without asking for confirmation",
            "double_space_exits":
            "True exits with doubles space, else clears screen",
            "trickle_connection":
            "True enables trickle connection to hide metadata",
            "trickle_stat_delay": "Static delay between trickle packets",
            "trickle_rand_delay": "Max random delay for timing obfuscation",
            "long_packet_rand_d": "True adds spam guard evading delay",
            "max_val_for_rand_d":
            "Maximum time for random spam guard evasion delay",

            # RxM
            "rxm_serial_adapter":
            "False uses system's integrated serial interface",
            "new_msg_notify_dur":
            "Number of seconds new msg notification appears"
        }

        clear_screen()
        tty_w = get_tty_w()

        print(
            "Setting name        Current value      Default value      Description"
        )
        print(tty_w * '-')

        for key in self.defaults:
            def_value = str(self.defaults[key]).ljust(len('%Y-%m-%d %H:%M:%S'))
            description = desc_d[key]
            wrapper = textwrap.TextWrapper(width=max(1, (tty_w - 59)))
            desc_lines = wrapper.fill(description).split('\n')
            current_value = str(self.__getattribute__(key)).ljust(17)

            print(f"{key}  {current_value}  {def_value}  {desc_lines[0]}")

            # Print wrapped description lines with indent
            if len(desc_lines) > 1:
                for line in desc_lines[1:]:
                    print(58 * ' ' + line)
                print('')

        print('\n')