summaryrefslogtreecommitdiff
path: root/nyx/panel/torrc.py
blob: 5efa2d4592767ae8b81556672ac929549141e0d3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# Copyright 2010-2020, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Panel displays our syntax highlighted torrc.
"""

import functools
import math
import string

import nyx.curses

from nyx.curses import RED, GREEN, YELLOW, CYAN, WHITE, BOLD, HIGHLIGHT
from nyx.menu import MenuItem, Submenu
from nyx import expand_path, panel, tor_controller

from stem import ControllerError
from stem.control import State


def _read_torrc(path):
  contents = []

  with open(path) as torrc_file:
    for line in torrc_file.readlines():
      line = line.replace('\t', '   ').replace('\xc2', "'").rstrip()
      contents.append(''.join(filter(lambda char: char in string.printable, line)))

  return contents


class TorrcPanel(panel.Panel):
  """
  Renders our syntax highlighted torrc within a scrollable area.
  """

  def __init__(self):
    panel.Panel.__init__(self)

    self._scroller = nyx.curses.Scroller()
    self._show_line_numbers = True  # shows left aligned line numbers
    self._show_comments = True  # shows comments and extra whitespace
    self._last_content_height = 0

    self._torrc_location = None
    self._torrc_content = None
    self._torrc_load_error = None

    controller = tor_controller()
    controller.add_status_listener(self._reset_listener)
    self._reset_listener(controller, State.RESET, None)

  def _reset_listener(self, controller, event_type, _):
    """
    Reloads and displays the torrc on tor reload (sighup) events.
    """

    if event_type == State.RESET:
      try:
        self._torrc_location = expand_path(controller.get_info('config-file'))
        self._torrc_content = _read_torrc(self._torrc_location)
      except ControllerError as exc:
        self._torrc_load_error = 'Unable to determine our torrc location: %s' % exc
        self._torrc_location = None
        self._torrc_content = None
      except Exception as exc:
        exc_msg = exc.strerror if (hasattr(exc, 'strerror') and exc.strerror) else str(exc)
        self._torrc_load_error = 'Unable to read our torrc: %s' % exc_msg
        self._torrc_content = None

  def key_handlers(self):
    def _scroll(key):
      page_height = self.get_height() - 1
      is_changed = self._scroller.handle_key(key, self._last_content_height, page_height)

      if is_changed:
        self.redraw()

    def _toggle_comment_stripping():
      self._show_comments = not self._show_comments

    def _toggle_line_numbers():
      self._show_line_numbers = not self._show_line_numbers

    return (
      nyx.panel.KeyHandler('arrows', 'scroll up and down', _scroll, key_func = lambda key: key.is_scroll()),
      nyx.panel.KeyHandler('s', 'comment stripping', _toggle_comment_stripping, 'off' if self._show_comments else 'on'),
      nyx.panel.KeyHandler('l', 'line numbering', _toggle_line_numbers, 'on' if self._show_line_numbers else 'off'),
    )

  def submenu(self):
    """
    Submenu consisting of...

      Reload
      Show / Hide Comments
      Show / Hide Line Numbers
    """

    comments_label, comments_arg = ('Hide Comments', False) if self._show_comments else ('Show Comments', True)
    line_number_label, line_number_arg = ('Hide Line Numbers', False) if self._show_line_numbers else ('Show Line Numbers', True)

    return Submenu('Torrc', [
      MenuItem(comments_label, functools.partial(setattr, self, '_show_comments'), comments_arg),
      MenuItem(line_number_label, functools.partial(setattr, self, '_show_line_numbers'), line_number_arg),
    ])

  def _draw(self, subwindow):
    scroll = self._scroller.location(self._last_content_height, subwindow.height - 1)

    if self._torrc_content is None:
      subwindow.addstr(0, 1, self._torrc_load_error, RED, BOLD)
      new_content_height = 1
    else:
      if not self._show_line_numbers:
        line_number_offset = 0
      elif len(self._torrc_content) == 0:
        line_number_offset = 2
      else:
        line_number_offset = int(math.log10(len(self._torrc_content))) + 2

      scroll_offset = 0

      if self._last_content_height > subwindow.height - 1:
        scroll_offset = 3
        subwindow.scrollbar(1, scroll, self._last_content_height)

      y = 1 - scroll
      is_multiline = False  # true if we're in the middle of a multiline torrc entry

      for line_number, line in enumerate(self._torrc_content):
        if not self._show_comments:
          line = line[:line.find('#')].rstrip() if '#' in line else line

          if not line:
            continue  # skip blank lines

        if '#' in line:
          line, comment = line.split('#', 1)
          comment = '#' + comment
        else:
          comment = ''

        if is_multiline:
          option, argument = '', line  # previous line ended with a '\'
        elif ' ' not in line.strip():
          option, argument = line, ''  # no argument
        else:
          whitespace = ' ' * (len(line) - len(line.lstrip()))
          option, argument = line.lstrip().split(' ', 1)
          option = whitespace + option + ' '

        is_multiline = line.endswith('\\')  # next line's part of a multi-line entry

        if self._show_line_numbers:
          subwindow.addstr(scroll_offset, y, str(line_number + 1).rjust(line_number_offset - 1), YELLOW, BOLD)

        x = line_number_offset + scroll_offset
        min_x = line_number_offset + scroll_offset

        x, y = subwindow.addstr_wrap(x, y, option, subwindow.width, min_x, GREEN, BOLD)
        x, y = subwindow.addstr_wrap(x, y, argument, subwindow.width, min_x, CYAN, BOLD)
        x, y = subwindow.addstr_wrap(x, y, comment, subwindow.width, min_x, WHITE)

        y += 1

      new_content_height = y + scroll - 1

    subwindow.addstr(0, 0, ' ' * subwindow.width)  # clear line
    location = ' (%s)' % self._torrc_location if self._torrc_location else ''
    subwindow.addstr(0, 0, 'Tor Configuration File%s:' % location, HIGHLIGHT)

    if self._last_content_height != new_content_height:
      self._last_content_height = new_content_height
      self.redraw()