[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] r24249: {arm} Connection details popup for the rewritten page. Changes fro (in arm/trunk/src: interface interface/connections util)
Author: atagar
Date: 2011-02-21 06:00:17 +0000 (Mon, 21 Feb 2011)
New Revision: 24249
Modified:
arm/trunk/src/interface/configPanel.py
arm/trunk/src/interface/connections/connPanel.py
arm/trunk/src/interface/connections/listings.py
arm/trunk/src/interface/controller.py
arm/trunk/src/util/torTools.py
arm/trunk/src/util/uiTools.py
Log:
Connection details popup for the rewritten page. Changes from the original version (besides being a sane, maintainable implementation) is:
- using the consensus exit policies rather than the longer descriptor versions (which never fit anyway...)
- displaying connection details no longer freezes the rest of the display
- detail panel is dynamically resizable
- more resilient to missing descriptors
Modified: arm/trunk/src/interface/configPanel.py
===================================================================
--- arm/trunk/src/interface/configPanel.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/configPanel.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -305,40 +305,40 @@
def _getConfigOptions(self):
return self.confContents if self.showAll else self.confImportantContents
- def _drawSelectionPanel(self, cursorSelection, width, detailPanelHeight, isScrollbarVisible):
+ def _drawSelectionPanel(self, selection, width, detailPanelHeight, isScrollbarVisible):
"""
Renders a panel for the selected configuration option.
"""
# This is a solid border unless the scrollbar is visible, in which case a
# 'T' pipe connects the border to the bar.
- uiTools.drawBox(self, 0, 0, width, detailPanelHeight)
+ uiTools.drawBox(self, 0, 0, width, detailPanelHeight + 1)
if isScrollbarVisible: self.addch(detailPanelHeight, 1, curses.ACS_TTEE)
- selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[cursorSelection.get(Field.CATEGORY)])
+ selectionFormat = curses.A_BOLD | uiTools.getColor(CATEGORY_COLOR[selection.get(Field.CATEGORY)])
# first entry:
# <option> (<category> Option)
- optionLabel =" (%s Option)" % cursorSelection.get(Field.CATEGORY)
- self.addstr(1, 2, cursorSelection.get(Field.OPTION) + optionLabel, selectionFormat)
+ optionLabel =" (%s Option)" % selection.get(Field.CATEGORY)
+ self.addstr(1, 2, selection.get(Field.OPTION) + optionLabel, selectionFormat)
# second entry:
# Value: <value> ([default|custom], <type>, usage: <argument usage>)
if detailPanelHeight >= 3:
valueAttr = []
- valueAttr.append("default" if cursorSelection.get(Field.IS_DEFAULT) else "custom")
- valueAttr.append(cursorSelection.get(Field.TYPE))
- valueAttr.append("usage: %s" % (cursorSelection.get(Field.ARG_USAGE)))
+ valueAttr.append("default" if selection.get(Field.IS_DEFAULT) else "custom")
+ valueAttr.append(selection.get(Field.TYPE))
+ valueAttr.append("usage: %s" % (selection.get(Field.ARG_USAGE)))
valueAttrLabel = ", ".join(valueAttr)
valueLabelWidth = width - 12 - len(valueAttrLabel)
- valueLabel = uiTools.cropStr(cursorSelection.get(Field.VALUE), valueLabelWidth)
+ valueLabel = uiTools.cropStr(selection.get(Field.VALUE), valueLabelWidth)
self.addstr(2, 2, "Value: %s (%s)" % (valueLabel, valueAttrLabel), selectionFormat)
# remainder is filled with the man page description
descriptionHeight = max(0, detailPanelHeight - 3)
- descriptionContent = "Description: " + cursorSelection.get(Field.DESCRIPTION)
+ descriptionContent = "Description: " + selection.get(Field.DESCRIPTION)
for i in range(descriptionHeight):
# checks if we're done writing the description
Modified: arm/trunk/src/interface/connections/connPanel.py
===================================================================
--- arm/trunk/src/interface/connections/connPanel.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/connections/connPanel.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -7,10 +7,15 @@
import threading
from interface.connections import listings
-from util import connections, enum, log, panel, uiTools
+from util import connections, enum, log, panel, torTools, uiTools
+REDRAW_RATE = 10 # TODO: make a config option
+
DEFAULT_CONFIG = {}
+# height of the detail panel content, not counting top and bottom border
+DETAILS_HEIGHT = 7
+
# listing types
Listing = enum.Enum(("IP", "IP Address"), "HOSTNAME", "FINGERPRINT", "NICKNAME")
@@ -36,6 +41,7 @@
self.scroller = uiTools.Scroller(True)
self._title = "Connections:" # title line of the panel
self._connections = [] # last fetched connections
+ self._showDetails = False # presents the details panel if true
self._lastUpdate = -1 # time the content was last revised
self._isPaused = True # prevents updates if true
@@ -52,6 +58,8 @@
self._update() # populates initial entries
# TODO: should listen for tor shutdown
+ # TODO: hasn't yet had its pausing functionality tested (for instance, the
+ # key handler still accepts events when paused)
def setPaused(self, isPause):
"""
@@ -73,8 +81,12 @@
if uiTools.isScrollKey(key):
pageHeight = self.getPreferredSize()[0] - 1
+ if self._showDetails: pageHeight -= (DETAILS_HEIGHT + 1)
isChanged = self.scroller.handleKey(key, self._connections, pageHeight)
if isChanged: self.redraw(True)
+ elif uiTools.isSelectionKey(key):
+ self._showDetails = not self._showDetails
+ self.redraw(True)
self.valsLock.release()
@@ -87,7 +99,7 @@
while not self._halt:
currentTime = time.time()
- if self._isPaused or currentTime - lastDraw < 1:
+ if self._isPaused or currentTime - lastDraw < REDRAW_RATE:
self._cond.acquire()
if not self._halt: self._cond.wait(0.2)
self._cond.release()
@@ -95,26 +107,35 @@
# updates content if their's new results, otherwise just redraws
self._update()
self.redraw(True)
- lastDraw += 1
+ lastDraw += REDRAW_RATE
def draw(self, width, height):
self.valsLock.acquire()
- # title label with connection counts
- self.addstr(0, 0, self._title, curses.A_STANDOUT)
+ # extra line when showing the detail panel is for the bottom border
+ detailPanelOffset = DETAILS_HEIGHT + 1 if self._showDetails else 0
+ isScrollbarVisible = len(self._connections) > height - detailPanelOffset - 1
- scrollLoc = self.scroller.getScrollLoc(self._connections, height - 1)
+ scrollLoc = self.scroller.getScrollLoc(self._connections, height - detailPanelOffset - 1)
cursorSelection = self.scroller.getCursorSelection(self._connections)
+ # draws the detail panel if currently displaying it
+ if self._showDetails:
+ self._drawSelectionPanel(cursorSelection, width, isScrollbarVisible)
+
+ # title label with connection counts
+ title = "Connection Details:" if self._showDetails else self._title
+ self.addstr(0, 0, title, curses.A_STANDOUT)
+
scrollOffset = 0
- if len(self._connections) > height - 1:
+ if isScrollbarVisible:
scrollOffset = 3
- self.addScrollBar(scrollLoc, scrollLoc + height - 1, len(self._connections), 1)
+ self.addScrollBar(scrollLoc, scrollLoc + height - detailPanelOffset - 1, len(self._connections), 1 + detailPanelOffset)
currentTime = self._pauseTime if self._pauseTime else time.time()
for lineNum in range(scrollLoc, len(self._connections)):
entry = self._connections[lineNum]
- drawLine = lineNum + 1 - scrollLoc
+ drawLine = lineNum + detailPanelOffset + 1 - scrollLoc
entryType = entry.getType()
lineFormat = uiTools.getColor(listings.CATEGORY_COLOR[entryType])
@@ -203,4 +224,113 @@
self._connections = newConnections
self._lastResourceFetch = currentResolutionCount
self.valsLock.release()
+
+ def _drawSelectionPanel(self, selection, width, isScrollbarVisible):
+ """
+ Renders a panel for details on the selected connnection.
+ """
+
+ # This is a solid border unless the scrollbar is visible, in which case a
+ # 'T' pipe connects the border to the bar.
+ uiTools.drawBox(self, 0, 0, width, DETAILS_HEIGHT + 2)
+ if isScrollbarVisible: self.addch(DETAILS_HEIGHT + 1, 1, curses.ACS_TTEE)
+
+ selectionFormat = curses.A_BOLD | uiTools.getColor(listings.CATEGORY_COLOR[selection.getType()])
+ lines = [""] * 7
+
+ lines[0] = "address: %s" % selection.getDestinationLabel(width - 11, listings.DestAttr.NONE)
+ lines[1] = "locale: %s" % ("??" if selection.isPrivate() else selection.foreign.getLocale())
+
+ # Remaining data concerns the consensus results, with three possible cases:
+ # - if there's a single match then display its details
+ # - if there's multiple potenial relays then list all of the combinations
+ # of ORPorts / Fingerprints
+ # - if no consensus data is available then say so (probably a client or
+ # exit connection)
+
+ fingerprint = selection.foreign.getFingerprint()
+ conn = torTools.getConn()
+
+ if fingerprint != "UNKNOWN":
+ # single match - display information available about it
+ nsEntry = conn.getConsensusEntry(fingerprint)
+ descEntry = conn.getDescriptorEntry(fingerprint)
+
+ # append the fingerprint to the second line
+ lines[1] = "%-13sfingerprint: %s" % (lines[1], fingerprint)
+
+ if nsEntry:
+ # example consensus entry:
+ # r murble R8sCM1ar1sS2GulQYFVmvN95xsk RJr6q+wkTFG+ng5v2bdCbVVFfA4 2011-02-21 00:25:32 195.43.157.85 443 0
+ # s Exit Fast Guard Named Running Stable Valid
+ # w Bandwidth=2540
+ # p accept 20-23,43,53,79-81,88,110,143,194,443
+
+ nsLines = nsEntry.split("\n")
+
+ firstLineComp = nsLines[0].split(" ")
+ if len(firstLineComp) >= 9:
+ _, nickname, _, _, pubDate, pubTime, _, orPort, dirPort = firstLineComp[:9]
+ else: nickname, pubDate, pubTime, orPort, dirPort = "", "", "", "", ""
+
+ flags = nsLines[1][2:]
+ microExit = nsLines[3][2:]
+
+ dirPortLabel = "" if dirPort == "0" else "dirport: %s" % dirPort
+ lines[2] = "nickname: %-25s orport: %-10s %s" % (nickname, orPort, dirPortLabel)
+ lines[3] = "published: %s %s" % (pubDate, pubTime)
+ lines[4] = "flags: %s" % flags.replace(" ", ", ")
+ lines[5] = "exit policy: %s" % microExit.replace(",", ", ")
+
+ if descEntry:
+ torVersion, patform, contact = "", "", ""
+
+ for descLine in descEntry.split("\n"):
+ if descLine.startswith("platform"):
+ # has the tor version and platform, ex:
+ # platform Tor 0.2.1.29 (r318f470bc5f2ad43) on Linux x86_64
+
+ torVersion = descLine[13:descLine.find(" ", 13)]
+ platform = descLine[descLine.rfind(" on ") + 4:]
+ elif descLine.startswith("contact"):
+ contact = descLine[8:]
+
+ # clears up some highly common obscuring
+ for alias in (" at ", " AT "): contact = contact.replace(alias, "@")
+ for alias in (" dot ", " DOT "): contact = contact.replace(alias, ".")
+
+ break # contact lines come after the platform
+
+ lines[3] = "%-36s os: %-14s version: %s" % (lines[3], platform, torVersion)
+
+ # contact information is an optional field
+ if contact: lines[6] = "contact: %s" % contact
+ else:
+ allMatches = conn.getRelayFingerprint(selection.foreign.getIpAddr(), getAllMatches = True)
+
+ if allMatches:
+ # multiple matches
+ lines[2] = "Muliple matches, possible fingerprints are:"
+
+ for i in range(len(allMatches)):
+ isLastLine = i == 3
+
+ relayPort, relayFingerprint = allMatches[i]
+ lineText = "%i. or port: %-5s fingerprint: %s" % (i, relayPort, relayFingerprint)
+
+ # if there's multiple lines remaining at the end then give a count
+ remainingRelays = len(allMatches) - i
+ if isLastLine and remainingRelays > 1:
+ lineText = "... %i more" % remainingRelays
+
+ lines[3 + i] = lineText
+
+ if isLastLine: break
+ else:
+ # no consensus entry for this ip address
+ lines[2] = "No consensus data found"
+
+ for i in range(len(lines)):
+ lineText = uiTools.cropStr(lines[i], width - 2)
+ self.addstr(1 + i, 2, lineText, selectionFormat)
Modified: arm/trunk/src/interface/connections/listings.py
===================================================================
--- arm/trunk/src/interface/connections/listings.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/connections/listings.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -16,6 +16,7 @@
# Control Tor controller (arm, vidalia, etc).
# TODO: add recognizing of CLIENT connection type
+DestAttr = enum.Enum("NONE", "LOCALE", "HOSTNAME")
Category = enum.Enum("INBOUND", "OUTBOUND", "EXIT", "SOCKS", "CLIENT", "DIRECTORY", "CONTROL")
CATEGORY_COLOR = {Category.INBOUND: "green", Category.OUTBOUND: "blue",
Category.EXIT: "red", Category.SOCKS: "cyan",
@@ -160,6 +161,56 @@
return Category.EXIT if isExitConnection else Category.OUTBOUND
else: return self.baseType
+ def getDestinationLabel(self, maxLength, extraAttr=DestAttr.NONE):
+ """
+ Provides a short description of the destination. This is made up of two
+ components, the base <ip addr>:<port> and an extra piece of information in
+ parentheses. The IP address is scrubbed from private connections.
+
+ Extra information is...
+ - the port's purpose for exit connections
+ - the extraAttr if the address isn't private and isn't on the local network
+ - nothing otherwise
+
+ Arguments:
+ maxLength - maximum length of the string returned
+ """
+
+ # destination of the connection
+ if self.isPrivate():
+ dstAddress = "<scrubbed>:%s" % self.foreign.getPort()
+ else:
+ dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
+
+ # Only append the extra info if there's at least a couple characters of
+ # space (this is what's needed for the country codes).
+ if len(dstAddress) + 5 <= maxLength:
+ spaceAvailable = maxLength - len(dstAddress) - 3
+
+ if self.getType() == Category.EXIT:
+ purpose = connections.getPortUsage(self.foreign.getPort())
+
+ if purpose:
+ # BitTorrent is a common protocol to truncate, so just use "Torrent"
+ # if there's not enough room.
+ if len(purpose) > spaceAvailable and purpose == "BitTorrent":
+ purpose = "Torrent"
+
+ # crops with a hyphen if too long
+ purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
+
+ dstAddress += " (%s)" % purpose
+ elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
+ if extraAttr == DestAttr.LOCALE:
+ dstAddress += " (%s)" % self.foreign.getLocale()
+ elif extraAttr == DestAttr.HOSTNAME:
+ dstHostname = self.foreign.getHostname()
+
+ if dstHostname:
+ dstAddress += " (%s)" % uiTools.cropStr(dstHostname, spaceAvailable)
+
+ return dstAddress[:maxLength]
+
def isPrivate(self):
"""
Returns true if the endpoint is private, possibly belonging to a client
@@ -228,36 +279,8 @@
conn = torTools.getConn()
myType = self.getType()
+ dstAddress = self.getDestinationLabel(26, DestAttr.LOCALE)
- # destination of the connection
- if self.isPrivate():
- dstAddress = "<scrubbed>:%s" % self.foreign.getPort()
- else:
- dstAddress = "%s:%s" % (self.foreign.getIpAddr(), self.foreign.getPort())
-
- # Appends an extra field which could be...
- # - the port's purpose for exits
- # - locale for most other connections
- # - blank if it's on the local network
-
- if myType == Category.EXIT:
- purpose = connections.getPortUsage(self.foreign.getPort())
-
- if purpose:
- spaceAvailable = 26 - len(dstAddress) - 3
-
- # BitTorrent is a common protocol to truncate, so just use "Torrent"
- # if there's not enough room.
- if len(purpose) > spaceAvailable and purpose == "BitTorrent":
- purpose = "Torrent"
-
- # crops with a hyphen if too long
- purpose = uiTools.cropStr(purpose, spaceAvailable, endType = uiTools.Ending.HYPHEN)
-
- dstAddress += " (%s)" % purpose
- elif not connections.isIpAddressPrivate(self.foreign.getIpAddr()):
- dstAddress += " (%s)" % self.foreign.getLocale()
-
src, dst, etc = "", "", ""
if listingType == connPanel.Listing.IP:
# base data requires 73 characters
Modified: arm/trunk/src/interface/controller.py
===================================================================
--- arm/trunk/src/interface/controller.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/interface/controller.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -745,7 +745,7 @@
isResize = lastSize != newSize
lastSize = newSize
- if panelKey in ("header", "graph", "log", "config", "torrc"):
+ if panelKey in ("header", "graph", "log", "config", "torrc", "conn2"):
# revised panel (manages its own content refreshing)
panels[panelKey].redraw(isResize)
else:
@@ -1280,7 +1280,9 @@
hostnames.setPaused(True)
panels["conn"].sortConnections()
elif page == 1 and panels["conn"].isCursorEnabled and uiTools.isSelectionKey(key):
- # TODO: deprecated when migrated to the new connection panel
+ # TODO: deprecated when migrated to the new connection panel, thought as
+ # well keep around until there's a counterpart for hostname fetching
+
# provides details on selected connection
panel.CURSES_LOCK.acquire()
try:
Modified: arm/trunk/src/util/torTools.py
===================================================================
--- arm/trunk/src/util/torTools.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/util/torTools.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -246,6 +246,8 @@
self._fingerprintLookupCache = {} # lookup cache with (ip, port) -> fingerprint mappings
self._fingerprintsAttachedCache = None # cache of relays we're connected to
self._nicknameLookupCache = {} # lookup cache with fingerprint -> nickname mappings
+ self._consensusLookupCache = {} # lookup cache with network status entries
+ self._descriptorLookupCache = {} # lookup cache with relay descriptors
self._isReset = False # internal flag for tracking resets
self._status = State.CLOSED # current status of the attached control port
self._statusTime = 0 # unix time-stamp for the duration of the status
@@ -299,6 +301,8 @@
self._fingerprintLookupCache = {}
self._fingerprintsAttachedCache = None
self._nicknameLookupCache = {}
+ self._consensusLookupCache = {}
+ self._descriptorLookupCache = {}
self._exitPolicyChecker = self.getExitPolicy()
self._isExitingAllowed = self._exitPolicyChecker.isExitingAllowed()
@@ -805,8 +809,55 @@
return result
- def getRelayFingerprint(self, relayAddress, relayPort = None):
+ def getConsensusEntry(self, relayFingerprint):
"""
+ Provides the most recently available consensus information for the given
+ relay. This is none if no such information exists.
+
+ Arguments:
+ relayFingerprint - fingerprint of the relay
+ """
+
+ self.connLock.acquire()
+
+ result = None
+ if self.isAlive():
+ if not relayFingerprint in self._consensusLookupCache:
+ nsEntry = self.getInfo("ns/id/%s" % relayFingerprint)
+ self._consensusLookupCache[relayFingerprint] = nsEntry
+
+ result = self._consensusLookupCache[relayFingerprint]
+
+ self.connLock.release()
+
+ return result
+
+ def getDescriptorEntry(self, relayFingerprint):
+ """
+ Provides the most recently available descriptor information for the given
+ relay. Unless FetchUselessDescriptors is set this may frequently be
+ unavailable. If no such descriptor is available then this returns None.
+
+ Arguments:
+ relayFingerprint - fingerprint of the relay
+ """
+
+ self.connLock.acquire()
+
+ result = None
+ if self.isAlive():
+ if not relayFingerprint in self._descriptorLookupCache:
+ descEntry = self.getInfo("desc/id/%s" % relayFingerprint)
+ self._descriptorLookupCache[relayFingerprint] = descEntry
+
+ result = self._descriptorLookupCache[relayFingerprint]
+
+ self.connLock.release()
+
+ return result
+
+ def getRelayFingerprint(self, relayAddress, relayPort = None, getAllMatches = False):
+ """
Provides the fingerprint associated with the given address. If there's
multiple potential matches or the mapping is unknown then this returns
None. This disambiguates the fingerprint if there's multiple relays on
@@ -814,20 +865,32 @@
we have a connection with.
Arguments:
- relayAddress - address of relay to be returned
- relayPort - orport of relay (to further narrow the results)
+ relayAddress - address of relay to be returned
+ relayPort - orport of relay (to further narrow the results)
+ getAllMatches - ignores the relayPort and provides all of the
+ (port, fingerprint) tuples matching the given
+ address
"""
self.connLock.acquire()
result = None
if self.isAlive():
- # query the fingerprint if it isn't yet cached
- if not (relayAddress, relayPort) in self._fingerprintLookupCache:
- relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
- self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
-
- result = self._fingerprintLookupCache[(relayAddress, relayPort)]
+ if getAllMatches:
+ # populates the ip -> fingerprint mappings if not yet available
+ if self._fingerprintMappings == None:
+ self._fingerprintMappings = self._getFingerprintMappings()
+
+ if relayAddress in self._fingerprintMappings:
+ result = self._fingerprintMappings[relayAddress]
+ else: result = []
+ else:
+ # query the fingerprint if it isn't yet cached
+ if not (relayAddress, relayPort) in self._fingerprintLookupCache:
+ relayFingerprint = self._getRelayFingerprint(relayAddress, relayPort)
+ self._fingerprintLookupCache[(relayAddress, relayPort)] = relayFingerprint
+
+ result = self._fingerprintLookupCache[(relayAddress, relayPort)]
self.connLock.release()
@@ -854,7 +917,7 @@
self._nicknameLookupCache[relayFingerprint] = myNickname
else:
# check the consensus for the relay
- nsEntry = self.getInfo("ns/id/%s" % relayFingerprint)
+ nsEntry = self.getConsensusEntry(relayFingerprint)
if nsEntry: relayNickname = nsEntry[2:nsEntry.find(" ", 2)]
else: relayNickname = None
@@ -1108,6 +1171,7 @@
def ns_event(self, event):
self._updateHeartbeat()
+ self._consensusLookupCache = {}
myFingerprint = self.getInfo("fingerprint")
if myFingerprint:
@@ -1135,6 +1199,7 @@
self._fingerprintLookupCache = {}
self._fingerprintsAttachedCache = None
self._nicknameLookupCache = {}
+ self._consensusLookupCache = {}
if self._fingerprintMappings != None:
self._fingerprintMappings = self._getFingerprintMappings(event.nslist)
@@ -1155,6 +1220,7 @@
# the new relays.
self._fingerprintLookupCache = {}
self._fingerprintsAttachedCache = None
+ self._descriptorLookupCache = {}
if self._fingerprintMappings != None:
for fingerprint in event.idlist:
Modified: arm/trunk/src/util/uiTools.py
===================================================================
--- arm/trunk/src/util/uiTools.py 2011-02-20 21:31:49 UTC (rev 24248)
+++ arm/trunk/src/util/uiTools.py 2011-02-21 06:00:17 UTC (rev 24249)
@@ -166,6 +166,7 @@
# checks if there isn't the minimum space needed to include anything
lastWordbreak = msg.rfind(" ", 0, size + 1)
+ lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces
if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1):
if getRemainder: return ("", msg)
else: return ""
@@ -183,13 +184,14 @@
returnMsg, remainder = msg[:size], msg[size:]
if endType == Ending.HYPHEN:
remainder = returnMsg[-1] + remainder
- returnMsg = returnMsg[:-1] + "-"
+ returnMsg = returnMsg[:-1].rstrip() + "-"
else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
# if this is ending with a comma or period then strip it off
if not getRemainder and returnMsg[-1] in (",", "."): returnMsg = returnMsg[:-1]
- if endType == Ending.ELLIPSE: returnMsg += "..."
+ if endType == Ending.ELLIPSE:
+ returnMsg = returnMsg.rstrip() + "..."
if getRemainder: return (returnMsg, remainder)
else: return returnMsg
@@ -209,17 +211,17 @@
# draws the top and bottom
panel.hline(top, left + 1, width - 1, attr)
- panel.hline(top + height, left + 1, width - 1, attr)
+ panel.hline(top + height - 1, left + 1, width - 1, attr)
# draws the left and right sides
- panel.vline(top + 1, left, height - 1, attr)
- panel.vline(top + 1, left + width, height - 1, attr)
+ panel.vline(top + 1, left, height - 2, attr)
+ panel.vline(top + 1, left + width, height - 2, attr)
# draws the corners
panel.addch(top, left, curses.ACS_ULCORNER, attr)
panel.addch(top, left + width, curses.ACS_URCORNER, attr)
- panel.addch(top + height, left, curses.ACS_LLCORNER, attr)
- panel.addch(top + height, left + width, curses.ACS_LRCORNER, attr)
+ panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
+ panel.addch(top + height - 1, left + width, curses.ACS_LRCORNER, attr)
def isSelectionKey(key):
"""
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits