This file is indexed.

/usr/share/arm/util/panel.py is in tor-arm 1.4.5.0-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
"""
Wrapper for safely working with curses subwindows.
"""

import copy
import time
import curses
import curses.ascii
import curses.textpad
from threading import RLock

from util import log, textInput, uiTools

# global ui lock governing all panel instances (curses isn't thread save and 
# concurrency bugs produce especially sinister glitches)
CURSES_LOCK = RLock()

# tags used by addfstr - this maps to functor/argument combinations since the
# actual values (in the case of color attributes) might not yet be initialized
def _noOp(arg): return arg
FORMAT_TAGS = {"<b>": (_noOp, curses.A_BOLD),
               "<u>": (_noOp, curses.A_UNDERLINE),
               "<h>": (_noOp, curses.A_STANDOUT)}
for colorLabel in uiTools.COLOR_LIST: FORMAT_TAGS["<%s>" % colorLabel] = (uiTools.getColor, colorLabel)

CONFIG = {"log.panelRecreated": log.DEBUG}

# prevents curses redraws if set
HALT_ACTIVITY = False

def loadConfig(config):
  config.update(CONFIG)

class Panel():
  """
  Wrapper for curses subwindows. This hides most of the ugliness in common
  curses operations including:
    - locking when concurrently drawing to multiple windows
    - gracefully handle terminal resizing
    - clip text that falls outside the panel
    - convenience methods for word wrap, in-line formatting, etc
  
  This uses a design akin to Swing where panel instances provide their display
  implementation by overwriting the draw() method, and are redrawn with
  redraw().
  """
  
  def __init__(self, parent, name, top, left=0, height=-1, width=-1):
    """
    Creates a durable wrapper for a curses subwindow in the given parent.
    
    Arguments:
      parent - parent curses window
      name   - identifier for the panel
      top    - positioning of top within parent
      left   - positioning of the left edge within the parent
      height - maximum height of panel (uses all available space if -1)
      width  - maximum width of panel (uses all available space if -1)
    """
    
    # The not-so-pythonic getters for these parameters are because some
    # implementations aren't entirely deterministic (for instance panels
    # might chose their height based on its parent's current width).
    
    self.panelName = name
    self.parent = parent
    self.visible = False
    self.titleVisible = True
    
    # Attributes for pausing. The pauseAttr contains variables our getAttr
    # method is tracking, and the pause buffer has copies of the values from
    # when we were last unpaused (unused unless we're paused).
    
    self.paused = False
    self.pauseAttr = []
    self.pauseBuffer = {}
    self.pauseTime = -1
    
    self.top = top
    self.left = left
    self.height = height
    self.width = width
    
    # The panel's subwindow instance. This is made available to implementors
    # via their draw method and shouldn't be accessed directly.
    # 
    # This is None if either the subwindow failed to be created or needs to be
    # remade before it's used. The later could be for a couple reasons:
    # - The subwindow was never initialized.
    # - Any of the parameters used for subwindow initialization have changed.
    self.win = None
    
    self.maxY, self.maxX = -1, -1 # subwindow dimensions when last redrawn
  
  def getName(self):
    """
    Provides panel's identifier.
    """
    
    return self.panelName
  
  def isTitleVisible(self):
    """
    True if the title is configured to be visible, False otherwise.
    """
    
    return self.titleVisible
  
  def setTitleVisible(self, isVisible):
    """
    Configures the panel's title to be visible or not when it's next redrawn.
    This is not guarenteed to be respected (not all panels have a title).
    """
    
    self.titleVisible = isVisible
  
  def getParent(self):
    """
    Provides the parent used to create subwindows.
    """
    
    return self.parent
  
  def setParent(self, parent):
    """
    Changes the parent used to create subwindows.
    
    Arguments:
      parent - parent curses window
    """
    
    if self.parent != parent:
      self.parent = parent
      self.win = None
  
  def isVisible(self):
    """
    Provides if the panel's configured to be visible or not.
    """
    
    return self.visible
  
  def setVisible(self, isVisible):
    """
    Toggles if the panel is visible or not.
    
    Arguments:
      isVisible - panel is redrawn when requested if true, skipped otherwise
    """
    
    self.visible = isVisible
  
  def isPaused(self):
    """
    Provides if the panel's configured to be paused or not.
    """
    
    return self.paused
  
  def setPauseAttr(self, attr):
    """
    Configures the panel to track the given attribute so that getAttr provides
    the value when it was last unpaused (or its current value if we're
    currently unpaused). For instance...
    
    > self.setPauseAttr("myVar")
    > self.myVar = 5
    > self.myVar = 6 # self.getAttr("myVar") -> 6
    > self.setPaused(True)
    > self.myVar = 7 # self.getAttr("myVar") -> 6
    > self.setPaused(False)
    > self.myVar = 7 # self.getAttr("myVar") -> 7
    
    Arguments:
      attr - parameter to be tracked for getAttr
    """
    
    self.pauseAttr.append(attr)
    self.pauseBuffer[attr] = self.copyAttr(attr)
  
  def getAttr(self, attr):
    """
    Provides the value of the given attribute when we were last unpaused. If
    we're currently unpaused then this is the current value. If untracked this
    returns None.
    
    Arguments:
      attr - local variable to be returned
    """
    
    if not attr in self.pauseAttr: return None
    elif self.paused: return self.pauseBuffer[attr]
    else: return self.__dict__.get(attr)
  
  def copyAttr(self, attr):
    """
    Provides a duplicate of the given configuration value, suitable for the
    pause buffer.
    
    Arguments:
      attr - parameter to be provided back
    """
    
    currentValue = self.__dict__.get(attr)
    return copy.copy(currentValue)
  
  def setPaused(self, isPause, suppressRedraw = False):
    """
    Toggles if the panel is paused or not. This causes the panel to be redrawn
    when toggling is pause state unless told to do otherwise. This is
    important when pausing since otherwise the panel's display could change
    when redrawn for other reasons.
    
    This returns True if the panel's pause state was changed, False otherwise.
    
    Arguments:
      isPause        - freezes the state of the pause attributes if true, makes
                       them editable otherwise
      suppressRedraw - if true then this will never redraw the panel
    """
    
    if isPause != self.paused:
      if isPause: self.pauseTime = time.time()
      self.paused = isPause
      
      if isPause:
        # copies tracked attributes so we know what they were before pausing
        for attr in self.pauseAttr:
          self.pauseBuffer[attr] = self.copyAttr(attr)
      
      if not suppressRedraw: self.redraw(True)
      return True
    else: return False
  
  def getPauseTime(self):
    """
    Provides the time that we were last paused, returning -1 if we've never
    been paused.
    """
    
    return self.pauseTime
  
  def getTop(self):
    """
    Provides the position subwindows are placed at within its parent.
    """
    
    return self.top
  
  def setTop(self, top):
    """
    Changes the position where subwindows are placed within its parent.
    
    Arguments:
      top - positioning of top within parent
    """
    
    if self.top != top:
      self.top = top
      self.win = None
  
  def getLeft(self):
    """
    Provides the left position where this subwindow is placed within its
    parent.
    """
    
    return self.left
  
  def setLeft(self, left):
    """
    Changes the left position where this subwindow is placed within its parent.
    
    Arguments:
      left - positioning of top within parent
    """
    
    if self.left != left:
      self.left = left
      self.win = None
  
  def getHeight(self):
    """
    Provides the height used for subwindows (-1 if it isn't limited).
    """
    
    return self.height
  
  def setHeight(self, height):
    """
    Changes the height used for subwindows. This uses all available space if -1.
    
    Arguments:
      height - maximum height of panel (uses all available space if -1)
    """
    
    if self.height != height:
      self.height = height
      self.win = None
  
  def getWidth(self):
    """
    Provides the width used for subwindows (-1 if it isn't limited).
    """
    
    return self.width
  
  def setWidth(self, width):
    """
    Changes the width used for subwindows. This uses all available space if -1.
    
    Arguments:
      width - maximum width of panel (uses all available space if -1)
    """
    
    if self.width != width:
      self.width = width
      self.win = None
  
  def getPreferredSize(self):
    """
    Provides the dimensions the subwindow would use when next redrawn, given
    that none of the properties of the panel or parent change before then. This
    returns a tuple of (height, width).
    """
    
    newHeight, newWidth = self.parent.getmaxyx()
    setHeight, setWidth = self.getHeight(), self.getWidth()
    newHeight = max(0, newHeight - self.top)
    newWidth = max(0, newWidth - self.left)
    if setHeight != -1: newHeight = min(newHeight, setHeight)
    if setWidth != -1: newWidth = min(newWidth, setWidth)
    return (newHeight, newWidth)
  
  def handleKey(self, key):
    """
    Handler for user input. This returns true if the key press was consumed,
    false otherwise.
    
    Arguments:
      key - keycode for the key pressed
    """
    
    return False
  
  def getHelp(self):
    """
    Provides help information for the controls this page provides. This is a
    list of tuples of the form...
    (control, description, status)
    """
    
    return []
  
  def draw(self, width, height):
    """
    Draws display's content. This is meant to be overwritten by 
    implementations and not called directly (use redraw() instead). The
    dimensions provided are the drawable dimensions, which in terms of width is
    a column less than the actual space.
    
    Arguments:
      width  - horizontal space available for content
      height - vertical space available for content
    """
    
    pass
  
  def redraw(self, forceRedraw=False, block=False):
    """
    Clears display and redraws its content. This can skip redrawing content if
    able (ie, the subwindow's unchanged), instead just refreshing the display.
    
    Arguments:
      forceRedraw - forces the content to be cleared and redrawn if true
      block       - if drawing concurrently with other panels this determines
                    if the request is willing to wait its turn or should be
                    abandoned
    """
    
    # skipped if not currently visible or activity has been halted
    if not self.isVisible() or HALT_ACTIVITY: return
    
    # if the panel's completely outside its parent then this is a no-op
    newHeight, newWidth = self.getPreferredSize()
    if newHeight == 0 or newWidth == 0:
      self.win = None
      return
    
    # recreates the subwindow if necessary
    isNewWindow = self._resetSubwindow()
    
    # The reset argument is disregarded in a couple of situations:
    # - The subwindow's been recreated (obviously it then doesn't have the old
    #   content to refresh).
    # - The subwindow's dimensions have changed since last drawn (this will
    #   likely change the content's layout)
    
    subwinMaxY, subwinMaxX = self.win.getmaxyx()
    if isNewWindow or subwinMaxY != self.maxY or subwinMaxX != self.maxX:
      forceRedraw = True
    
    self.maxY, self.maxX = subwinMaxY, subwinMaxX
    if not CURSES_LOCK.acquire(block): return
    try:
      if forceRedraw:
        self.win.erase() # clears any old contents
        self.draw(self.maxX, self.maxY)
      self.win.refresh()
    finally:
      CURSES_LOCK.release()
  
  def hline(self, y, x, length, attr=curses.A_NORMAL):
    """
    Draws a horizontal line. This should only be called from the context of a
    panel's draw method.
    
    Arguments:
      y      - vertical location
      x      - horizontal location
      length - length the line spans
      attr   - text attributes
    """
    
    if self.win and self.maxX > x and self.maxY > y:
      try:
        drawLength = min(length, self.maxX - x)
        self.win.hline(y, x, curses.ACS_HLINE | attr, drawLength)
      except:
        # in edge cases drawing could cause a _curses.error
        pass
  
  def vline(self, y, x, length, attr=curses.A_NORMAL):
    """
    Draws a vertical line. This should only be called from the context of a
    panel's draw method.
    
    Arguments:
      y      - vertical location
      x      - horizontal location
      length - length the line spans
      attr   - text attributes
    """
    
    if self.win and self.maxX > x and self.maxY > y:
      try:
        drawLength = min(length, self.maxY - y)
        self.win.vline(y, x, curses.ACS_VLINE | attr, drawLength)
      except:
        # in edge cases drawing could cause a _curses.error
        pass
  
  def addch(self, y, x, char, attr=curses.A_NORMAL):
    """
    Draws a single character. This should only be called from the context of a
    panel's draw method.
    
    Arguments:
      y    - vertical location
      x    - horizontal location
      char - character to be drawn
      attr - text attributes
    """
    
    if self.win and self.maxX > x and self.maxY > y:
      try:
        self.win.addch(y, x, char, attr)
      except:
        # in edge cases drawing could cause a _curses.error
        pass
  
  def addstr(self, y, x, msg, attr=curses.A_NORMAL):
    """
    Writes string to subwindow if able. This takes into account screen bounds
    to avoid making curses upset. This should only be called from the context
    of a panel's draw method.
    
    Arguments:
      y    - vertical location
      x    - horizontal location
      msg  - text to be added
      attr - text attributes
    """
    
    # subwindows need a single character buffer (either in the x or y 
    # direction) from actual content to prevent crash when shrank
    if self.win and self.maxX > x and self.maxY > y:
      try:
        self.win.addstr(y, x, msg[:self.maxX - x], attr)
      except:
        # this might produce a _curses.error during edge cases, for instance
        # when resizing with visible popups
        pass
  
  def addfstr(self, y, x, msg):
    """
    Writes string to subwindow. The message can contain xhtml-style tags for
    formatting, including:
    <b>text</b>               bold
    <u>text</u>               underline
    <h>text</h>               highlight
    <[color]>text</[color]>   use color (see uiTools.getColor() for constants)
    
    Tag nesting is supported and tag closing is strictly enforced (raising an
    exception for invalid formatting). Unrecognized tags are treated as normal
    text. This should only be called from the context of a panel's draw method.
    
    Text in multiple color tags (for instance "<blue><red>hello</red></blue>")
    uses the bitwise OR of those flags (hint: that's probably not what you
    want).
    
    Arguments:
      y    - vertical location
      x    - horizontal location
      msg  - formatted text to be added
    """
    
    if self.win and self.maxY > y:
      formatting = [curses.A_NORMAL]
      expectedCloseTags = []
      unusedMsg = msg
      
      while self.maxX > x and len(unusedMsg) > 0:
        # finds next consumeable tag (left as None if there aren't any left)
        nextTag, tagStart, tagEnd = None, -1, -1
        
        tmpChecked = 0 # portion of the message cleared for having any valid tags
        expectedTags = FORMAT_TAGS.keys() + expectedCloseTags
        while nextTag == None:
          tagStart = unusedMsg.find("<", tmpChecked)
          tagEnd = unusedMsg.find(">", tagStart) + 1 if tagStart != -1 else -1
          
          if tagStart == -1 or tagEnd == -1: break # no more tags to consume
          else:
            # check if the tag we've found matches anything being expected
            if unusedMsg[tagStart:tagEnd] in expectedTags:
              nextTag = unusedMsg[tagStart:tagEnd]
              break # found a tag to use
            else:
              # not a valid tag - narrow search to everything after it
              tmpChecked = tagEnd
        
        # splits into text before and after tag
        if nextTag:
          msgSegment = unusedMsg[:tagStart]
          unusedMsg = unusedMsg[tagEnd:]
        else:
          msgSegment = unusedMsg
          unusedMsg = ""
        
        # adds text before tag with current formatting
        attr = 0
        for format in formatting: attr |= format
        self.win.addstr(y, x, msgSegment[:self.maxX - x - 1], attr)
        x += len(msgSegment)
        
        # applies tag attributes for future text
        if nextTag:
          formatTag = "<" + nextTag[2:] if nextTag.startswith("</") else nextTag
          formatMatch = FORMAT_TAGS[formatTag][0](FORMAT_TAGS[formatTag][1])
          
          if not nextTag.startswith("</"):
            # open tag - add formatting
            expectedCloseTags.append("</" + nextTag[1:])
            formatting.append(formatMatch)
          else:
            # close tag - remove formatting
            expectedCloseTags.remove(nextTag)
            formatting.remove(formatMatch)
      
      # only check for unclosed tags if we processed the whole message (if we
      # stopped processing prematurely it might still be valid)
      if expectedCloseTags and not unusedMsg:
        # if we're done then raise an exception for any unclosed tags (tisk, tisk)
        baseMsg = "Unclosed formatting tag%s:" % ("s" if len(expectedCloseTags) > 1 else "")
        raise ValueError("%s: '%s'\n  \"%s\"" % (baseMsg, "', '".join(expectedCloseTags), msg))
  
  def getstr(self, y, x, initialText = "", format = None, maxWidth = None, validator = None):
    """
    Provides a text field where the user can input a string, blocking until
    they've done so and returning the result. If the user presses escape then
    this terminates and provides back None. This should only be called from
    the context of a panel's draw method.
    
    This blanks any content within the space that the input field is rendered
    (otherwise stray characters would be interpreted as part of the initial
    input).
    
    Arguments:
      y           - vertical location
      x           - horizontal location
      initialText - starting text in this field
      format      - format used for the text
      maxWidth    - maximum width for the text field
      validator   - custom TextInputValidator for handling keybindings
    """
    
    if not format: format = curses.A_NORMAL
    
    # makes cursor visible
    try: previousCursorState = curses.curs_set(1)
    except curses.error: previousCursorState = 0
    
    # temporary subwindow for user input
    displayWidth = self.getPreferredSize()[1]
    if maxWidth: displayWidth = min(displayWidth, maxWidth + x)
    inputSubwindow = self.parent.subwin(1, displayWidth - x, self.top + y, self.left + x)
    
    # blanks the field's area, filling it with the font in case it's hilighting
    inputSubwindow.clear()
    inputSubwindow.bkgd(' ', format)
    
    # prepopulates the initial text
    if initialText:
      inputSubwindow.addstr(0, 0, initialText[:displayWidth - x - 1], format)
    
    # Displays the text field, blocking until the user's done. This closes the
    # text panel and returns userInput to the initial text if the user presses
    # escape.
    
    textbox = curses.textpad.Textbox(inputSubwindow)
    
    if not validator:
      validator = textInput.BasicValidator()
    
    textbox.win.attron(format)
    userInput = textbox.edit(lambda key: validator.validate(key, textbox)).strip()
    textbox.win.attroff(format)
    if textbox.lastcmd == curses.ascii.BEL: userInput = None
    
    # reverts visability settings
    try: curses.curs_set(previousCursorState)
    except curses.error: pass
    
    return userInput
  
  def addScrollBar(self, top, bottom, size, drawTop = 0, drawBottom = -1, drawLeft = 0):
    """
    Draws a left justified scroll bar reflecting position within a vertical
    listing. This is shorted if necessary, and left undrawn if no space is
    available. The bottom is squared off, having a layout like:
     | 
    *|
    *|
    *|
     |
    -+
    
    This should only be called from the context of a panel's draw method.
    
    Arguments:
      top        - list index for the top-most visible element
      bottom     - list index for the bottom-most visible element
      size       - size of the list in which the listed elements are contained
      drawTop    - starting row where the scroll bar should be drawn
      drawBottom - ending row where the scroll bar should end, -1 if it should
                   span to the bottom of the panel
      drawLeft   - left offset at which to draw the scroll bar
    """
    
    if (self.maxY - drawTop) < 2: return # not enough room
    
    # sets drawBottom to be the actual row on which the scrollbar should end
    if drawBottom == -1: drawBottom = self.maxY - 1
    else: drawBottom = min(drawBottom, self.maxY - 1)
    
    # determines scrollbar dimensions
    scrollbarHeight = drawBottom - drawTop
    sliderTop = scrollbarHeight * top / size
    sliderSize = scrollbarHeight * (bottom - top) / size
    
    # ensures slider isn't at top or bottom unless really at those extreme bounds
    if top > 0: sliderTop = max(sliderTop, 1)
    if bottom != size: sliderTop = min(sliderTop, scrollbarHeight - sliderSize - 2)
    
    # avoids a rounding error that causes the scrollbar to be too low when at
    # the bottom
    if bottom == size: sliderTop = scrollbarHeight - sliderSize - 1
    
    # draws scrollbar slider
    for i in range(scrollbarHeight):
      if i >= sliderTop and i <= sliderTop + sliderSize:
        self.addstr(i + drawTop, drawLeft, " ", curses.A_STANDOUT)
      else:
        self.addstr(i + drawTop, drawLeft, " ")
    
    # draws box around the scroll bar
    self.vline(drawTop, drawLeft + 1, drawBottom - 1)
    self.addch(drawBottom, drawLeft + 1, curses.ACS_LRCORNER)
    self.addch(drawBottom, drawLeft, curses.ACS_HLINE)
  
  def _resetSubwindow(self):
    """
    Create a new subwindow instance for the panel if:
    - Panel currently doesn't have a subwindow (was uninitialized or
      invalidated).
    - There's room for the panel to grow vertically (curses automatically
      lets subwindows regrow horizontally, but not vertically).
    - The subwindow has been displaced. This is a curses display bug that
      manifests if the terminal's shrank then re-expanded. Displaced
      subwindows are never restored to their proper position, resulting in
      graphical glitches if we draw to them.
    - The preferred size is smaller than the actual size (should shrink).
    
    This returns True if a new subwindow instance was created, False otherwise.
    """
    
    newHeight, newWidth = self.getPreferredSize()
    if newHeight == 0: return False # subwindow would be outside its parent
    
    # determines if a new subwindow should be recreated
    recreate = self.win == None
    if self.win:
      subwinMaxY, subwinMaxX = self.win.getmaxyx()
      recreate |= subwinMaxY < newHeight              # check for vertical growth
      recreate |= self.top > self.win.getparyx()[0]   # check for displacement
      recreate |= subwinMaxX > newWidth or subwinMaxY > newHeight # shrinking
    
    # I'm not sure if recreating subwindows is some sort of memory leak but the
    # Python curses bindings seem to lack all of the following:
    # - subwindow deletion (to tell curses to free the memory)
    # - subwindow moving/resizing (to restore the displaced windows)
    # so this is the only option (besides removing subwindows entirely which 
    # would mean far more complicated code and no more selective refreshing)
    
    if recreate:
      self.win = self.parent.subwin(newHeight, newWidth, self.top, self.left)
      
      # note: doing this log before setting win produces an infinite loop
      msg = "recreating panel '%s' with the dimensions of %i/%i" % (self.getName(), newHeight, newWidth)
      log.log(CONFIG["log.panelRecreated"], msg)
    return recreate