[tor-commits] [arm/master] Relay options page for the setup wizard

atagar at torproject.org atagar at torproject.org
Sat Jun 25 03:07:48 UTC 2011


commit 2449cb349a24ab23872f415d0f192b25996f8588
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Jun 24 20:05:25 2011 -0700

    Relay options page for the setup wizard
    
    This has the display and navigation logic for the relay settings page of the
    wizard. The only bit missing is input validation of the text field inputs
    (for instance, checking that a bandwidth value will be recognized), but
    otherwise this part of the wizard is pretty much done.
---
 src/cli/wizard.py |  242 ++++++++++++++++++++++++++++++++++++++++++++---------
 src/settings.cfg  |   47 ++++++++---
 2 files changed, 237 insertions(+), 52 deletions(-)

diff --git a/src/cli/wizard.py b/src/cli/wizard.py
index f9dd770..a3404b9 100644
--- a/src/cli/wizard.py
+++ b/src/cli/wizard.py
@@ -1,6 +1,6 @@
 """
 Provides user prompts for setting up a new relay. This autogenerates a torrc
-that's used by arm to start its tor instance.
+that's used by arm to run its own tor instance.
 """
 
 import curses
@@ -13,23 +13,124 @@ from util import enum, uiTools
 # basic configuration types we can run as
 RelayType = enum.Enum("RELAY", "EXIT", "BRIDGE", "CLIENT")
 
+# all options that can be configured
+Options = enum.Enum("NICKNAME", "CONTACT", "NOTIFY", "BANDWIDTH", "LIMIT", "STARTUP")
+RelayOptions = (Options.NICKNAME, Options.CONTACT, Options.NOTIFY, Options.BANDWIDTH, Options.LIMIT, Options.STARTUP)
+
 # other options provided in the prompts
-CANCEL, BACK = "Cancel", "Back"
+CANCEL, NEXT, BACK = "Cancel", "Next", "Back"
+
+MSG_COLOR = "green"
+OPTION_COLOR = "yellow"
 
-CONFIG = {"wizard.role.message": "",
-          "wizard.role.option.label": {},
-          "wizard.role.option.description": {}}
+CONFIG = {"wizard.message.role": "",
+          "wizard.message.relay": "",
+          "wizard.toggle": {},
+          "wizard.default": {},
+          "wizard.label.general": {},
+          "wizard.label.role": {},
+          "wizard.label.opt": {},
+          "wizard.description.general": {},
+          "wizard.description.role": {},
+          "wizard.description.opt": {}}
 
 def loadConfig(config):
   config.update(CONFIG)
 
+class ConfigOption:
+  """
+  Attributes of a configuraition option.
+  """
+  
+  def __init__(self, key, group, default):
+    """
+    Configuration option constructor.
+    
+    Arguments:
+      key     - configuration option identifier used when querying attributes
+      group   - configuration attribute group this belongs to
+      default - initial value, uses the config default if unset
+    """
+    
+    self.key = key
+    self.group = group
+    self.descriptionCache = None
+    self.descriptionCacheArg = None
+    self.value = default
+  
+  def getKey(self):
+    return self.key
+  
+  def getValue(self):
+    return self.value
+  
+  def getDisplayValue(self):
+    return self.value
+  
+  def setValue(self, value):
+    self.value = value
+  
+  def getLabel(self, prefix = ""):
+    return prefix + CONFIG["wizard.label.%s" % self.group].get(self.key, "")
+  
+  def getDescription(self, width, prefix = ""):
+    if not self.descriptionCache or self.descriptionCacheArg != width:
+      optDescription = CONFIG["wizard.description.%s" % self.group].get(self.key, "")
+      self.descriptionCache = _splitStr(optDescription, width)
+      self.descriptionCacheArg = width
+    
+    return [prefix + line for line in self.descriptionCache]
+
+class ToggleConfigOption(ConfigOption):
+  def __init__(self, key, group, default, trueLabel, falseLabel):
+    ConfigOption.__init__(self, key, group, default)
+    self.trueLabel = trueLabel
+    self.falseLabel = falseLabel
+  
+  def getDisplayValue(self):
+    return self.trueLabel if self.value else self.falseLabel
+  
+  def toggle(self):
+    self.value = not self.value
+
 def showWizard():
-  myRelayType = promptRelayType()
+  relayType, config = None, {}
+  
+  for option in Options.values():
+    toggleValues = CONFIG["wizard.toggle"].get(option)
+    default = CONFIG["wizard.default"].get(option, "")
+    
+    if toggleValues:
+      if "," in toggleValues:
+        trueLabel, falseLabel = toggleValues.split(",", 1)
+      else: trueLabel, falseLabel = toggleValues, ""
+      
+      isSet = default.lower() == "true"
+      config[option] = ToggleConfigOption(option, "opt", isSet, trueLabel.strip(), falseLabel.strip())
+    else: config[option] = ConfigOption(option, "opt", default)
+  
+  while True:
+    if relayType == None:
+      selection = promptRelayType()
+      
+      if selection == CANCEL: break
+      else: relayType = selection
+    else:
+      if relayType == RelayType.RELAY:
+        selection = promptRelayOptions(config)
+        
+        if selection == BACK: relayType = None
+        elif selection == NEXT: break # TODO: implement next screen
+      else:
+        break # TODO: other catagories not yet implemented
+    
+    # redraws screen to clear away the dialog we just showed
+    cli.controller.getController().requestRedraw(True)
 
 def promptRelayType():
   """
   Provides a prompt for selecting the general role we'd like Tor to run with.
-  This returns a RelayType enumeration for the selection, or None if the
+  This returns a RelayType enumeration for the selection, or CANCEL if the
   dialog was canceled.
   """
   
@@ -37,66 +138,127 @@ def promptRelayType():
   if not popup: return
   control = cli.controller.getController()
   key, selection = 0, 0
-  
-  # constructs (enum, label, [description lines]) tuples for our options
-  options = []
-  
-  for runType in RelayType.values() + [CANCEL]:
-    label = CONFIG["wizard.role.option.label"].get(runType, "")
-    descRemainder = CONFIG["wizard.role.option.description"].get(runType, "")
-    descLines = []
-    
-    while descRemainder:
-      descLine, descRemainder = uiTools.cropStr(descRemainder, 52, None, endType = None, getRemainder = True)
-      descLines.append(descLine.strip())
-    
-    options.append((runType, label, descLines))
+  options = [ConfigOption(opt, "role", opt) for opt in RelayType.values()]
+  options.append(ConfigOption(CANCEL, "general", CANCEL))
   
   try:
     popup.win.box()
     curses.cbreak()
-    format = uiTools.getColor("green")
-    y, msgRemainder = 1, CONFIG["wizard.role.message"]
     
     # provides the welcoming message
-    while msgRemainder:
-      msg, msgRemainder = uiTools.cropStr(msgRemainder, 54, None, endType = None, getRemainder = True)
-      popup.addstr(y, 2, msg.strip(), format | curses.A_BOLD)
-      y += 1
+    topContent = _splitStr(CONFIG["wizard.message.role"], 54)
+    for i in range(len(topContent)):
+      popup.addstr(i + 1, 2, topContent[i], curses.A_BOLD | uiTools.getColor(MSG_COLOR))
     
-    while not uiTools.isSelectionKey(key):
-      offset = 0
+    while True:
+      y, offset = len(topContent) + 1, 0
       
       for i in range(len(options)):
-        _, label, lines = options[i]
-        optionFormat = format | curses.A_STANDOUT if i == selection else format
-        
-        # appends an extra space to the start to provide nicer centering
-        label = " " + label
-        lines = [" " + line for line in lines]
+        optionFormat = uiTools.getColor(MSG_COLOR)
+        if i == selection: optionFormat |= curses.A_STANDOUT
         
         # Curses has a weird bug where there's a one-pixel alignment
         # difference between bold and regular text, so it looks better
         # to render the whitespace here as not being bold.
         
         offset += 1
+        label = options[i].getLabel(" ")
         popup.addstr(y + offset, 2, label, optionFormat | curses.A_BOLD)
         popup.addstr(y + offset, 2 + len(label), " " * (54 - len(label)), optionFormat)
         offset += 1
         
-        for line in lines:
+        for line in options[i].getDescription(52, " "):
           popup.addstr(y + offset, 2, uiTools.padStr(line, 54), optionFormat)
           offset += 1
       
       popup.win.refresh()
       key = control.getScreen().getch()
       
-      if key == curses.KEY_UP: selection = max(0, selection - 1)
-      elif key == curses.KEY_DOWN: selection = min(len(options) - 1, selection + 1)
+      if key == curses.KEY_UP: selection = (selection - 1) % len(options)
+      elif key == curses.KEY_DOWN: selection = (selection + 1) % len(options)
+      elif uiTools.isSelectionKey(key): return options[selection].getValue()
+      elif key == 27: return CANCEL # esc - cancel
+  finally:
+    cli.popups.finalize()
+
+def promptRelayOptions(config):
+  """
+  Prompts the user for the configuration of an internal relay.
+  """
+  
+  popup, _, _ = cli.popups.init(23, 58)
+  if not popup: return
+  control = cli.controller.getController()
+  options = [config[opt] for opt in RelayOptions]
+  options.append(ConfigOption(BACK, "general", "(to role selection)"))
+  options.append(ConfigOption(NEXT, "general", "(to confirm options)"))
+  key, selection = 0, 0
+  
+  try:
+    curses.cbreak()
+    
+    while True:
+      popup.win.erase()
+      popup.win.box()
+      
+      # provides the description for internal relays
+      topContent = _splitStr(CONFIG["wizard.message.relay"], 54)
+      for i in range(len(topContent)):
+        popup.addstr(i + 1, 2, topContent[i], curses.A_BOLD | uiTools.getColor(MSG_COLOR))
+      
+      y, offset = len(topContent) + 1, 0
+      for i in range(len(options)):
+        label = " %-30s%s" % (options[i].getLabel(), options[i].getDisplayValue())
+        optionFormat = curses.A_BOLD | uiTools.getColor(OPTION_COLOR)
+        if i == selection: optionFormat |= curses.A_STANDOUT
+        
+        offset += 1
+        popup.addstr(y + offset, 2, uiTools.padStr(label, 54), optionFormat)
+        
+        # extra space to divide options/navigation
+        if i == len(options) - 3: offset += 1
+      
+      # divider between the options and description
+      offset += 2
+      popup.addch(y + offset, 0, curses.ACS_LTEE)
+      popup.addch(y + offset, popup.getWidth() - 1, curses.ACS_RTEE)
+      popup.hline(y + offset, 1, popup.getWidth() - 2)
+      
+      # description for the currently selected option
+      for line in options[selection].getDescription(54, " "):
+        offset += 1
+        popup.addstr(y + offset, 1, line, uiTools.getColor(MSG_COLOR))
+      
+      popup.win.refresh()
+      key = control.getScreen().getch()
+      
+      if key == curses.KEY_UP: selection = (selection - 1) % len(options)
+      elif key == curses.KEY_DOWN: selection = (selection + 1) % len(options)
+      elif uiTools.isSelectionKey(key):
+        if selection == len(options) - 2: return BACK # selected back
+        elif selection == len(options) - 1: return NEXT # selected next
+        elif isinstance(options[selection], ToggleConfigOption):
+          options[selection].toggle()
+        else:
+          newValue = popup.getstr(y + selection + 1, 33, options[selection].getValue(), curses.A_STANDOUT | uiTools.getColor(OPTION_COLOR), 23)
+          if newValue: options[selection].setValue(newValue.strip())
       elif key == 27: selection, key = -1, curses.KEY_ENTER # esc - cancel
   finally:
     cli.popups.finalize()
+
+def _splitStr(msg, width):
+  """
+  Splits a string into substrings of a given length.
+  
+  Arguments:
+    msg   - string to be broken up
+    width - max length of any returned substring
+  """
+  
+  results = []
+  while msg:
+    msgSegment, msg = uiTools.cropStr(msg, width, None, endType = None, getRemainder = True)
+    results.append(msgSegment.strip())
   
-  selectedOption = options[selection][0]
-  return None if selectedOption == CANCEL else selectedOption
+  return results
 
diff --git a/src/settings.cfg b/src/settings.cfg
index 2ad2784..94ff9c1 100644
--- a/src/settings.cfg
+++ b/src/settings.cfg
@@ -340,20 +340,43 @@ msg.ARM_DEBUG GETINFO traffic/written
 msg.ARM_DEBUG GETCONF
 msg.ARM_DEBUG Unable to query process resource usage from ps
 
-# descriptions used in the relay setup wizard
-wizard.role.message Welcome to the Tor network! This will step you through the configuration process for being a part of it. To start with, what role would you like to have?
+# configuration option attributes used in the relay setup wizard
+wizard.message.role Welcome to the Tor network! This will step you through the configuration process for becoming a part of it. To start with, what role would you like to have?
+wizard.message.relay Internal relays provide connections within the Tor network. Since you will only be connecting to Tor users and relays this is an easy, hassle free way of helping to make the network better.
 
-wizard.role.option.label Relay => Internal Relay
-wizard.role.option.label Exit => Exit Relay
-wizard.role.option.label Bridge => Bridge
-wizard.role.option.label Client => Client
-wizard.role.option.label Cancel => Cancel
+wizard.toggle Notify => Yes, No
+wizard.toggle Startup => Yes, No
 
-wizard.role.option.description Relay => Provides interconnections with other Tor relays. This is a safe and easy of making the network better.
-wizard.role.option.description Exit => Connects between Tor an the outside Internet. This is a vital role, but can lead to abuse complaints.
-wizard.role.option.description Bridge => Non-public relay specifically for helping censored users.
-wizard.role.option.description Client => Use the network without contributing to it.
-wizard.role.option.description Cancel => Close without starting Tor.
+wizard.default Nickname => Unnamed
+wizard.default Notify => true
+wizard.default Bandwidth => 5 MB/s
+wizard.default Startup => true
+
+wizard.label.general Cancel => Cancel
+wizard.label.general Back => Previous
+wizard.label.general Next => Next
+wizard.label.role Relay => Internal Relay
+wizard.label.role Exit => Exit Relay
+wizard.label.role Bridge => Bridge
+wizard.label.role Client => Client
+wizard.label.opt Nickname => Nickname
+wizard.label.opt Contact => Contact Information
+wizard.label.opt Notify => Issue Notification
+wizard.label.opt Bandwidth => Relay Speed
+wizard.label.opt Limit => Monthly Limit
+wizard.label.opt Startup => Run At Startup
+
+wizard.description.general Cancel => Close without starting Tor.
+wizard.description.role Relay => Provides interconnections with other Tor relays. This is a safe and easy of making the network better.
+wizard.description.role Exit => Connects between Tor an the outside Internet. This is a vital role, but can lead to abuse complaints.
+wizard.description.role Bridge => Non-public relay specifically for helping censored users.
+wizard.description.role Client => Use the network without contributing to it.
+wizard.description.opt Nickname => Human friendly name for your relay. If this is unique then it's used instead of your fingerprint (a forty character hex string) when pages like TorStatus refer to you.
+wizard.description.opt Contact => Address we can contact you at if there's a problem with your relay. This is public information so, if it looks like an email address, we'll obscure it a bit.
+wizard.description.opt Notify => Sends automated email notifications to the above address if your relay is unreachable or out of date. This service is provided by Tor Weather (https://weather.torproject.org/) and will send you a confirmation email before it's started.
+wizard.description.opt Bandwidth => Limit for the average rate at which you relay traffic.
+wizard.description.opt Limit => Maximum amount of traffic to relay each month. Some ISPs, like Comcast, cap their customer's Internet usage so this is an easy way of staying below that limit.
+wizard.description.opt Startup => Runs Tor in the background when the system starts.
 
 # some config options are fetched via special values
 torrc.map HiddenServiceDir => HiddenServiceOptions



More information about the tor-commits mailing list