changeset 41:032bba868ec0

- split things up into different files -- still runs... ;)
author Mark Edgington <edgimar@gmail.com>
date Sun, 01 Feb 2009 15:05:09 +0100
parents 33ae101d3887
children 2af4836f6d61
files crecord/__init__.py crecord/chunk_selector.py crecord/crecord_core.py crecord/crpatch.py
diffstat 4 files changed, 1769 insertions(+), 1735 deletions(-) [+]
line wrap: on
line diff
--- a/crecord/__init__.py
+++ b/crecord/__init__.py
@@ -8,1741 +8,10 @@
 # Much of this extension is based on Bryan O'Sullivan's record extension.
 
 '''text-gui based change selection during commit or qrefresh'''
-from mercurial.i18n import gettext, _
-from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
-from mercurial import util
-import copy, cStringIO, errno, operator, os, re, tempfile
-import signal
-import locale
-# os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce'
-if os.name == 'posix':
-    import curses
-else:
-    # I have no idea if wcurses works with crecord...
-    import wcurses as curses
-
-import curses.textpad
-
-try:
-    curses
-except NameError:
-    raise util.Abort(_('the python curses/wcurses module is not available/installed'))
-    
-
-# deal with unicode correctly
-locale.setlocale(locale.LC_ALL, '')
-
-lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
-
-def scanpatch(fp):
-    """like patch.iterhunks, but yield different events
-
-    - ('file',    [header_lines + fromfile + tofile])
-    - ('context', [context_lines])
-    - ('hunk',    [hunk_lines])
-    - ('range',   (-start,len, +start,len, diffp))
-    """
-    lr = patch.linereader(fp)
-
-    def scanwhile(first, p):
-        """scan lr while predicate holds"""
-        lines = [first]
-        while True:
-            line = lr.readline()
-            if not line:
-                break
-            if p(line):
-                lines.append(line)
-            else:
-                lr.push(line)
-                break
-        return lines
-
-    while True:
-        line = lr.readline()
-        if not line:
-            break
-        if line.startswith('diff --git a/'):
-            def notheader(line):
-                s = line.split(None, 1)
-                return not s or s[0] not in ('---', 'diff')
-            header = scanwhile(line, notheader)
-            fromfile = lr.readline()
-            if fromfile.startswith('---'):
-                tofile = lr.readline()
-                header += [fromfile, tofile]
-            else:
-                lr.push(fromfile)
-            yield 'file', header
-        elif line[0] == ' ':
-            yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
-        elif line[0] in '-+':
-            yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
-        else:
-            m = lines_re.match(line)
-            if m:
-                yield 'range', m.groups()
-            else:
-                raise patch.PatchError('unknown patch content: %r' % line)
-
-class PatchNode(object):
-    "Abstract Class for Patch Graph Nodes (i.e. PatchRoot, header, hunk, HunkLine)"
-
-    def firstChild(self):
-        raise NotImplementedError("method must be implemented by subclass")
-
-    def lastChild(self):
-        raise NotImplementedError("method must be implemented by subclass")
-
-    def allChildren(self):
-        "Return a list of all of the direct children of this node"
-        raise NotImplementedError("method must be implemented by subclass")
-    def nextSibling(self):
-        """
-        Return the closest next item of the same type where there are no items
-        of different types between the current item and this closest item.
-        If no such item exists, return None.
-
-        """
-        raise NotImplementedError("method must be implemented by subclass")
-
-    def prevSibling(self):
-        """
-        Return the closest previous item of the same type where there are no
-        items of different types between the current item and this closest item.
-        If no such item exists, return None.
-
-        """
-        raise NotImplementedError("method must be implemented by subclass")
-
-    def parentItem(self):
-        raise NotImplementedError("method must be implemented by subclass")
-
-
-    def nextItem(self, constrainLevel=True, skipFolded=True):
-        """
-        If constrainLevel == True, return the closest next item
-        of the same type where there are no items of different types between
-        the current item and this closest item.
-
-        If constrainLevel == False, then try to return the next item
-        closest to this item, regardless of item's type (header, hunk, or
-        HunkLine).
-
-        If skipFolded == True, and the current item is folded, then the child
-        items that are hidden due to folding will be skipped when determining
-        the next item.
-
-        If it is not possible to get the next item, return None.
-
-        """
-        try:
-            itemFolded = self.folded
-        except AttributeError:
-            itemFolded = False
-        if constrainLevel:
-            return self.nextSibling()
-        elif skipFolded and itemFolded:
-            nextItem = self.nextSibling()
-            if nextItem is None:
-                try:
-                    nextItem = self.parentItem().nextSibling()
-                except AttributeError:
-                    nextItem = None
-            return nextItem
-        else:
-            # try child
-            item = self.firstChild()
-            if item is not None:
-                return item
-
-            # else try next sibling
-            item = self.nextSibling()
-            if item is not None:
-                return item
-
-            try:
-                # else try parent's next sibling
-                item = self.parentItem().nextSibling()
-                if item is not None:
-                    return item
-
-                # else return grandparent's next sibling (or None)
-                return self.parentItem().parentItem().nextSibling()
-
-            except AttributeError: # parent and/or grandparent was None
-                return None
-
-    def prevItem(self, constrainLevel=True, skipFolded=True):
-        """
-        If constrainLevel == True, return the closest previous item
-        of the same type where there are no items of different types between
-        the current item and this closest item.
-
-        If constrainLevel == False, then try to return the previous item
-        closest to this item, regardless of item's type (header, hunk, or
-        HunkLine).
-
-        If skipFolded == True, and the current item is folded, then the items
-        that are hidden due to folding will be skipped when determining the
-        next item.
-
-        If it is not possible to get the previous item, return None.
-
-        """
-        if constrainLevel:
-            return self.prevSibling()
-        else:
-            # try previous sibling's last child's last child,
-            # else try previous sibling's last child, else try previous sibling
-            prevSibling = self.prevSibling()
-            if prevSibling is not None:
-                prevSiblingLastChild = prevSibling.lastChild()
-                if (prevSiblingLastChild is not None) and not prevSibling.folded:
-                    prevSiblingLCLC = prevSiblingLastChild.lastChild()
-                    if (prevSiblingLCLC is not None) and not prevSiblingLastChild.folded:
-                        return prevSiblingLCLC
-                    else:
-                        return prevSiblingLastChild
-                else:
-                    return prevSibling
-
-            # try parent (or None)
-            return self.parentItem()
-
-class Patch(PatchNode, list): # TODO: rename PatchRoot
-    """
-    List of header objects representing the patch.
-
-    """
-    def __init__(self, headerList):
-        self.extend(headerList)
-        # add parent patch object reference to each header
-        for header in self:
-            header.patch = self
-
-class header(PatchNode):
-    """patch header
-
-    XXX shoudn't we move this to mercurial/patch.py ?
-    """
-    diff_re = re.compile('diff --git a/(.*) b/(.*)$')
-    allhunks_re = re.compile('(?:index|new file|deleted file) ')
-    pretty_re = re.compile('(?:new file|deleted file) ')
-    special_re = re.compile('(?:index|new|deleted|copy|rename) ')
-
-    def __init__(self, header):
-        self.header = header
-        self.hunks = []
-        # flag to indicate whether to apply this chunk
-        self.applied = True
-        # flag which only affects the status display indicating if a node's
-        # children are partially applied (i.e. some applied, some not).
-        self.partial = False
-
-        # flag to indicate whether to display as folded/unfolded to user
-        self.folded = True
-
-        # list of all headers in patch
-        self.patch = None
-
-        # flag is False if this header was ever unfolded from initial state
-        self.neverUnfolded = True
-    def binary(self):
-        """
-        Return True if the file represented by the header is a binary file.
-        Otherwise return False.
-
-        """
-        for h in self.header:
-            if h.startswith('index '):
-                return True
-        return False
-
-    def pretty(self, fp):
-        for h in self.header:
-            if h.startswith('index '):
-                fp.write(_('this modifies a binary file (all or nothing)\n'))
-                break
-            if self.pretty_re.match(h):
-                fp.write(h)
-                if self.binary():
-                    fp.write(_('this is a binary file\n'))
-                break
-            if h.startswith('---'):
-                fp.write(_('%d hunks, %d lines changed\n') %
-                         (len(self.hunks),
-                          sum([h.added + h.removed for h in self.hunks])))
-                break
-            fp.write(h)
-
-    def prettyStr(self):
-        x = cStringIO.StringIO()
-        self.pretty(x)
-        return x.getvalue()
-
-    def write(self, fp):
-        fp.write(''.join(self.header))
-
-    def allhunks(self):
-        """
-        Return True if the file which the header represents was changed completely (i.e.
-        there is no possibility of applying a hunk of changes smaller than the size of the
-        entire file.)  Otherwise return False
-
-        """
-        for h in self.header:
-            if self.allhunks_re.match(h):
-                return True
-        return False
-
-    def files(self):
-        fromfile, tofile = self.diff_re.match(self.header[0]).groups()
-        if fromfile == tofile:
-            return [fromfile]
-        return [fromfile, tofile]
-
-    def filename(self):
-        return self.files()[-1]
-
-    def __repr__(self):
-        return '<header %s>' % (' '.join(map(repr, self.files())))
-
-    def special(self):
-        for h in self.header:
-            if self.special_re.match(h):
-                return True
-
-    def nextSibling(self):
-        numHeadersInPatch = len(self.patch)
-        indexOfThisHeader = self.patch.index(self)
-
-        if indexOfThisHeader < numHeadersInPatch - 1:
-            nextHeader = self.patch[indexOfThisHeader + 1]
-            return nextHeader
-        else:
-            return None
-
-    def prevSibling(self):
-        indexOfThisHeader = self.patch.index(self)
-        if indexOfThisHeader > 0:
-            previousHeader = self.patch[indexOfThisHeader - 1]
-            return previousHeader
-        else:
-            return None
-
-    def parentItem(self):
-        """
-        There is no 'real' parent item of a header that can be selected,
-        so return None.
-        """
-        return None
-
-    def firstChild(self):
-        "Return the first child of this item, if one exists.  Otherwise None."
-        if len(self.hunks) > 0:
-            return self.hunks[0]
-        else:
-            return None
-
-    def lastChild(self):
-        "Return the last child of this item, if one exists.  Otherwise None."
-        if len(self.hunks) > 0:
-            return self.hunks[-1]
-        else:
-            return None
-
-    def allChildren(self):
-        "Return a list of all of the direct children of this node"
-        return self.hunks
-class HunkLine(PatchNode):
-    "Represents a changed line in a hunk"
-    def __init__(self, lineText, hunk):
-        self.lineText = lineText
-        self.applied = True
-        # the parent hunk to which this line belongs
-        self.hunk = hunk
-        # folding lines currently is not used/needed, but this flag is needed
-        # in the prevItem method.
-        self.folded = False
-
-    def prettyStr(self):
-        return self.lineText
-
-    def nextSibling(self):
-        numLinesInHunk = len(self.hunk.changedLines)
-        indexOfThisLine = self.hunk.changedLines.index(self)
-
-        if (indexOfThisLine < numLinesInHunk - 1):
-            nextLine = self.hunk.changedLines[indexOfThisLine + 1]
-            return nextLine
-        else:
-            return None
-
-    def prevSibling(self):
-        indexOfThisLine = self.hunk.changedLines.index(self)
-        if indexOfThisLine > 0:
-            previousLine = self.hunk.changedLines[indexOfThisLine - 1]
-            return previousLine
-        else:
-            return None
-
-    def parentItem(self):
-        "Return the parent to the current item"
-        return self.hunk
-
-    def firstChild(self):
-        "Return the first child of this item, if one exists.  Otherwise None."
-        # hunk-lines don't have children
-        return None
-
-    def lastChild(self):
-        "Return the last child of this item, if one exists.  Otherwise None."
-        # hunk-lines don't have children
-        return None
-
-class hunk(PatchNode):
-    """patch hunk
-
-    XXX shouldn't we merge this with patch.hunk ?
-    """
-    maxcontext = 3
-
-    def __init__(self, header, fromline, toline, proc, before, hunk, after):
-        def trimcontext(number, lines):
-            delta = len(lines) - self.maxcontext
-            if False and delta > 0:
-                return number + delta, lines[:self.maxcontext]
-            return number, lines
-
-        self.header = header
-        self.fromline, self.before = trimcontext(fromline, before)
-        self.toline, self.after = trimcontext(toline, after)
-        self.proc = proc
-        self.changedLines = [HunkLine(line, self) for line in hunk]
-        self.added, self.removed = self.countchanges()
-        # used at end for detecting how many removed lines were un-applied
-        self.originalremoved = self.removed
-
-        # flag to indicate whether to display as folded/unfolded to user
-        self.folded = True
-        # flag to indicate whether to apply this chunk
-        self.applied = True
-        # flag which only affects the status display indicating if a node's
-        # children are partially applied (i.e. some applied, some not).
-        self.partial = False
-
-    def nextSibling(self):
-        numHunksInHeader = len(self.header.hunks)
-        indexOfThisHunk = self.header.hunks.index(self)
-
-        if (indexOfThisHunk < numHunksInHeader - 1):
-            nextHunk = self.header.hunks[indexOfThisHunk + 1]
-            return nextHunk
-        else:
-            return None
-
-    def prevSibling(self):
-        indexOfThisHunk = self.header.hunks.index(self)
-        if indexOfThisHunk > 0:
-            previousHunk = self.header.hunks[indexOfThisHunk - 1]
-            return previousHunk
-        else:
-            return None
-
-    def parentItem(self):
-        "Return the parent to the current item"
-        return self.header
-
-    def firstChild(self):
-        "Return the first child of this item, if one exists.  Otherwise None."
-        if len(self.changedLines) > 0:
-            return self.changedLines[0]
-        else:
-            return None
-
-    def lastChild(self):
-        "Return the last child of this item, if one exists.  Otherwise None."
-        if len(self.changedLines) > 0:
-            return self.changedLines[-1]
-        else:
-            return None
-
-    def allChildren(self):
-        "Return a list of all of the direct children of this node"
-        return self.changedLines
-    def countchanges(self):
-        """changedLines -> (n+,n-)"""
-        add = len([l for l in self.changedLines if l.applied and l.prettyStr()[0] == '+'])
-        rem = len([l for l in self.changedLines if l.applied and l.prettyStr()[0] == '-'])
-        return add, rem
-
-    def getFromToLine(self):
-        # calculate the number of removed lines converted to context lines
-        removedConvertedToContext = self.originalremoved - self.removed
-        delta = len(self.before) + len(self.after) + removedConvertedToContext
-        if self.after and self.after[-1] == '\\ No newline at end of file\n':
-            delta -= 1
-        fromlen = delta + self.removed
-        tolen = delta + self.added
-        fromToLine = '@@ -%d,%d +%d,%d @@%s\n' % \
-                 (self.fromline, fromlen, self.toline, tolen,
-                  self.proc and (' ' + self.proc))
-        return fromToLine
-
-    def write(self, fp):
-        # updated self.added/removed, which are used by getFromToLine()
-        self.added, self.removed = self.countchanges()
-        fp.write(self.getFromToLine())
-
-        hunkLineList = []
-        # add the following to the list: (1) all applied lines, and
-        # (2) all unapplied removal lines (convert these to context lines)
-        for changedLine in self.changedLines:
-            changedLineStr = changedLine.prettyStr()
-            if changedLine.applied:
-                hunkLineList.append(changedLineStr)
-            elif changedLineStr[0] == "-":
-                hunkLineList.append(" " + changedLineStr[1:])
-
-        fp.write(''.join(self.before + hunkLineList + self.after))
-
-    pretty = write
-
-    def filename(self):
-        return self.header.filename()
-
-    def prettyStr(self):
-        x = cStringIO.StringIO()
-        self.pretty(x)
-        return x.getvalue()
-
-    def __repr__(self):
-        return '<hunk %r@%d>' % (self.filename(), self.fromline)
-
-def parsepatch(changes, fp):
-    "Parse a patch, returning a list of header and hunk objects."
-    class parser(object):
-        """patch parsing state machine"""
-        def __init__(self):
-            self.fromline = 0
-            self.toline = 0
-            self.proc = ''
-            self.header = None
-            self.context = []
-            self.before = []
-            self.changedlines = []
-            self.stream = []
-            self.modified, self.added, self.removed = changes
-
-        def _range(self, (fromstart, fromend, tostart, toend, proc)):
-            "Store range line info to associated instance variables."
-            self.fromline = int(fromstart)
-            self.toline = int(tostart)
-            self.proc = proc
-
-        def add_new_hunk(self):
-            """
-            Create a new complete hunk object, adding it to the latest header
-            and to self.stream.
-
-            Add all of the previously collected information about
-            the hunk to the new hunk object.  This information includes
-            header, from/to-lines, function (self.proc), preceding context
-            lines, changed lines, as well as the current context lines (which
-            follow the changed lines).
-
-            The size of the from/to lines are updated to be correct for the
-            next hunk we parse.
-
-            """
-            h = hunk(self.header, self.fromline, self.toline, self.proc,
-                     self.before, self.changedlines, self.context)
-            self.header.hunks.append(h)
-            self.stream.append(h)
-            self.fromline += len(self.before) + h.removed
-            self.toline += len(self.before) + h.added
-            self.before = []
-            self.changedlines = []
-            self.proc = ''
-
-        def _context(self, context):
-            """
-            Set the value of self.context.
-
-            Also, if an unprocessed set of changelines was previously
-            encountered, this is the condition for creating a complete
-            hunk object.  In this case, we create and add a new hunk object to
-            the most recent header object, and to self.strem. 
-
-            """
-            self.context = context
-            # if there have been changed lines encountered that haven't yet
-            # been add to a hunk.
-            if self.changedlines:
-                self.add_new_hunk()
-
-        def _changedlines(self, changedlines):
-            """
-            Store the changed lines in self.changedlines.
-
-            Mark any context lines in the context-line buffer (self.context) as
-            lines preceding the changed-lines (i.e. stored in self.before), and
-            clear the context-line buffer.
-
-            """
-            self.changedlines = changedlines
-            self.before = self.context
-            self.context = []
-
-        def add_new_header(self, hdr):
-            """
-            Create a header object containing the header lines, and the
-            filename the header applies to.  Add the header to self.stream.
-
-            """
-            # if there are any lines in the unchanged-lines buffer, create a 
-            # new hunk using them, and add it to the last header.
-            if self.changedlines:
-                self.add_new_hunk()
-
-            # create a new header and add it to self.stream
-            self.header = header(hdr)
-            fileName = self.header.filename()
-            if fileName in self.modified:
-                self.header.changetype = "M"
-            elif fileName in self.added:
-                self.header.changetype = "A"
-            elif fileName in self.removed:
-                self.header.changetype = "R"
-            self.stream.append(self.header)
-
-        def finished(self):
-            # if there are any lines in the unchanged-lines buffer, create a 
-            # new hunk using them, and add it to the last header.
-            if self.changedlines:
-                self.add_new_hunk()
-
-            return self.stream
-
-        transitions = {
-            'file': {'context': _context,
-                     'file': add_new_header,
-                     'hunk': _changedlines,
-                     'range': _range},
-            'context': {'file': add_new_header,
-                        'hunk': _changedlines,
-                        'range': _range},
-            'hunk': {'context': _context,
-                     'file': add_new_header,
-                     'range': _range},
-            'range': {'context': _context,
-                      'hunk': _changedlines},
-            }
-
-    p = parser()
-
-    # run the state-machine
-    state = 'context'
-    for newstate, data in scanpatch(fp):
-        try:
-            p.transitions[state][newstate](p, data)
-        except KeyError:
-            raise patch.PatchError('unhandled transition: %s -> %s' %
-                                   (state, newstate))
-        state = newstate
-    return p.finished()
-
-def filterpatch(opts, chunks):
-    """Interactively filter patch chunks into applied-only chunks"""
-    chunks = list(chunks)
-    # convert chunks list into structure suitable for displaying/modifying
-    # with curses.  Create a list of headers only.
-    headers = [c for c in chunks if isinstance(c, header)]
-
-    # if there are no changed files
-    if len(headers) == 0:
-        return []
-
-    # let user choose headers/hunks/lines, and mark their applied flags accordingly
-    selectChunks(opts, headers)
-
-    appliedHunkList = []
-    for hdr in headers:
-        if hdr.applied and (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0):
-            appliedHunkList.append(hdr)
-            fixoffset = 0
-            for hnk in hdr.hunks:
-                if hnk.applied:
-                    appliedHunkList.append(hnk)
-                    # adjust the 'to'-line offset of the hunk to be correct
-                    # after de-activating some of the other hunks for this file
-                    if fixoffset:
-                        #hnk = copy.copy(hnk) # necessary??
-                        hnk.toline += fixoffset
-                else:
-                    fixoffset += hnk.removed - hnk.added
-
-    return appliedHunkList
-
-def selectChunks(opts, headerList):
-    """
-    Curses interface to get selection of chunks, and mark the applied flags
-    of the chosen chunks.
-
-    """
-    stdscr = curses.initscr()
-    curses.start_color()
-
-    chunkSelector = CursesChunkSelector(headerList)
-    curses.wrapper(chunkSelector.main, opts)
-
-class CursesChunkSelector(object):
-    def __init__(self, headerList):
-        # put the headers into a patch object
-        self.headerList = Patch(headerList)
-
-        # list of all chunks
-        self.chunkList = []
-        for h in headerList:
-            self.chunkList.append(h)
-            self.chunkList.extend(h.hunks)
-
-        # dictionary mapping (fgColor,bgColor) pairs to the corresponding curses
-        # color-pair value.
-        self.colorPairs = {}
-        # maps custom nicknames of color-pairs to curses color-pair values
-        self.colorPairNames = {}
-
-        # the currently selected header, hunk, or hunk-line
-        self.currentSelectedItem = self.headerList[0]
-
-        # updated when printing out patch-display -- the 'lines' here are the
-        # line positions *in the pad*, not on the screen.
-        self.selectedItemStartLine = 0
-        self.selectedItemEndLine = None
-
-        # define indentation levels
-        self.headerIndentNumChars = 0
-        self.hunkIndentNumChars = 3
-        self.hunkLineIndentNumChars = 6
-
-        # the first line of the pad to print to the screen
-        self.firstLineOfPadToPrint = 0
-
-        # keeps track of the number of lines in the pad
-        self.numPadLines = None
-
-        self.numStatusLines = 2
-
-        # keep a running count of the number of lines printed to the pad
-        # (used for determining when the selected item begins/ends)
-        self.linesPrintedToPadSoFar = 0
-
-        # the first line of the pad which is visible on the screen
-        self.firstLineOfPadToPrint = 0
-
-        # stores optional text for a commit comment provided by the user
-        self.commentText = ""
-
-    def upArrowEvent(self):
-        """
-        Try to select the previous item to the current item that has the
-        most-indented level.  For example, if a hunk is selected, try to select
-        the last HunkLine of the hunk prior to the selected hunk.  Or, if
-        the first HunkLine of a hunk is currently selected, then select the
-        hunk itself.
-
-        If the currently selected item is already at the top of the screen, 
-        scroll the screen down to show the new-selected item.
-
-        """
-        currentItem = self.currentSelectedItem
-
-        nextItem = currentItem.prevItem(constrainLevel=False)
-
-        if nextItem is None:
-            # if no parent item (i.e. currentItem is the first header), then
-            # no change...
-            nextItem = currentItem
-
-        self.currentSelectedItem = nextItem
-
-    def upArrowShiftEvent(self):
-        """
-        Select (if possible) the previous item on the same level as the currently
-        selected item.  Otherwise, select (if possible) the parent-item of the
-        currently selected item.
-
-        If the currently selected item is already at the top of the screen, 
-        scroll the screen down to show the new-selected item.
-
-        """
-        currentItem = self.currentSelectedItem
-        nextItem = currentItem.prevItem()
-        # if there's no previous item on this level, try choosing the parent
-        if nextItem is None:
-            nextItem = currentItem.parentItem()
-        if nextItem is None:
-            # if no parent item (i.e. currentItem is the first header), then
-            # no change...
-            nextItem = currentItem
-
-        self.currentSelectedItem = nextItem
-
-    def downArrowEvent(self):
-        """
-        Try to select the next item to the current item that has the
-        most-indented level.  For example, if a hunk is selected, select
-        the first HunkLine of the selected hunk.  Or, if the last HunkLine of
-        a hunk is currently selected, then select the next hunk, if one exists,
-        or if not, the next header if one exists.
-
-        If the currently selected item is already at the bottom of the screen, 
-        scroll the screen up to show the new-selected item.
-
-        """
-        #self.startPrintLine += 1 #DEBUG
-        currentItem = self.currentSelectedItem
-
-        nextItem = currentItem.nextItem(constrainLevel=False)
-        # if there's no next item, keep the selection as-is
-        if nextItem is None:
-            nextItem = currentItem
-
-        self.currentSelectedItem = nextItem
-
-    def downArrowShiftEvent(self):
-        """
-        If the cursor is already at the bottom chunk, scroll the screen up and move the cursor-position
-        to the subsequent chunk.  Otherwise, only move the cursor position down one chunk.
-
-        """
-        # TODO: update docstring
-
-        currentItem = self.currentSelectedItem
-        nextItem = currentItem.nextItem()
-        # if there's no previous item on this level, try choosing the parent's
-        # nextItem.
-        if nextItem is None:
-            try:
-                nextItem = currentItem.parentItem().nextItem()
-            except AttributeError:
-                # parentItem returned None, so nextItem() can't be called
-                nextItem = None
-        if nextItem is None:
-            # if no next item on parent-level, then no change...
-            nextItem = currentItem
-
-        self.currentSelectedItem = nextItem
-
-    def rightArrowEvent(self):
-        """
-        Select (if possible) the first of this item's child-items.
-
-        """
-        currentItem = self.currentSelectedItem
-        nextItem = currentItem.firstChild()
-
-        # turn off folding if we want to show a child-item
-        if currentItem.folded:
-            self.toggleFolded(currentItem)
-
-        if nextItem is None:
-            # if no next item on parent-level, then no change...
-            nextItem = currentItem
-
-        self.currentSelectedItem = nextItem
-
-    def leftArrowEvent(self):
-        """
-        Select (if possible) the parent of this item.
-        Otherwise, if this item is a header, then fold it.
-
-        """
-        currentItem = self.currentSelectedItem
-        nextItem = currentItem.parentItem()
-        
-        if nextItem is None:
-            # if no item on parent-level, then no change...
-            nextItem = currentItem
-            if not nextItem.folded:
-                self.toggleFolded(item=nextItem)
-
-        self.currentSelectedItem = nextItem
+from mercurial.i18n import _
+from mercurial import commands, extensions
 
-    def updateScroll(self):
-        "Scroll the screen in such a way to fully show the currently-selected item."
-        selStart = self.selectedItemStartLine
-        selEnd = self.selectedItemEndLine
-        #selNumLines = selEnd - selStart
-        padStart = self.firstLineOfPadToPrint
-        padEnd = padStart + self.yScreenSize - self.numStatusLines - 1
-        screenMiddleLine = self.yScreenSize / 2
-        # 'buffered' pad start/end values which scroll with a certain
-        # top/bottom context margin
-        padStartBuffered = padStart + 3
-        padEndBuffered = padEnd - 3
-
-        if selEnd > padEndBuffered:
-            self.scrollLines(selEnd - padEndBuffered)
-        elif selStart < padStartBuffered:
-            # negative values scroll in pgup direction
-            self.scrollLines(selStart - padStartBuffered)
-
-
-    def scrollLines(self, numLines):
-        "Scroll the screen up (down) by numLines when numLines >0 (<0)."
-        self.firstLineOfPadToPrint += numLines
-        if self.firstLineOfPadToPrint < 0:
-            self.firstLineOfPadToPrint = 0
-        if self.firstLineOfPadToPrint > self.numPadLines-1:
-            self.firstLineOfPadToPrint = self.numPadLines-1
-
-    def toggleApply(self, item=None):
-        """
-        Toggle the applied flag of the specified item.  If no item is specified,
-        toggle the flag of the currently selected item.
-
-        """
-        if item is None:
-            item = self.currentSelectedItem
-
-        item.applied = not item.applied
-
-        if isinstance(item, header):
-            item.partial = False
-            if item.applied:
-                if not item.special():
-                    # apply all its hunks
-                    for hnk in item.hunks:
-                        hnk.applied = True
-                        # apply all their HunkLines
-                        for hunkLine in hnk.changedLines:
-                            hunkLine.applied = True
-                else:
-                    # all children are off (but the header is on)
-                    if len(item.allChildren()) > 0:
-                        item.partial = True
-            else:
-                # un-apply all its hunks
-                for hnk in item.hunks:
-                    hnk.applied = False
-                    # un-apply all their HunkLines
-                    for hunkLine in hnk.changedLines:
-                        hunkLine.applied = False
-        elif isinstance(item, hunk):
-            item.partial = False
-            # apply all it's HunkLines
-            for hunkLine in item.changedLines:
-                hunkLine.applied = item.applied
-
-            siblingAppliedStatus = [hnk.applied for hnk in item.header.hunks]
-            allSiblingsApplied = not (False in siblingAppliedStatus) 
-            noSiblingsApplied = not (True in siblingAppliedStatus)
-
-            siblingsPartialStatus = [hnk.partial for hnk in item.header.hunks]
-            someSiblingsPartial = (True in siblingsPartialStatus)
-
-            #cases where applied or partial should be removed from header
-
-            # if no 'sibling' hunks are applied (including this hunk)
-            if noSiblingsApplied:
-                if not item.header.special():
-                    item.header.applied = False
-                    item.header.partial = False
-            else: # some/all parent siblings are applied
-                item.header.applied = True
-                item.header.partial = (someSiblingsPartial or \
-                                        not allSiblingsApplied)
-
-        elif isinstance(item, HunkLine):
-            siblingAppliedStatus = [hnkln.applied for hnkln in item.hunk.changedLines]
-            allSiblingsApplied = not (False in siblingAppliedStatus) 
-            noSiblingsApplied = not (True in siblingAppliedStatus)
-
-            # if no 'sibling' lines are applied
-            if noSiblingsApplied:
-                item.hunk.applied = False
-                item.hunk.partial = False
-            elif allSiblingsApplied:
-                item.hunk.applied = True
-                item.hunk.partial = False
-            else: # some siblings applied
-                item.hunk.applied = True
-                item.hunk.partial = True
-
-            parentSiblingsAppliedStatus = [hnk.applied for hnk in item.hunk.header.hunks]
-            noParentSiblingsApplied = not (True in parentSiblingsAppliedStatus)
-            allParentSiblingsApplied = not (False in parentSiblingsAppliedStatus)
-
-            parentSiblingsPartialStatus = [hnk.partial for hnk in item.hunk.header.hunks]
-            someParentSiblingsPartial = (True in parentSiblingsPartialStatus)
-
-            # if all parent hunks are not applied, un-apply header
-            if noParentSiblingsApplied:
-                if not item.hunk.header.special():
-                    item.hunk.header.applied = False
-                    item.hunk.header.partial = False
-            # set the applied and partial status of the header if needed
-            else: # some/all parent siblings are applied
-                item.hunk.header.applied = True
-                item.hunk.header.partial = (someParentSiblingsPartial or \
-                                            not allParentSiblingsApplied)
-    def toggleFolded(self, item=None, foldParent=False):
-        "Toggle folded flag of specified item (defaults to currently selected)"
-        if item is None:
-            item = self.currentSelectedItem
-        if foldParent or (isinstance(item, header) and item.neverUnfolded):
-            if not isinstance(item, header):
-                # we need to select the parent item in this case
-                self.currentSelectedItem = item = item.parentItem()
-            elif item.neverUnfolded:
-                item.neverUnfolded = False
-            
-            # also fold any foldable children of the parent/current item
-            if isinstance(item, header): # the original OR 'new' item
-                for child in item.allChildren():
-                    child.folded = not item.folded
-
-        if isinstance(item, (header, hunk)):
-            item.folded = not item.folded
-
-
-    def alignString(self, inStr, window):
-        """
-        Add whitespace to the end of a string in order to make it fill
-        the screen in the x direction.  The current cursor position is
-        taken into account when making this calculation.  The string can span
-        multiple lines.
-
-        """
-        y,xStart = window.getyx()
-        width = self.xScreenSize
-        # turn tabs into spaces
-        inStr = inStr.expandtabs(4)
-        strLen = len(unicode(util.fromlocal(inStr), 'utf-8'))
-        numSpaces = (width - ((strLen + xStart) % width) - 1)
-        return inStr + " " * numSpaces + "\n"
-
-    def printString(self, window, text, fgColor=None, bgColor=None, pair=None,
-        pairName=None, attrList=None, toWin=True, align=True, showWhtSpc=False):
-        """
-        Print the string, text, with the specified colors and attributes, to
-        the specified curses window object.
-
-        The foreground and background colors are of the form
-        curses.COLOR_XXXX, where XXXX is one of: [BLACK, BLUE, CYAN, GREEN,
-        MAGENTA, RED, WHITE, YELLOW].  If pairName is provided, a color
-        pair will be looked up in the self.colorPairNames dictionary.
-
-        attrList is a list containing text attributes in the form of 
-        curses.A_XXXX, where XXXX can be: [BOLD, DIM, NORMAL, STANDOUT,
-        UNDERLINE].
-        
-        If align == True, whitespace is added to the printed string such that
-        the string stretches to the right border of the window.
-        
-        If showWhtSpc == True, trailing whitespace of a string is highlighted.
-
-        """
-        # preprocess the text, converting tabs to spaces
-        text = text.expandtabs(4)
-
-        if pair is not None:
-            colorPair = pair
-        elif pairName is not None:
-            colorPair = self.colorPairNames[pairName]
-        else:
-            if fgColor is None:
-                fgColor = curses.COLOR_WHITE
-            if bgColor is None:
-                bgColor = curses.COLOR_BLACK
-            if self.colorPairs.has_key((fgColor,bgColor)):
-                colorPair = self.colorPairs[(fgColor,bgColor)]
-            else:
-                colorPair = self.getColorPair(fgColor, bgColor)
-        # add attributes if possible
-        if attrList is None:
-            attrList = []
-        if colorPair < 256:
-            # then it is safe to apply all attributes
-            for textAttr in attrList:
-                colorPair |= textAttr
-        else:
-            # just apply a select few (safe?) attributes
-            for textAttr in (curses.A_UNDERLINE, curses.A_BOLD):
-                if textAttr in attrList:
-                    colorPair |= textAttr
-
-        y,xStart = self.chunkpad.getyx()
-        t = "" # variable for counting lines printed
-        # if requested, show trailing whitespace
-        if showWhtSpc:
-            origLen = len(text)
-            text = text.rstrip(' \n') # tabs have already been expanded
-            strippedLen = len(text)
-            numTrailingSpaces = origLen - strippedLen
-        
-        if toWin:
-            window.addstr(text, colorPair)
-        t += text
-
-        if showWhtSpc:
-                wsColorPair = colorPair | curses.A_REVERSE
-                if toWin:
-                    for i in range(numTrailingSpaces):
-                        window.addch(curses.ACS_CKBOARD, wsColorPair)
-                t += " " * numTrailingSpaces
-        
-        if align:
-            if toWin:
-                extraWhiteSpace = self.alignString("", window)
-                window.addstr(extraWhiteSpace, colorPair)
-            else:
-                # need to use t, since the x position hasn't incremented
-                extraWhiteSpace = self.alignString(t, window)
-            t += extraWhiteSpace
-        
-        # is reset to 0 at the beginning of printItem()
-        
-        linesPrinted = (xStart + len(t)) / self.xScreenSize
-        self.linesPrintedToPadSoFar += linesPrinted
-        return t
-
-    def updateScreen(self):
-        self.statuswin.erase()
-        self.chunkpad.erase()
-
-        width = self.xScreenSize
-        alignString = self.alignString
-        printString = self.printString
-
-        # print out the status lines at the top
-        try:
-            printString(self.statuswin, "SELECT CHUNKS: (j/k/up/down/pgup/pgdn) move cursor; (space) toggle applied", pairName="legend")
-            printString(self.statuswin, " (f)old/unfold; (c)ommit applied; (q)uit; (?) help | [X]=hunk applied **=folded", pairName="legend")
-        except curses.error:
-            pass
-
-        # print out the patch in the remaining part of the window
-        try:
-            self.printItem()
-            self.updateScroll()
-            self.chunkpad.refresh(self.firstLineOfPadToPrint,0,self.numStatusLines,0,self.yScreenSize+1-self.numStatusLines,self.xScreenSize)
-        except curses.error:
-            pass
-
-        # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
-        self.statuswin.refresh()
-
-    def getStatusPrefixString(self, item):
-        """
-        Create a string to prefix a line with which indicates whether 'item'
-        is applied and/or folded.
-
-        """
-        # create checkBox string
-        if item.applied:
-            if not isinstance(item, HunkLine) and item.partial:
-                checkBox = "[~]"
-            else:
-                checkBox = "[X]"
-        else:
-            checkBox = "[ ]"
-
-        try:
-            if item.folded:
-                checkBox += "**"
-                if isinstance(item, header):
-                    # one of "M", "A", or "D" (modified, added, deleted)
-                    fileStatus = item.changetype
-                 
-                    checkBox += fileStatus + " "
-            else:
-                checkBox += "  "
-                if isinstance(item, header):
-                    # add two more spaces for headers
-                    checkBox += "  "
-        except AttributeError: # not foldable
-            checkBox += "  "
-
-        return checkBox
-
-    def printHeader(self, header, selected=False, toWin=True, ignoreFolding=False):
-        """
-        Print the header to the pad.  If countLines is True, don't print
-        anything, but just count the number of lines which would be printed.
-
-        """
-        outStr = ""
-        text = header.prettyStr()
-        chunkIndex = self.chunkList.index(header)
-
-        if chunkIndex != 0 and not header.folded:
-            # add separating line before headers
-            outStr += self.printString(self.chunkpad, '_'*self.xScreenSize, toWin=toWin, align=False)
-        # select color-pair based on if the header is selected
-        if selected:
-            colorPair = self.getColorPair(name="selected", attrList=[curses.A_BOLD])
-        else:
-            colorPair = self.getColorPair(name="normal", attrList=[curses.A_BOLD])
-
-        # print out each line of the chunk, expanding it to screen width
-
-        # number of characters to indent lines on this level by
-        indentNumChars = 0
-        checkBox = self.getStatusPrefixString(header)
-        if not header.folded or ignoreFolding:
-            textList = text.split("\n")
-            lineStr = checkBox + textList[0]
-        else:
-            lineStr = checkBox + header.filename()
-        outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin)
-        if not header.folded or ignoreFolding:
-            if len(textList) > 1:
-                for line in textList[1:]:
-                    lineStr = " "*(indentNumChars + len(checkBox)) + line
-                    outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin)
-
-        return outStr
-
-    def printHunkLinesBefore(self, hunk, selected=False, toWin=True, ignoreFolding=False):
-        "includes start/end line indicator"
-        outStr = ""
-        # where hunk is in list of siblings
-        hunkIndex = hunk.header.hunks.index(hunk)
-
-        if hunkIndex != 0:
-            # add separating line before headers
-            outStr += self.printString(self.chunkpad, ' '*self.xScreenSize, toWin=toWin, align=False)
-
-        if selected:
-            colorPair = self.getColorPair(name="selected", attrList=[curses.A_BOLD])
-        else:
-            colorPair = self.getColorPair(name="normal", attrList=[curses.A_BOLD])
-
-        # print out from-to line with checkbox
-        checkBox = self.getStatusPrefixString(hunk)
-
-        linePrefix = " "*self.hunkIndentNumChars + checkBox
-        frToLine = "   " + hunk.getFromToLine().strip("\n")
-
-
-        outStr += self.printString(self.chunkpad, linePrefix, toWin=toWin, align=False) # add uncolored checkbox/indent
-        outStr += self.printString(self.chunkpad, frToLine, pair=colorPair, toWin=toWin)
-
-        if hunk.folded and not ignoreFolding:
-            # skip remainder of output
-            return outStr
-
-        # print out lines of the chunk preceeding changed-lines
-        for line in hunk.before:
-            lineStr = " "*(self.hunkLineIndentNumChars + len(checkBox)) + line.strip("\n")
-            outStr += self.printString(self.chunkpad, lineStr, toWin=toWin)
-
-        return outStr
-
-    def printHunkLinesAfter(self, hunk, toWin=True, ignoreFolding=False):
-        outStr = ""
-        if hunk.folded and not ignoreFolding:
-            return outStr
-
-        indentNumChars = self.hunkLineIndentNumChars-1
-        # a bit superfluous, but to avoid hard-coding indent amount
-        checkBox = self.getStatusPrefixString(hunk)
-        for line in hunk.after:
-            lineStr = " "*(indentNumChars + len(checkBox)) + line.strip("\n")
-            outStr += self.printString(self.chunkpad, lineStr, toWin=toWin)
-
-        return outStr
-
-    def printHunkChangedLine(self, hunkLine, selected=False, toWin=True):
-        outStr = ""
-        indentNumChars = self.hunkLineIndentNumChars
-        checkBox = self.getStatusPrefixString(hunkLine)
-
-        lineStr = hunkLine.prettyStr().strip("\n")
-
-        # select color-pair based on whether line is an addition/removal
-        if selected:
-            colorPair = self.getColorPair(name="selected")
-        elif lineStr.startswith("+"):
-            colorPair = self.getColorPair(name="addition")
-        elif lineStr.startswith("-"):
-            colorPair = self.getColorPair(name="deletion")
-        elif lineStr.startswith("\\"):
-            colorPair = self.getColorPair(name="normal")
-
-        linePrefix = " "*indentNumChars + checkBox
-        outStr += self.printString(self.chunkpad, linePrefix, toWin=toWin, align=False) # add uncolored checkbox/indent
-        outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin, showWhtSpc=True)
-        return outStr
-
-    def printItem(self, item=None, ignoreFolding=False, recurseChildren=True, toWin=True):
-        """
-        Use __printItem() to print the the specified item.applied.
-        If item is not specified, then print the entire patch.
-        (hiding folded elements, etc. -- see __printitem() docstring)
-        """
-        if item is None:
-            item = self.headerList
-        if recurseChildren:
-            self.linesPrintedToPadSoFar = 0
-            global outStr
-        retStr = self.__printItem(item, ignoreFolding, recurseChildren, toWin=toWin)
-        if recurseChildren:
-            # remove the string when finished, so it doesn't accumulate
-            del outStr
-
-        return retStr
-
-    def __printItem(self, item, ignoreFolding, recurseChildren, toWin=True):
-        """
-        Recursive method for printing out patch/header/hunk/hunk-line data to
-        screen.  Also returns a string with all of the content of the displayed
-        patch (not including coloring, etc.).
-
-        If ignoreFolding is True, then folded items are printed out.
-
-        If recurseChildren is False, then only print the item without its
-        child items.
-
-        """
-        # keep outStr local, since we're not recursing
-        if recurseChildren:
-            global outStr
-            try:
-                outStr
-            except:
-                outStr = ""
-        else:
-            outStr = ""
-
-        selected = (item is self.currentSelectedItem)
-        if selected and recurseChildren:
-            # assumes line numbering starting from line 0
-            self.selectedItemStartLine = self.linesPrintedToPadSoFar
-            selectedItemLines = self.getNumLinesDisplayed(item, recurseChildren=False)
-            self.selectedItemEndLine = self.selectedItemStartLine + selectedItemLines - 1
-
-        # Patch object is a list of headers
-        if isinstance(item, Patch):
-            if recurseChildren:
-                for hdr in item:
-                    self.__printItem(hdr, ignoreFolding, recurseChildren, toWin)
-        if isinstance(item, header):
-            outStr += self.printHeader(item, selected, toWin=toWin, ignoreFolding=ignoreFolding)
-            if recurseChildren:
-                for hnk in item.hunks:
-                    self.__printItem(hnk, ignoreFolding, recurseChildren, toWin)
-        elif isinstance(item, hunk) and \
-        ((not item.header.folded) or ignoreFolding):
-            # print the hunk data which comes before the changed-lines
-            outStr += self.printHunkLinesBefore(item, selected, toWin=toWin, ignoreFolding=ignoreFolding)
-            if recurseChildren:
-                for line in item.changedLines:
-                    self.__printItem(line, ignoreFolding, recurseChildren, toWin)
-                outStr += self.printHunkLinesAfter(item, toWin=toWin, ignoreFolding=ignoreFolding)
-        elif isinstance(item, HunkLine) and ((not item.hunk.folded) or ignoreFolding):
-            outStr += self.printHunkChangedLine(item, selected, toWin=toWin)
-
-        return outStr
-
-    def getNumLinesDisplayed(self, item=None, ignoreFolding=False, recurseChildren=True):
-        """
-        Return the number of lines which would be displayed if the item were
-        to be printed to the display.  The item will NOT be printed to the
-        display (pad).
-        If no item is given, assume the entire patch.
-        If ignoreFolding is True, folded items will be unfolded when counting
-        the number of lines.
-
-        """
-        # temporarily disable printing to windows by printString
-        patchDisplayString = self.printItem(item, ignoreFolding, recurseChildren, toWin=False)
-        numLines = len(patchDisplayString)/self.xScreenSize
-        return numLines
-
-    def sigwinchHandler(self, n, frame):
-        "Handle window resizing"
-        try:
-            curses.endwin()
-            self.stdscr = curses.initscr()
-            self.yScreenSize, self.xScreenSize = self.stdscr.getmaxyx()
-
-            self.statuswin = curses.newwin(self.numStatusLines,self.xScreenSize,0,0)
-        except curses.error:
-            pass
-            # TODO: make resizing to a smaller width work (also for help screen)
-            # re-calculate an upper-bound on the number of lines in the pad
-            #self.numPadLines = self.getNumLinesDisplayed()
-            #self.chunkpad = curses.newpad(self.numPadLines, self.xScreenSize)
-            #self.updateScreen()
-
-    def getColorPair(self, fgColor=None, bgColor=None, name=None, attrList=None):
-        """
-        Get a curses color pair, adding it to self.colorPairs if it is not already
-        defined.  An optional string, name, can be passed as a shortcut for
-        referring to the color-pair.  By default, if no arguments are specified,
-        the white foreground / black background color-pair is returned.
-
-        It is expected that this function will be used exclusively for initializing
-        color pairs, and NOT curses.init_pair().
-
-        attrList is used to 'flavor' the returned color-pair.  This information
-        is not stored in self.colorPairs.  It contains attribute values like
-        curses.A_BOLD.
-
-        """
-        if (name is not None) and self.colorPairNames.has_key(name):
-            # then get the associated color pair and return it
-            colorPair = self.colorPairNames[name]
-        else:
-            if fgColor is None:
-                fgColor = curses.COLOR_WHITE
-            if bgColor is None:
-                bgColor = curses.COLOR_BLACK
-            if self.colorPairs.has_key((fgColor,bgColor)):
-                colorPair = self.colorPairs[(fgColor,bgColor)]
-            else:
-                pairIndex = len(self.colorPairs) + 1
-                curses.init_pair(pairIndex, fgColor, bgColor)
-                colorPair = self.colorPairs[(fgColor, bgColor)] = curses.color_pair(pairIndex)
-                if name is not None:
-                    self.colorPairNames[name] = curses.color_pair(pairIndex)
-
-        # add attributes if possible
-        if attrList is None:
-            attrList = []
-        if colorPair < 256:
-            # then it is safe to apply all attributes
-            for textAttr in attrList:
-                colorPair |= textAttr
-        else:
-            # just apply a select few (safe?) attributes
-            for textAttrib in (curses.A_UNDERLINE, curses.A_BOLD):
-                if textAttrib in attrList:
-                    colorPair |= textAttrib
-        return colorPair
-
-    def initColorPair(self, *args, **kwargs):
-        "Same as getColorPair."
-        self.getColorPair(*args, **kwargs)    
-
-    def helpWindow(self):
-        "Print a help window to the screen.  Exit after any keypress."
-        helpText = """            [press any key to return to the patch-display]
-
-crecord allows you to interactively choose among the changes you have made,
-and commit only those changes you select.  After committing the selected
-changes, the unselected changes are still present in your working copy, so you
-can use crecord multiple times to split large changes into smaller changesets.
-The following are valid keystrokes:
-
-                [SPACE] : (un-)select item ([~]/[X] = partly/fully applied)
-    Up/Down-arrow [k/j] : go to previous/next unfolded item
-        PgUp/PgDn [K/J] : go to previous/next item of same type
- Right/Left-arrow [l/h] : go to child item / parent item
-                      f : fold / unfold item, hiding/revealing its children
-                      F : fold / unfold parent item and all of its ancestors
-                      m : edit / resume editing the commit message
-                      c : commit selected changes
-                      q : quit without committing (no changes will be made)
-                      ? : help (what you're currently reading)"""
-
-        helpwin = curses.newwin(self.yScreenSize,0,0,0)
-        helpLines = helpText.split("\n")
-        helpLines = helpLines + [" "]*(self.yScreenSize-self.numStatusLines-len(helpLines)-1)
-        try:
-            for line in helpLines:
-                self.printString(helpwin, line, pairName="legend")
-        except curses.error:
-            pass
-        helpwin.refresh()
-        self.stdscr.getch()
-
-    def commitMessageWindow(self):
-        "Create a temporary commit message editing window on the screen."
-        # In Python versions < 2.6, there is no insert mode (only overwrite) :(
-        def keyFilter(key):
-            "provide keymappings to emacs-style keys"
-            if key in (7,):
-                # diable keys we're re-mapping
-                return ""
-            elif key in (24, 3): # CTRL-X,  3 = CTRL-C
-                return 7 # CTRL-G (i.e. exit comment window)
-            else:
-                return key
-        statusline = curses.newwin(2,0,0,0)
-        statusLineText = \
-        " Begin/resume editing commit message. CTRL-C/-X returns to patch view."
-        self.printString(statusline, statusLineText, pairName="legend")
-        statusline.refresh()
-        helpwin = curses.newwin(self.yScreenSize-1,0,1,0)
-        reversedCommentText = self.commentText[::-1]
-        for char in reversedCommentText:
-            curses.ungetch(ord(char))
-        t = curses.textpad.Textbox(helpwin)
-        curses.raw()
-        self.commentText = t.edit(keyFilter).rstrip(" \n")
-        curses.cbreak()
-
-    def confirmCommit(self):
-        "Ask for 'Y' to be pressed to confirm commit. Return True if confirmed."
-        confirmText = "Are you sure you want to commit the selected changes [yN]? "
-
-        confirmWin = curses.newwin(self.yScreenSize,0,0,0)
-        try:
-            self.printString(confirmWin, confirmText, pairName="selected")
-        except curses.error:
-            pass
-        confirmWin.refresh()
-        try:
-            response = chr(self.stdscr.getch())
-        except ValueError:
-            response = "n"
-        if response.lower().startswith("y"):
-            return True
-        else:
-            return False
-
-    def main(self, stdscr, opts):
-        """
-        Method to be wrapped by curses.wrapper() for selecting chunks.
-
-        """
-        signal.signal(signal.SIGWINCH, self.sigwinchHandler)
-        self.stdscr = stdscr
-        self.yScreenSize, self.xScreenSize = self.stdscr.getmaxyx()
-
-        # available colors: black, blue, cyan, green, magenta, white, yellow
-        # init_pair(color_id, foreground_color, background_color)
-        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_BLACK, name="normal")
-        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_RED, name="selected")
-        self.initColorPair(curses.COLOR_RED, curses.COLOR_BLACK, name="deletion")
-        self.initColorPair(curses.COLOR_GREEN, curses.COLOR_BLACK, name="addition")
-        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
-        # newwin([height, width,] begin_y, begin_x)
-        self.statuswin = curses.newwin(self.numStatusLines,0,0,0)
-
-        # figure out how much space to allocate for the chunk-pad which is
-        # used for displaying the patch
-
-        # stupid hack to prevent getNumLinesDisplayed from failing
-        self.chunkpad = curses.newpad(1,self.xScreenSize)
-
-        # add 1 so to account for last line text reaching end of line
-        self.numPadLines = self.getNumLinesDisplayed(ignoreFolding=True) + 1
-        self.chunkpad = curses.newpad(self.numPadLines, self.xScreenSize)
-
-        # initialize selecteItemEndLine (initial start-line is 0)
-        self.selectedItemEndLine = self.getNumLinesDisplayed(self.currentSelectedItem, recurseChildren=False)
-
-        try:
-            self.commentText = opts['message']
-        except KeyError:
-            pass
-
-
-        #import rpdb2; rpdb2.start_embedded_debugger("secret")
-        #import rpdb2; rpdb2.setbreak()
-
-        while True:
-            self.updateScreen()
-            self.lastKeyPressed = keyPressed = stdscr.getch()
-            if keyPressed in [ord("k"), curses.KEY_UP]:
-                self.upArrowEvent()
-            if keyPressed in [ord("K"), curses.KEY_PPAGE]:
-                self.upArrowShiftEvent()
-            elif keyPressed in [ord("j"), curses.KEY_DOWN]:
-                self.downArrowEvent()
-            elif keyPressed in [ord("J"), curses.KEY_NPAGE]:
-                self.downArrowShiftEvent()
-            elif keyPressed in [ord("l"), curses.KEY_RIGHT]:
-                self.rightArrowEvent()
-            elif keyPressed in [ord("h"), curses.KEY_LEFT]:
-                self.leftArrowEvent()
-            elif keyPressed in [ord("q")]:
-                raise util.Abort(_('user quit'))
-            elif keyPressed in [ord("c")]:
-                if self.confirmCommit():
-                    break
-            elif keyPressed in [ord(' ')]:
-                self.toggleApply()
-            elif keyPressed in [ord("f")]:
-                self.toggleFolded()
-            elif keyPressed in [ord("F")]:
-                self.toggleFolded(foldParent=True)
-            elif keyPressed in [ord("?")]:
-                self.helpWindow()
-            elif keyPressed in [ord("m")]:
-                self.commitMessageWindow()
-
-        if self.commentText != "":
-            opts['message'] = self.commentText
-
-
-def crecord(ui, repo, *pats, **opts):
-    '''interactively select changes to commit
-
-    If a list of files is omitted, all changes reported by "hg status"
-    will be candidates for recording.
-
-    See 'hg help dates' for a list of formats valid for -d/--date.
-
-    You will be shown a list of patch hunks from which you can select
-    those you would like to apply to the commit.
-
-    '''
-    def record_committer(ui, repo, pats, opts):
-        commands.commit(ui, repo, *pats, **opts)
-    dorecord(ui, repo, record_committer, *pats, **opts)
-
-
-def qcrecord(ui, repo, patch, *pats, **opts):
-    '''interactively record a new patch
-
-    see 'hg help qnew' & 'hg help record' for more information and usage
-    '''
-
-    try:
-        mq = extensions.find('mq')
-    except KeyError:
-        raise util.Abort(_("'mq' extension not loaded"))
-
-    def qrecord_committer(ui, repo, pats, opts):
-        mq.new(ui, repo, patch, *pats, **opts)
-
-    opts = opts.copy()
-    opts['force'] = True    # always 'qnew -f'
-    dorecord(ui, repo, qrecord_committer, *pats, **opts)
-
-
-def dorecord(ui, repo, committer, *pats, **opts):
-    if not ui.interactive:
-        raise util.Abort(_('running non-interactively, use commit instead'))
-
-    def recordfunc(ui, repo, message, match, opts):
-        """This is generic record driver.
-
-        It's job is to interactively filter local changes, and accordingly
-        prepare working dir into a state, where the job can be delegated to
-        non-interactive commit command such as 'commit' or 'qrefresh'.
-
-        After the actual job is done by non-interactive command, working dir
-        state is restored to original.
-
-        In the end we'll record intresting changes, and everything else will be
-        left in place, so the user can continue his work.
-        """
-        if match.files():
-            changes = None
-        else:
-            changes = repo.status(match=match)[:3]
-            modified, added, removed = changes
-            match = cmdutil.matchfiles(repo, modified + added + removed)
-        diffopts = mdiff.diffopts(git=True, nodates=True)
-        chunks = patch.diff(repo, repo.dirstate.parents()[0], match=match,
-                            changes=changes, opts=diffopts)
-        fp = cStringIO.StringIO()
-        fp.write(''.join(chunks))
-        fp.seek(0)
-
-        # 1. filter patch, so we have intending-to apply subset of it
-        if changes is not None:
-            chunks = filterpatch(opts, parsepatch(changes, fp))
-        else:
-            chgs = repo.status(match=match)[:3]
-            chunks = filterpatch(opts, parsepatch(chgs, fp))
-            
-        del fp
-
-        contenders = {}
-        for h in chunks:
-            try: contenders.update(dict.fromkeys(h.files()))
-            except AttributeError: pass
-
-        newfiles = [f for f in match.files() if f in contenders]
-
-        if not newfiles:
-            ui.status(_('no changes to record\n'))
-            return 0
-
-        if changes is None:
-            match = cmdutil.matchfiles(repo, newfiles)
-            changes = repo.status(match=match)
-        modified = dict.fromkeys(changes[0])
-
-        # 2. backup changed files, so we can restore them in the end
-        backups = {}
-        backupdir = repo.join('record-backups')
-        try:
-            os.mkdir(backupdir)
-        except OSError, err:
-            if err.errno != errno.EEXIST:
-                raise
-        try:
-            # backup continues
-            for f in newfiles:
-                if f not in modified:
-                    continue
-                fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
-                                               dir=backupdir)
-                os.close(fd)
-                ui.debug(_('backup %r as %r\n') % (f, tmpname))
-                util.copyfile(repo.wjoin(f), tmpname)
-                backups[f] = tmpname
-
-            fp = cStringIO.StringIO()
-            for c in chunks:
-                if c.filename() in backups:
-                    c.write(fp)
-            dopatch = fp.tell()
-            fp.seek(0)
-
-            # 3a. apply filtered patch to clean repo  (clean)
-            if backups:
-                hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
-
-            # 3b. (apply)
-            if dopatch:
-                try:
-                    ui.debug(_('applying patch\n'))
-                    ui.debug(fp.getvalue())
-                    patch.internalpatch(fp, ui, 1, repo.root)
-                except patch.PatchError, err:
-                    s = str(err)
-                    if s:
-                        raise util.Abort(s)
-                    else:
-                        raise util.Abort(_('patch failed to apply'))
-            del fp
-
-            # 4. We prepared working directory according to filtered patch.
-            #    Now is the time to delegate the job to commit/qrefresh or the like!
-
-            # it is important to first chdir to repo root -- we'll call a
-            # highlevel command with list of pathnames relative to repo root
-            cwd = os.getcwd()
-            os.chdir(repo.root)
-            try:
-                committer(ui, repo, newfiles, opts)
-            finally:
-                os.chdir(cwd)
-
-            return 0
-        finally:
-            # 5. finally restore backed-up files
-            try:
-                for realname, tmpname in backups.iteritems():
-                    ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
-                    util.copyfile(tmpname, repo.wjoin(realname))
-                    os.unlink(tmpname)
-                os.rmdir(backupdir)
-            except OSError:
-                pass
-    return cmdutil.commit(ui, repo, recordfunc, pats, opts)
+from crecord_core import crecord, qcrecord
 
 cmdtable = {
     "crecord":
@@ -1772,4 +41,3 @@
     }
 
     cmdtable.update(qcmdtable)
-
new file mode 100644
--- /dev/null
+++ b/crecord/chunk_selector.py
@@ -0,0 +1,925 @@
+from mercurial.i18n import gettext, _
+from mercurial import cmdutil, commands, extensions, hg, mdiff
+from mercurial import util
+import copy, cStringIO, errno, operator, os, re, tempfile
+
+
+import signal
+import locale
+from crpatch import Patch, header, hunk, HunkLine
+
+# os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce'
+if os.name == 'posix':
+    import curses
+else:
+    # I have no idea if wcurses works with crecord...
+    import wcurses as curses
+
+import curses.textpad
+
+try:
+    curses
+except NameError:
+    raise util.Abort(_('the python curses/wcurses module is not available/installed'))
+    
+
+# deal with unicode correctly
+locale.setlocale(locale.LC_ALL, '')
+
+def chunkselector(opts, headerList):
+    """
+    Curses interface to get selection of chunks, and mark the applied flags
+    of the chosen chunks.
+
+    """
+    stdscr = curses.initscr()
+    curses.start_color()
+
+    chunkSelector = CursesChunkSelector(headerList)
+    curses.wrapper(chunkSelector.main, opts)
+
+class CursesChunkSelector(object):
+    def __init__(self, headerList):
+        # put the headers into a patch object
+        self.headerList = Patch(headerList)
+
+        # list of all chunks
+        self.chunkList = []
+        for h in headerList:
+            self.chunkList.append(h)
+            self.chunkList.extend(h.hunks)
+
+        # dictionary mapping (fgColor,bgColor) pairs to the corresponding curses
+        # color-pair value.
+        self.colorPairs = {}
+        # maps custom nicknames of color-pairs to curses color-pair values
+        self.colorPairNames = {}
+
+        # the currently selected header, hunk, or hunk-line
+        self.currentSelectedItem = self.headerList[0]
+
+        # updated when printing out patch-display -- the 'lines' here are the
+        # line positions *in the pad*, not on the screen.
+        self.selectedItemStartLine = 0
+        self.selectedItemEndLine = None
+
+        # define indentation levels
+        self.headerIndentNumChars = 0
+        self.hunkIndentNumChars = 3
+        self.hunkLineIndentNumChars = 6
+
+        # the first line of the pad to print to the screen
+        self.firstLineOfPadToPrint = 0
+
+        # keeps track of the number of lines in the pad
+        self.numPadLines = None
+
+        self.numStatusLines = 2
+
+        # keep a running count of the number of lines printed to the pad
+        # (used for determining when the selected item begins/ends)
+        self.linesPrintedToPadSoFar = 0
+
+        # the first line of the pad which is visible on the screen
+        self.firstLineOfPadToPrint = 0
+
+        # stores optional text for a commit comment provided by the user
+        self.commentText = ""
+
+    def upArrowEvent(self):
+        """
+        Try to select the previous item to the current item that has the
+        most-indented level.  For example, if a hunk is selected, try to select
+        the last HunkLine of the hunk prior to the selected hunk.  Or, if
+        the first HunkLine of a hunk is currently selected, then select the
+        hunk itself.
+
+        If the currently selected item is already at the top of the screen, 
+        scroll the screen down to show the new-selected item.
+
+        """
+        currentItem = self.currentSelectedItem
+
+        nextItem = currentItem.prevItem(constrainLevel=False)
+
+        if nextItem is None:
+            # if no parent item (i.e. currentItem is the first header), then
+            # no change...
+            nextItem = currentItem
+
+        self.currentSelectedItem = nextItem
+
+    def upArrowShiftEvent(self):
+        """
+        Select (if possible) the previous item on the same level as the currently
+        selected item.  Otherwise, select (if possible) the parent-item of the
+        currently selected item.
+
+        If the currently selected item is already at the top of the screen, 
+        scroll the screen down to show the new-selected item.
+
+        """
+        currentItem = self.currentSelectedItem
+        nextItem = currentItem.prevItem()
+        # if there's no previous item on this level, try choosing the parent
+        if nextItem is None:
+            nextItem = currentItem.parentItem()
+        if nextItem is None:
+            # if no parent item (i.e. currentItem is the first header), then
+            # no change...
+            nextItem = currentItem
+
+        self.currentSelectedItem = nextItem
+
+    def downArrowEvent(self):
+        """
+        Try to select the next item to the current item that has the
+        most-indented level.  For example, if a hunk is selected, select
+        the first HunkLine of the selected hunk.  Or, if the last HunkLine of
+        a hunk is currently selected, then select the next hunk, if one exists,
+        or if not, the next header if one exists.
+
+        If the currently selected item is already at the bottom of the screen, 
+        scroll the screen up to show the new-selected item.
+
+        """
+        #self.startPrintLine += 1 #DEBUG
+        currentItem = self.currentSelectedItem
+
+        nextItem = currentItem.nextItem(constrainLevel=False)
+        # if there's no next item, keep the selection as-is
+        if nextItem is None:
+            nextItem = currentItem
+
+        self.currentSelectedItem = nextItem
+
+    def downArrowShiftEvent(self):
+        """
+        If the cursor is already at the bottom chunk, scroll the screen up and move the cursor-position
+        to the subsequent chunk.  Otherwise, only move the cursor position down one chunk.
+
+        """
+        # TODO: update docstring
+
+        currentItem = self.currentSelectedItem
+        nextItem = currentItem.nextItem()
+        # if there's no previous item on this level, try choosing the parent's
+        # nextItem.
+        if nextItem is None:
+            try:
+                nextItem = currentItem.parentItem().nextItem()
+            except AttributeError:
+                # parentItem returned None, so nextItem() can't be called
+                nextItem = None
+        if nextItem is None:
+            # if no next item on parent-level, then no change...
+            nextItem = currentItem
+
+        self.currentSelectedItem = nextItem
+
+    def rightArrowEvent(self):
+        """
+        Select (if possible) the first of this item's child-items.
+
+        """
+        currentItem = self.currentSelectedItem
+        nextItem = currentItem.firstChild()
+
+        # turn off folding if we want to show a child-item
+        if currentItem.folded:
+            self.toggleFolded(currentItem)
+
+        if nextItem is None:
+            # if no next item on parent-level, then no change...
+            nextItem = currentItem
+
+        self.currentSelectedItem = nextItem
+
+    def leftArrowEvent(self):
+        """
+        Select (if possible) the parent of this item.
+        Otherwise, if this item is a header, then fold it.
+
+        """
+        currentItem = self.currentSelectedItem
+        nextItem = currentItem.parentItem()
+        
+        if nextItem is None:
+            # if no item on parent-level, then no change...
+            nextItem = currentItem
+            if not nextItem.folded:
+                self.toggleFolded(item=nextItem)
+
+        self.currentSelectedItem = nextItem
+
+    def updateScroll(self):
+        "Scroll the screen in such a way to fully show the currently-selected item."
+        selStart = self.selectedItemStartLine
+        selEnd = self.selectedItemEndLine
+        #selNumLines = selEnd - selStart
+        padStart = self.firstLineOfPadToPrint
+        padEnd = padStart + self.yScreenSize - self.numStatusLines - 1
+        screenMiddleLine = self.yScreenSize / 2
+        # 'buffered' pad start/end values which scroll with a certain
+        # top/bottom context margin
+        padStartBuffered = padStart + 3
+        padEndBuffered = padEnd - 3
+
+        if selEnd > padEndBuffered:
+            self.scrollLines(selEnd - padEndBuffered)
+        elif selStart < padStartBuffered:
+            # negative values scroll in pgup direction
+            self.scrollLines(selStart - padStartBuffered)
+
+
+    def scrollLines(self, numLines):
+        "Scroll the screen up (down) by numLines when numLines >0 (<0)."
+        self.firstLineOfPadToPrint += numLines
+        if self.firstLineOfPadToPrint < 0:
+            self.firstLineOfPadToPrint = 0
+        if self.firstLineOfPadToPrint > self.numPadLines-1:
+            self.firstLineOfPadToPrint = self.numPadLines-1
+
+    def toggleApply(self, item=None):
+        """
+        Toggle the applied flag of the specified item.  If no item is specified,
+        toggle the flag of the currently selected item.
+
+        """
+        if item is None:
+            item = self.currentSelectedItem
+
+        item.applied = not item.applied
+
+        if isinstance(item, header):
+            item.partial = False
+            if item.applied:
+                if not item.special():
+                    # apply all its hunks
+                    for hnk in item.hunks:
+                        hnk.applied = True
+                        # apply all their HunkLines
+                        for hunkLine in hnk.changedLines:
+                            hunkLine.applied = True
+                else:
+                    # all children are off (but the header is on)
+                    if len(item.allChildren()) > 0:
+                        item.partial = True
+            else:
+                # un-apply all its hunks
+                for hnk in item.hunks:
+                    hnk.applied = False
+                    # un-apply all their HunkLines
+                    for hunkLine in hnk.changedLines:
+                        hunkLine.applied = False
+        elif isinstance(item, hunk):
+            item.partial = False
+            # apply all it's HunkLines
+            for hunkLine in item.changedLines:
+                hunkLine.applied = item.applied
+
+            siblingAppliedStatus = [hnk.applied for hnk in item.header.hunks]
+            allSiblingsApplied = not (False in siblingAppliedStatus) 
+            noSiblingsApplied = not (True in siblingAppliedStatus)
+
+            siblingsPartialStatus = [hnk.partial for hnk in item.header.hunks]
+            someSiblingsPartial = (True in siblingsPartialStatus)
+
+            #cases where applied or partial should be removed from header
+
+            # if no 'sibling' hunks are applied (including this hunk)
+            if noSiblingsApplied:
+                if not item.header.special():
+                    item.header.applied = False
+                    item.header.partial = False
+            else: # some/all parent siblings are applied
+                item.header.applied = True
+                item.header.partial = (someSiblingsPartial or \
+                                        not allSiblingsApplied)
+
+        elif isinstance(item, HunkLine):
+            siblingAppliedStatus = [hnkln.applied for hnkln in item.hunk.changedLines]
+            allSiblingsApplied = not (False in siblingAppliedStatus) 
+            noSiblingsApplied = not (True in siblingAppliedStatus)
+
+            # if no 'sibling' lines are applied
+            if noSiblingsApplied:
+                item.hunk.applied = False
+                item.hunk.partial = False
+            elif allSiblingsApplied:
+                item.hunk.applied = True
+                item.hunk.partial = False
+            else: # some siblings applied
+                item.hunk.applied = True
+                item.hunk.partial = True
+
+            parentSiblingsAppliedStatus = [hnk.applied for hnk in item.hunk.header.hunks]
+            noParentSiblingsApplied = not (True in parentSiblingsAppliedStatus)
+            allParentSiblingsApplied = not (False in parentSiblingsAppliedStatus)
+
+            parentSiblingsPartialStatus = [hnk.partial for hnk in item.hunk.header.hunks]
+            someParentSiblingsPartial = (True in parentSiblingsPartialStatus)
+
+            # if all parent hunks are not applied, un-apply header
+            if noParentSiblingsApplied:
+                if not item.hunk.header.special():
+                    item.hunk.header.applied = False
+                    item.hunk.header.partial = False
+            # set the applied and partial status of the header if needed
+            else: # some/all parent siblings are applied
+                item.hunk.header.applied = True
+                item.hunk.header.partial = (someParentSiblingsPartial or \
+                                            not allParentSiblingsApplied)
+    def toggleFolded(self, item=None, foldParent=False):
+        "Toggle folded flag of specified item (defaults to currently selected)"
+        if item is None:
+            item = self.currentSelectedItem
+        if foldParent or (isinstance(item, header) and item.neverUnfolded):
+            if not isinstance(item, header):
+                # we need to select the parent item in this case
+                self.currentSelectedItem = item = item.parentItem()
+            elif item.neverUnfolded:
+                item.neverUnfolded = False
+            
+            # also fold any foldable children of the parent/current item
+            if isinstance(item, header): # the original OR 'new' item
+                for child in item.allChildren():
+                    child.folded = not item.folded
+
+        if isinstance(item, (header, hunk)):
+            item.folded = not item.folded
+
+
+    def alignString(self, inStr, window):
+        """
+        Add whitespace to the end of a string in order to make it fill
+        the screen in the x direction.  The current cursor position is
+        taken into account when making this calculation.  The string can span
+        multiple lines.
+
+        """
+        y,xStart = window.getyx()
+        width = self.xScreenSize
+        # turn tabs into spaces
+        inStr = inStr.expandtabs(4)
+        strLen = len(unicode(util.fromlocal(inStr), 'utf-8'))
+        numSpaces = (width - ((strLen + xStart) % width) - 1)
+        return inStr + " " * numSpaces + "\n"
+
+    def printString(self, window, text, fgColor=None, bgColor=None, pair=None,
+        pairName=None, attrList=None, toWin=True, align=True, showWhtSpc=False):
+        """
+        Print the string, text, with the specified colors and attributes, to
+        the specified curses window object.
+
+        The foreground and background colors are of the form
+        curses.COLOR_XXXX, where XXXX is one of: [BLACK, BLUE, CYAN, GREEN,
+        MAGENTA, RED, WHITE, YELLOW].  If pairName is provided, a color
+        pair will be looked up in the self.colorPairNames dictionary.
+
+        attrList is a list containing text attributes in the form of 
+        curses.A_XXXX, where XXXX can be: [BOLD, DIM, NORMAL, STANDOUT,
+        UNDERLINE].
+        
+        If align == True, whitespace is added to the printed string such that
+        the string stretches to the right border of the window.
+        
+        If showWhtSpc == True, trailing whitespace of a string is highlighted.
+
+        """
+        # preprocess the text, converting tabs to spaces
+        text = text.expandtabs(4)
+
+        if pair is not None:
+            colorPair = pair
+        elif pairName is not None:
+            colorPair = self.colorPairNames[pairName]
+        else:
+            if fgColor is None:
+                fgColor = curses.COLOR_WHITE
+            if bgColor is None:
+                bgColor = curses.COLOR_BLACK
+            if self.colorPairs.has_key((fgColor,bgColor)):
+                colorPair = self.colorPairs[(fgColor,bgColor)]
+            else:
+                colorPair = self.getColorPair(fgColor, bgColor)
+        # add attributes if possible
+        if attrList is None:
+            attrList = []
+        if colorPair < 256:
+            # then it is safe to apply all attributes
+            for textAttr in attrList:
+                colorPair |= textAttr
+        else:
+            # just apply a select few (safe?) attributes
+            for textAttr in (curses.A_UNDERLINE, curses.A_BOLD):
+                if textAttr in attrList:
+                    colorPair |= textAttr
+
+        y,xStart = self.chunkpad.getyx()
+        t = "" # variable for counting lines printed
+        # if requested, show trailing whitespace
+        if showWhtSpc:
+            origLen = len(text)
+            text = text.rstrip(' \n') # tabs have already been expanded
+            strippedLen = len(text)
+            numTrailingSpaces = origLen - strippedLen
+        
+        if toWin:
+            window.addstr(text, colorPair)
+        t += text
+
+        if showWhtSpc:
+                wsColorPair = colorPair | curses.A_REVERSE
+                if toWin:
+                    for i in range(numTrailingSpaces):
+                        window.addch(curses.ACS_CKBOARD, wsColorPair)
+                t += " " * numTrailingSpaces
+        
+        if align:
+            if toWin:
+                extraWhiteSpace = self.alignString("", window)
+                window.addstr(extraWhiteSpace, colorPair)
+            else:
+                # need to use t, since the x position hasn't incremented
+                extraWhiteSpace = self.alignString(t, window)
+            t += extraWhiteSpace
+        
+        # is reset to 0 at the beginning of printItem()
+        
+        linesPrinted = (xStart + len(t)) / self.xScreenSize
+        self.linesPrintedToPadSoFar += linesPrinted
+        return t
+
+    def updateScreen(self):
+        self.statuswin.erase()
+        self.chunkpad.erase()
+
+        width = self.xScreenSize
+        alignString = self.alignString
+        printString = self.printString
+
+        # print out the status lines at the top
+        try:
+            printString(self.statuswin, "SELECT CHUNKS: (j/k/up/down/pgup/pgdn) move cursor; (space) toggle applied", pairName="legend")
+            printString(self.statuswin, " (f)old/unfold; (c)ommit applied; (q)uit; (?) help | [X]=hunk applied **=folded", pairName="legend")
+        except curses.error:
+            pass
+
+        # print out the patch in the remaining part of the window
+        try:
+            self.printItem()
+            self.updateScroll()
+            self.chunkpad.refresh(self.firstLineOfPadToPrint,0,self.numStatusLines,0,self.yScreenSize+1-self.numStatusLines,self.xScreenSize)
+        except curses.error:
+            pass
+
+        # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
+        self.statuswin.refresh()
+
+    def getStatusPrefixString(self, item):
+        """
+        Create a string to prefix a line with which indicates whether 'item'
+        is applied and/or folded.
+
+        """
+        # create checkBox string
+        if item.applied:
+            if not isinstance(item, HunkLine) and item.partial:
+                checkBox = "[~]"
+            else:
+                checkBox = "[X]"
+        else:
+            checkBox = "[ ]"
+
+        try:
+            if item.folded:
+                checkBox += "**"
+                if isinstance(item, header):
+                    # one of "M", "A", or "D" (modified, added, deleted)
+                    fileStatus = item.changetype
+                 
+                    checkBox += fileStatus + " "
+            else:
+                checkBox += "  "
+                if isinstance(item, header):
+                    # add two more spaces for headers
+                    checkBox += "  "
+        except AttributeError: # not foldable
+            checkBox += "  "
+
+        return checkBox
+
+    def printHeader(self, header, selected=False, toWin=True, ignoreFolding=False):
+        """
+        Print the header to the pad.  If countLines is True, don't print
+        anything, but just count the number of lines which would be printed.
+
+        """
+        outStr = ""
+        text = header.prettyStr()
+        chunkIndex = self.chunkList.index(header)
+
+        if chunkIndex != 0 and not header.folded:
+            # add separating line before headers
+            outStr += self.printString(self.chunkpad, '_'*self.xScreenSize, toWin=toWin, align=False)
+        # select color-pair based on if the header is selected
+        if selected:
+            colorPair = self.getColorPair(name="selected", attrList=[curses.A_BOLD])
+        else:
+            colorPair = self.getColorPair(name="normal", attrList=[curses.A_BOLD])
+
+        # print out each line of the chunk, expanding it to screen width
+
+        # number of characters to indent lines on this level by
+        indentNumChars = 0
+        checkBox = self.getStatusPrefixString(header)
+        if not header.folded or ignoreFolding:
+            textList = text.split("\n")
+            lineStr = checkBox + textList[0]
+        else:
+            lineStr = checkBox + header.filename()
+        outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin)
+        if not header.folded or ignoreFolding:
+            if len(textList) > 1:
+                for line in textList[1:]:
+                    lineStr = " "*(indentNumChars + len(checkBox)) + line
+                    outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin)
+
+        return outStr
+
+    def printHunkLinesBefore(self, hunk, selected=False, toWin=True, ignoreFolding=False):
+        "includes start/end line indicator"
+        outStr = ""
+        # where hunk is in list of siblings
+        hunkIndex = hunk.header.hunks.index(hunk)
+
+        if hunkIndex != 0:
+            # add separating line before headers
+            outStr += self.printString(self.chunkpad, ' '*self.xScreenSize, toWin=toWin, align=False)
+
+        if selected:
+            colorPair = self.getColorPair(name="selected", attrList=[curses.A_BOLD])
+        else:
+            colorPair = self.getColorPair(name="normal", attrList=[curses.A_BOLD])
+
+        # print out from-to line with checkbox
+        checkBox = self.getStatusPrefixString(hunk)
+
+        linePrefix = " "*self.hunkIndentNumChars + checkBox
+        frToLine = "   " + hunk.getFromToLine().strip("\n")
+
+
+        outStr += self.printString(self.chunkpad, linePrefix, toWin=toWin, align=False) # add uncolored checkbox/indent
+        outStr += self.printString(self.chunkpad, frToLine, pair=colorPair, toWin=toWin)
+
+        if hunk.folded and not ignoreFolding:
+            # skip remainder of output
+            return outStr
+
+        # print out lines of the chunk preceeding changed-lines
+        for line in hunk.before:
+            lineStr = " "*(self.hunkLineIndentNumChars + len(checkBox)) + line.strip("\n")
+            outStr += self.printString(self.chunkpad, lineStr, toWin=toWin)
+
+        return outStr
+
+    def printHunkLinesAfter(self, hunk, toWin=True, ignoreFolding=False):
+        outStr = ""
+        if hunk.folded and not ignoreFolding:
+            return outStr
+
+        indentNumChars = self.hunkLineIndentNumChars-1
+        # a bit superfluous, but to avoid hard-coding indent amount
+        checkBox = self.getStatusPrefixString(hunk)
+        for line in hunk.after:
+            lineStr = " "*(indentNumChars + len(checkBox)) + line.strip("\n")
+            outStr += self.printString(self.chunkpad, lineStr, toWin=toWin)
+
+        return outStr
+
+    def printHunkChangedLine(self, hunkLine, selected=False, toWin=True):
+        outStr = ""
+        indentNumChars = self.hunkLineIndentNumChars
+        checkBox = self.getStatusPrefixString(hunkLine)
+
+        lineStr = hunkLine.prettyStr().strip("\n")
+
+        # select color-pair based on whether line is an addition/removal
+        if selected:
+            colorPair = self.getColorPair(name="selected")
+        elif lineStr.startswith("+"):
+            colorPair = self.getColorPair(name="addition")
+        elif lineStr.startswith("-"):
+            colorPair = self.getColorPair(name="deletion")
+        elif lineStr.startswith("\\"):
+            colorPair = self.getColorPair(name="normal")
+
+        linePrefix = " "*indentNumChars + checkBox
+        outStr += self.printString(self.chunkpad, linePrefix, toWin=toWin, align=False) # add uncolored checkbox/indent
+        outStr += self.printString(self.chunkpad, lineStr, pair=colorPair, toWin=toWin, showWhtSpc=True)
+        return outStr
+
+    def printItem(self, item=None, ignoreFolding=False, recurseChildren=True, toWin=True):
+        """
+        Use __printItem() to print the the specified item.applied.
+        If item is not specified, then print the entire patch.
+        (hiding folded elements, etc. -- see __printitem() docstring)
+        """
+        if item is None:
+            item = self.headerList
+        if recurseChildren:
+            self.linesPrintedToPadSoFar = 0
+            global outStr
+        retStr = self.__printItem(item, ignoreFolding, recurseChildren, toWin=toWin)
+        if recurseChildren:
+            # remove the string when finished, so it doesn't accumulate
+            del outStr
+
+        return retStr
+
+    def __printItem(self, item, ignoreFolding, recurseChildren, toWin=True):
+        """
+        Recursive method for printing out patch/header/hunk/hunk-line data to
+        screen.  Also returns a string with all of the content of the displayed
+        patch (not including coloring, etc.).
+
+        If ignoreFolding is True, then folded items are printed out.
+
+        If recurseChildren is False, then only print the item without its
+        child items.
+
+        """
+        # keep outStr local, since we're not recursing
+        if recurseChildren:
+            global outStr
+            try:
+                outStr
+            except:
+                outStr = ""
+        else:
+            outStr = ""
+
+        selected = (item is self.currentSelectedItem)
+        if selected and recurseChildren:
+            # assumes line numbering starting from line 0
+            self.selectedItemStartLine = self.linesPrintedToPadSoFar
+            selectedItemLines = self.getNumLinesDisplayed(item, recurseChildren=False)
+            self.selectedItemEndLine = self.selectedItemStartLine + selectedItemLines - 1
+
+        # Patch object is a list of headers
+        if isinstance(item, Patch):
+            if recurseChildren:
+                for hdr in item:
+                    self.__printItem(hdr, ignoreFolding, recurseChildren, toWin)
+        # TODO: eliminate all isinstance() calls
+        if isinstance(item, header):
+            outStr += self.printHeader(item, selected, toWin=toWin, ignoreFolding=ignoreFolding)
+            if recurseChildren:
+                for hnk in item.hunks:
+                    self.__printItem(hnk, ignoreFolding, recurseChildren, toWin)
+        elif isinstance(item, hunk) and \
+        ((not item.header.folded) or ignoreFolding):
+            # print the hunk data which comes before the changed-lines
+            outStr += self.printHunkLinesBefore(item, selected, toWin=toWin, ignoreFolding=ignoreFolding)
+            if recurseChildren:
+                for line in item.changedLines:
+                    self.__printItem(line, ignoreFolding, recurseChildren, toWin)
+                outStr += self.printHunkLinesAfter(item, toWin=toWin, ignoreFolding=ignoreFolding)
+        elif isinstance(item, HunkLine) and ((not item.hunk.folded) or ignoreFolding):
+            outStr += self.printHunkChangedLine(item, selected, toWin=toWin)
+
+        return outStr
+
+    def getNumLinesDisplayed(self, item=None, ignoreFolding=False, recurseChildren=True):
+        """
+        Return the number of lines which would be displayed if the item were
+        to be printed to the display.  The item will NOT be printed to the
+        display (pad).
+        If no item is given, assume the entire patch.
+        If ignoreFolding is True, folded items will be unfolded when counting
+        the number of lines.
+
+        """
+        # temporarily disable printing to windows by printString
+        patchDisplayString = self.printItem(item, ignoreFolding, recurseChildren, toWin=False)
+        numLines = len(patchDisplayString)/self.xScreenSize
+        return numLines
+
+    def sigwinchHandler(self, n, frame):
+        "Handle window resizing"
+        try:
+            curses.endwin()
+            self.stdscr = curses.initscr()
+            self.yScreenSize, self.xScreenSize = self.stdscr.getmaxyx()
+
+            self.statuswin = curses.newwin(self.numStatusLines,self.xScreenSize,0,0)
+        except curses.error:
+            pass
+            # TODO: make resizing to a smaller width work (also for help screen)
+            # re-calculate an upper-bound on the number of lines in the pad
+            #self.numPadLines = self.getNumLinesDisplayed()
+            #self.chunkpad = curses.newpad(self.numPadLines, self.xScreenSize)
+            #self.updateScreen()
+
+    def getColorPair(self, fgColor=None, bgColor=None, name=None, attrList=None):
+        """
+        Get a curses color pair, adding it to self.colorPairs if it is not already
+        defined.  An optional string, name, can be passed as a shortcut for
+        referring to the color-pair.  By default, if no arguments are specified,
+        the white foreground / black background color-pair is returned.
+
+        It is expected that this function will be used exclusively for initializing
+        color pairs, and NOT curses.init_pair().
+
+        attrList is used to 'flavor' the returned color-pair.  This information
+        is not stored in self.colorPairs.  It contains attribute values like
+        curses.A_BOLD.
+
+        """
+        if (name is not None) and self.colorPairNames.has_key(name):
+            # then get the associated color pair and return it
+            colorPair = self.colorPairNames[name]
+        else:
+            if fgColor is None:
+                fgColor = curses.COLOR_WHITE
+            if bgColor is None:
+                bgColor = curses.COLOR_BLACK
+            if self.colorPairs.has_key((fgColor,bgColor)):
+                colorPair = self.colorPairs[(fgColor,bgColor)]
+            else:
+                pairIndex = len(self.colorPairs) + 1
+                curses.init_pair(pairIndex, fgColor, bgColor)
+                colorPair = self.colorPairs[(fgColor, bgColor)] = curses.color_pair(pairIndex)
+                if name is not None:
+                    self.colorPairNames[name] = curses.color_pair(pairIndex)
+
+        # add attributes if possible
+        if attrList is None:
+            attrList = []
+        if colorPair < 256:
+            # then it is safe to apply all attributes
+            for textAttr in attrList:
+                colorPair |= textAttr
+        else:
+            # just apply a select few (safe?) attributes
+            for textAttrib in (curses.A_UNDERLINE, curses.A_BOLD):
+                if textAttrib in attrList:
+                    colorPair |= textAttrib
+        return colorPair
+
+    def initColorPair(self, *args, **kwargs):
+        "Same as getColorPair."
+        self.getColorPair(*args, **kwargs)    
+
+    def helpWindow(self):
+        "Print a help window to the screen.  Exit after any keypress."
+        helpText = """            [press any key to return to the patch-display]
+
+crecord allows you to interactively choose among the changes you have made,
+and commit only those changes you select.  After committing the selected
+changes, the unselected changes are still present in your working copy, so you
+can use crecord multiple times to split large changes into smaller changesets.
+The following are valid keystrokes:
+
+                [SPACE] : (un-)select item ([~]/[X] = partly/fully applied)
+    Up/Down-arrow [k/j] : go to previous/next unfolded item
+        PgUp/PgDn [K/J] : go to previous/next item of same type
+ Right/Left-arrow [l/h] : go to child item / parent item
+                      f : fold / unfold item, hiding/revealing its children
+                      F : fold / unfold parent item and all of its ancestors
+                      m : edit / resume editing the commit message
+                      c : commit selected changes
+                      q : quit without committing (no changes will be made)
+                      ? : help (what you're currently reading)"""
+
+        helpwin = curses.newwin(self.yScreenSize,0,0,0)
+        helpLines = helpText.split("\n")
+        helpLines = helpLines + [" "]*(self.yScreenSize-self.numStatusLines-len(helpLines)-1)
+        try:
+            for line in helpLines:
+                self.printString(helpwin, line, pairName="legend")
+        except curses.error:
+            pass
+        helpwin.refresh()
+        self.stdscr.getch()
+
+    def commitMessageWindow(self):
+        "Create a temporary commit message editing window on the screen."
+        # In Python versions < 2.6, there is no insert mode (only overwrite) :(
+        def keyFilter(key):
+            "provide keymappings to emacs-style keys"
+            if key in (7,):
+                # diable keys we're re-mapping
+                return ""
+            elif key in (24, 3): # CTRL-X,  3 = CTRL-C
+                return 7 # CTRL-G (i.e. exit comment window)
+            else:
+                return key
+        statusline = curses.newwin(2,0,0,0)
+        statusLineText = \
+        " Begin/resume editing commit message. CTRL-C/-X returns to patch view."
+        self.printString(statusline, statusLineText, pairName="legend")
+        statusline.refresh()
+        helpwin = curses.newwin(self.yScreenSize-1,0,1,0)
+        reversedCommentText = self.commentText[::-1]
+        for char in reversedCommentText:
+            curses.ungetch(ord(char))
+        t = curses.textpad.Textbox(helpwin)
+        curses.raw()
+        self.commentText = t.edit(keyFilter).rstrip(" \n")
+        curses.cbreak()
+
+    def confirmCommit(self):
+        "Ask for 'Y' to be pressed to confirm commit. Return True if confirmed."
+        confirmText = "Are you sure you want to commit the selected changes [yN]? "
+
+        confirmWin = curses.newwin(self.yScreenSize,0,0,0)
+        try:
+            self.printString(confirmWin, confirmText, pairName="selected")
+        except curses.error:
+            pass
+        confirmWin.refresh()
+        try:
+            response = chr(self.stdscr.getch())
+        except ValueError:
+            response = "n"
+        if response.lower().startswith("y"):
+            return True
+        else:
+            return False
+
+    def main(self, stdscr, opts):
+        """
+        Method to be wrapped by curses.wrapper() for selecting chunks.
+
+        """
+        signal.signal(signal.SIGWINCH, self.sigwinchHandler)
+        self.stdscr = stdscr
+        self.yScreenSize, self.xScreenSize = self.stdscr.getmaxyx()
+
+        # available colors: black, blue, cyan, green, magenta, white, yellow
+        # init_pair(color_id, foreground_color, background_color)
+        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_BLACK, name="normal")
+        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_RED, name="selected")
+        self.initColorPair(curses.COLOR_RED, curses.COLOR_BLACK, name="deletion")
+        self.initColorPair(curses.COLOR_GREEN, curses.COLOR_BLACK, name="addition")
+        self.initColorPair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
+        # newwin([height, width,] begin_y, begin_x)
+        self.statuswin = curses.newwin(self.numStatusLines,0,0,0)
+
+        # figure out how much space to allocate for the chunk-pad which is
+        # used for displaying the patch
+
+        # stupid hack to prevent getNumLinesDisplayed from failing
+        self.chunkpad = curses.newpad(1,self.xScreenSize)
+
+        # add 1 so to account for last line text reaching end of line
+        self.numPadLines = self.getNumLinesDisplayed(ignoreFolding=True) + 1
+        self.chunkpad = curses.newpad(self.numPadLines, self.xScreenSize)
+
+        # initialize selecteItemEndLine (initial start-line is 0)
+        self.selectedItemEndLine = self.getNumLinesDisplayed(self.currentSelectedItem, recurseChildren=False)
+
+        try:
+            self.commentText = opts['message']
+        except KeyError:
+            pass
+
+
+        #import rpdb2; rpdb2.start_embedded_debugger("secret")
+        #import rpdb2; rpdb2.setbreak()
+
+        while True:
+            self.updateScreen()
+            self.lastKeyPressed = keyPressed = stdscr.getch()
+            if keyPressed in [ord("k"), curses.KEY_UP]:
+                self.upArrowEvent()
+            if keyPressed in [ord("K"), curses.KEY_PPAGE]:
+                self.upArrowShiftEvent()
+            elif keyPressed in [ord("j"), curses.KEY_DOWN]:
+                self.downArrowEvent()
+            elif keyPressed in [ord("J"), curses.KEY_NPAGE]:
+                self.downArrowShiftEvent()
+            elif keyPressed in [ord("l"), curses.KEY_RIGHT]:
+                self.rightArrowEvent()
+            elif keyPressed in [ord("h"), curses.KEY_LEFT]:
+                self.leftArrowEvent()
+            elif keyPressed in [ord("q")]:
+                raise util.Abort(_('user quit'))
+            elif keyPressed in [ord("c")]:
+                if self.confirmCommit():
+                    break
+            elif keyPressed in [ord(' ')]:
+                self.toggleApply()
+            elif keyPressed in [ord("f")]:
+                self.toggleFolded()
+            elif keyPressed in [ord("F")]:
+                self.toggleFolded(foldParent=True)
+            elif keyPressed in [ord("?")]:
+                self.helpWindow()
+            elif keyPressed in [ord("m")]:
+                self.commitMessageWindow()
+
+        if self.commentText != "":
+            opts['message'] = self.commentText
new file mode 100644
--- /dev/null
+++ b/crecord/crecord_core.py
@@ -0,0 +1,180 @@
+# crecord.py
+#
+# Copyright 2008 Mark Edgington <edgimar@gmail.com>
+#
+# This software may be used and distributed according to the terms of
+# the GNU General Public License, incorporated herein by reference.
+#
+# Much of this extension is based on Bryan O'Sullivan's record extension.
+
+'''text-gui based change selection during commit or qrefresh'''
+from mercurial.i18n import _
+from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
+from mercurial import util
+import cStringIO, errno, os, re, tempfile
+
+from crpatch import parsepatch, filterpatch
+from chunk_selector import chunkselector
+
+def dorecord(ui, repo, committer, *pats, **opts):
+    if not ui.interactive:
+        raise util.Abort(_('running non-interactively, use commit instead'))
+
+    def recordfunc(ui, repo, message, match, opts):
+        """This is generic record driver.
+
+        It's job is to interactively filter local changes, and accordingly
+        prepare working dir into a state, where the job can be delegated to
+        non-interactive commit command such as 'commit' or 'qrefresh'.
+
+        After the actual job is done by non-interactive command, working dir
+        state is restored to original.
+
+        In the end we'll record intresting changes, and everything else will be
+        left in place, so the user can continue his work.
+        """
+        if match.files():
+            changes = None
+        else:
+            changes = repo.status(match=match)[:3]
+            modified, added, removed = changes
+            match = cmdutil.matchfiles(repo, modified + added + removed)
+        diffopts = mdiff.diffopts(git=True, nodates=True)
+        chunks = patch.diff(repo, repo.dirstate.parents()[0], match=match,
+                            changes=changes, opts=diffopts)
+        fp = cStringIO.StringIO()
+        fp.write(''.join(chunks))
+        fp.seek(0)
+
+        # 1. filter patch, so we have intending-to apply subset of it
+        if changes is not None:
+            chunks = filterpatch(opts, parsepatch(changes, fp), chunkselector)
+        else:
+            chgs = repo.status(match=match)[:3]
+            chunks = filterpatch(opts, parsepatch(chgs, fp))
+            
+        del fp
+
+        contenders = {}
+        for h in chunks:
+            try: contenders.update(dict.fromkeys(h.files()))
+            except AttributeError: pass
+
+        newfiles = [f for f in match.files() if f in contenders]
+
+        if not newfiles:
+            ui.status(_('no changes to record\n'))
+            return 0
+
+        if changes is None:
+            match = cmdutil.matchfiles(repo, newfiles)
+            changes = repo.status(match=match)
+        modified = dict.fromkeys(changes[0])
+
+        # 2. backup changed files, so we can restore them in the end
+        backups = {}
+        backupdir = repo.join('record-backups')
+        try:
+            os.mkdir(backupdir)
+        except OSError, err:
+            if err.errno != errno.EEXIST:
+                raise
+        try:
+            # backup continues
+            for f in newfiles:
+                if f not in modified:
+                    continue
+                fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
+                                               dir=backupdir)
+                os.close(fd)
+                ui.debug(_('backup %r as %r\n') % (f, tmpname))
+                util.copyfile(repo.wjoin(f), tmpname)
+                backups[f] = tmpname
+
+            fp = cStringIO.StringIO()
+            for c in chunks:
+                if c.filename() in backups:
+                    c.write(fp)
+            dopatch = fp.tell()
+            fp.seek(0)
+
+            # 3a. apply filtered patch to clean repo  (clean)
+            if backups:
+                hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
+
+            # 3b. (apply)
+            if dopatch:
+                try:
+                    ui.debug(_('applying patch\n'))
+                    ui.debug(fp.getvalue())
+                    patch.internalpatch(fp, ui, 1, repo.root)
+                except patch.PatchError, err:
+                    s = str(err)
+                    if s:
+                        raise util.Abort(s)
+                    else:
+                        raise util.Abort(_('patch failed to apply'))
+            del fp
+
+            # 4. We prepared working directory according to filtered patch.
+            #    Now is the time to delegate the job to commit/qrefresh or the like!
+
+            # it is important to first chdir to repo root -- we'll call a
+            # highlevel command with list of pathnames relative to repo root
+            cwd = os.getcwd()
+            os.chdir(repo.root)
+            try:
+                committer(ui, repo, newfiles, opts)
+            finally:
+                os.chdir(cwd)
+
+            return 0
+        finally:
+            # 5. finally restore backed-up files
+            try:
+                for realname, tmpname in backups.iteritems():
+                    ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
+                    util.copyfile(tmpname, repo.wjoin(realname))
+                    os.unlink(tmpname)
+                os.rmdir(backupdir)
+            except OSError:
+                pass
+    return cmdutil.commit(ui, repo, recordfunc, pats, opts)
+
+
+######  MAIN ENTRY POINTS FOR EXTENSION (crecord / qcrecord functions) ########
+
+def crecord(ui, repo, *pats, **opts):
+    '''interactively select changes to commit
+
+    If a list of files is omitted, all changes reported by "hg status"
+    will be candidates for recording.
+
+    See 'hg help dates' for a list of formats valid for -d/--date.
+
+    You will be shown a list of patch hunks from which you can select
+    those you would like to apply to the commit.
+
+    '''
+    def record_committer(ui, repo, pats, opts):
+        commands.commit(ui, repo, *pats, **opts)
+    dorecord(ui, repo, record_committer, *pats, **opts)
+
+
+def qcrecord(ui, repo, patch, *pats, **opts):
+    '''interactively record a new patch
+
+    see 'hg help qnew' & 'hg help record' for more information and usage
+    '''
+
+    try:
+        mq = extensions.find('mq')
+    except KeyError:
+        raise util.Abort(_("'mq' extension not loaded"))
+
+    def qrecord_committer(ui, repo, pats, opts):
+        mq.new(ui, repo, patch, *pats, **opts)
+
+    opts = opts.copy()
+    opts['force'] = True    # always 'qnew -f'
+    dorecord(ui, repo, qrecord_committer, *pats, **opts)
new file mode 100644
--- /dev/null
+++ b/crecord/crpatch.py
@@ -0,0 +1,661 @@
+# stuff related specifically to patch manipulation / parsing
+from mercurial.i18n import _
+from mercurial import patch
+
+import cStringIO
+import re
+
+lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
+
+def scanpatch(fp):
+    """like patch.iterhunks, but yield different events
+
+    - ('file',    [header_lines + fromfile + tofile])
+    - ('context', [context_lines])
+    - ('hunk',    [hunk_lines])
+    - ('range',   (-start,len, +start,len, diffp))
+    """
+    lr = patch.linereader(fp)
+
+    def scanwhile(first, p):
+        """scan lr while predicate holds"""
+        lines = [first]
+        while True:
+            line = lr.readline()
+            if not line:
+                break
+            if p(line):
+                lines.append(line)
+            else:
+                lr.push(line)
+                break
+        return lines
+
+    while True:
+        line = lr.readline()
+        if not line:
+            break
+        if line.startswith('diff --git a/'):
+            def notheader(line):
+                s = line.split(None, 1)
+                return not s or s[0] not in ('---', 'diff')
+            header = scanwhile(line, notheader)
+            fromfile = lr.readline()
+            if fromfile.startswith('---'):
+                tofile = lr.readline()
+                header += [fromfile, tofile]
+            else:
+                lr.push(fromfile)
+            yield 'file', header
+        elif line[0] == ' ':
+            yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
+        elif line[0] in '-+':
+            yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
+        else:
+            m = lines_re.match(line)
+            if m:
+                yield 'range', m.groups()
+            else:
+                raise patch.PatchError('unknown patch content: %r' % line)
+
+class PatchNode(object):
+    "Abstract Class for Patch Graph Nodes (i.e. PatchRoot, header, hunk, HunkLine)"
+
+    def firstChild(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def lastChild(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def allChildren(self):
+        "Return a list of all of the direct children of this node"
+        raise NotImplementedError("method must be implemented by subclass")
+    def nextSibling(self):
+        """
+        Return the closest next item of the same type where there are no items
+        of different types between the current item and this closest item.
+        If no such item exists, return None.
+
+        """
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def prevSibling(self):
+        """
+        Return the closest previous item of the same type where there are no
+        items of different types between the current item and this closest item.
+        If no such item exists, return None.
+
+        """
+        raise NotImplementedError("method must be implemented by subclass")
+
+    def parentItem(self):
+        raise NotImplementedError("method must be implemented by subclass")
+
+
+    def nextItem(self, constrainLevel=True, skipFolded=True):
+        """
+        If constrainLevel == True, return the closest next item
+        of the same type where there are no items of different types between
+        the current item and this closest item.
+
+        If constrainLevel == False, then try to return the next item
+        closest to this item, regardless of item's type (header, hunk, or
+        HunkLine).
+
+        If skipFolded == True, and the current item is folded, then the child
+        items that are hidden due to folding will be skipped when determining
+        the next item.
+
+        If it is not possible to get the next item, return None.
+
+        """
+        try:
+            itemFolded = self.folded
+        except AttributeError:
+            itemFolded = False
+        if constrainLevel:
+            return self.nextSibling()
+        elif skipFolded and itemFolded:
+            nextItem = self.nextSibling()
+            if nextItem is None:
+                try:
+                    nextItem = self.parentItem().nextSibling()
+                except AttributeError:
+                    nextItem = None
+            return nextItem
+        else:
+            # try child
+            item = self.firstChild()
+            if item is not None:
+                return item
+
+            # else try next sibling
+            item = self.nextSibling()
+            if item is not None:
+                return item
+
+            try:
+                # else try parent's next sibling
+                item = self.parentItem().nextSibling()
+                if item is not None:
+                    return item
+
+                # else return grandparent's next sibling (or None)
+                return self.parentItem().parentItem().nextSibling()
+
+            except AttributeError: # parent and/or grandparent was None
+                return None
+
+    def prevItem(self, constrainLevel=True, skipFolded=True):
+        """
+        If constrainLevel == True, return the closest previous item
+        of the same type where there are no items of different types between
+        the current item and this closest item.
+
+        If constrainLevel == False, then try to return the previous item
+        closest to this item, regardless of item's type (header, hunk, or
+        HunkLine).
+
+        If skipFolded == True, and the current item is folded, then the items
+        that are hidden due to folding will be skipped when determining the
+        next item.
+
+        If it is not possible to get the previous item, return None.
+
+        """
+        if constrainLevel:
+            return self.prevSibling()
+        else:
+            # try previous sibling's last child's last child,
+            # else try previous sibling's last child, else try previous sibling
+            prevSibling = self.prevSibling()
+            if prevSibling is not None:
+                prevSiblingLastChild = prevSibling.lastChild()
+                if (prevSiblingLastChild is not None) and not prevSibling.folded:
+                    prevSiblingLCLC = prevSiblingLastChild.lastChild()
+                    if (prevSiblingLCLC is not None) and not prevSiblingLastChild.folded:
+                        return prevSiblingLCLC
+                    else:
+                        return prevSiblingLastChild
+                else:
+                    return prevSibling
+
+            # try parent (or None)
+            return self.parentItem()
+
+class Patch(PatchNode, list): # TODO: rename PatchRoot
+    """
+    List of header objects representing the patch.
+
+    """
+    def __init__(self, headerList):
+        self.extend(headerList)
+        # add parent patch object reference to each header
+        for header in self:
+            header.patch = self
+
+class header(PatchNode):
+    """patch header
+
+    XXX shoudn't we move this to mercurial/patch.py ?
+    """
+    diff_re = re.compile('diff --git a/(.*) b/(.*)$')
+    allhunks_re = re.compile('(?:index|new file|deleted file) ')
+    pretty_re = re.compile('(?:new file|deleted file) ')
+    special_re = re.compile('(?:index|new|deleted|copy|rename) ')
+
+    def __init__(self, header):
+        self.header = header
+        self.hunks = []
+        # flag to indicate whether to apply this chunk
+        self.applied = True
+        # flag which only affects the status display indicating if a node's
+        # children are partially applied (i.e. some applied, some not).
+        self.partial = False
+
+        # flag to indicate whether to display as folded/unfolded to user
+        self.folded = True
+
+        # list of all headers in patch
+        self.patch = None
+
+        # flag is False if this header was ever unfolded from initial state
+        self.neverUnfolded = True
+    def binary(self):
+        """
+        Return True if the file represented by the header is a binary file.
+        Otherwise return False.
+
+        """
+        for h in self.header:
+            if h.startswith('index '):
+                return True
+        return False
+
+    def pretty(self, fp):
+        for h in self.header:
+            if h.startswith('index '):
+                fp.write(_('this modifies a binary file (all or nothing)\n'))
+                break
+            if self.pretty_re.match(h):
+                fp.write(h)
+                if self.binary():
+                    fp.write(_('this is a binary file\n'))
+                break
+            if h.startswith('---'):
+                fp.write(_('%d hunks, %d lines changed\n') %
+                         (len(self.hunks),
+                          sum([h.added + h.removed for h in self.hunks])))
+                break
+            fp.write(h)
+
+    def prettyStr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def write(self, fp):
+        fp.write(''.join(self.header))
+
+    def allhunks(self):
+        """
+        Return True if the file which the header represents was changed completely (i.e.
+        there is no possibility of applying a hunk of changes smaller than the size of the
+        entire file.)  Otherwise return False
+
+        """
+        for h in self.header:
+            if self.allhunks_re.match(h):
+                return True
+        return False
+
+    def files(self):
+        fromfile, tofile = self.diff_re.match(self.header[0]).groups()
+        if fromfile == tofile:
+            return [fromfile]
+        return [fromfile, tofile]
+
+    def filename(self):
+        return self.files()[-1]
+
+    def __repr__(self):
+        return '<header %s>' % (' '.join(map(repr, self.files())))
+
+    def special(self):
+        for h in self.header:
+            if self.special_re.match(h):
+                return True
+
+    def nextSibling(self):
+        numHeadersInPatch = len(self.patch)
+        indexOfThisHeader = self.patch.index(self)
+
+        if indexOfThisHeader < numHeadersInPatch - 1:
+            nextHeader = self.patch[indexOfThisHeader + 1]
+            return nextHeader
+        else:
+            return None
+
+    def prevSibling(self):
+        indexOfThisHeader = self.patch.index(self)
+        if indexOfThisHeader > 0:
+            previousHeader = self.patch[indexOfThisHeader - 1]
+            return previousHeader
+        else:
+            return None
+
+    def parentItem(self):
+        """
+        There is no 'real' parent item of a header that can be selected,
+        so return None.
+        """
+        return None
+
+    def firstChild(self):
+        "Return the first child of this item, if one exists.  Otherwise None."
+        if len(self.hunks) > 0:
+            return self.hunks[0]
+        else:
+            return None
+
+    def lastChild(self):
+        "Return the last child of this item, if one exists.  Otherwise None."
+        if len(self.hunks) > 0:
+            return self.hunks[-1]
+        else:
+            return None
+
+    def allChildren(self):
+        "Return a list of all of the direct children of this node"
+        return self.hunks
+class HunkLine(PatchNode):
+    "Represents a changed line in a hunk"
+    def __init__(self, lineText, hunk):
+        self.lineText = lineText
+        self.applied = True
+        # the parent hunk to which this line belongs
+        self.hunk = hunk
+        # folding lines currently is not used/needed, but this flag is needed
+        # in the prevItem method.
+        self.folded = False
+
+    def prettyStr(self):
+        return self.lineText
+
+    def nextSibling(self):
+        numLinesInHunk = len(self.hunk.changedLines)
+        indexOfThisLine = self.hunk.changedLines.index(self)
+
+        if (indexOfThisLine < numLinesInHunk - 1):
+            nextLine = self.hunk.changedLines[indexOfThisLine + 1]
+            return nextLine
+        else:
+            return None
+
+    def prevSibling(self):
+        indexOfThisLine = self.hunk.changedLines.index(self)
+        if indexOfThisLine > 0:
+            previousLine = self.hunk.changedLines[indexOfThisLine - 1]
+            return previousLine
+        else:
+            return None
+
+    def parentItem(self):
+        "Return the parent to the current item"
+        return self.hunk
+
+    def firstChild(self):
+        "Return the first child of this item, if one exists.  Otherwise None."
+        # hunk-lines don't have children
+        return None
+
+    def lastChild(self):
+        "Return the last child of this item, if one exists.  Otherwise None."
+        # hunk-lines don't have children
+        return None
+
+class hunk(PatchNode):
+    """patch hunk
+
+    XXX shouldn't we merge this with patch.hunk ?
+    """
+    maxcontext = 3
+
+    def __init__(self, header, fromline, toline, proc, before, hunk, after):
+        def trimcontext(number, lines):
+            delta = len(lines) - self.maxcontext
+            if False and delta > 0:
+                return number + delta, lines[:self.maxcontext]
+            return number, lines
+
+        self.header = header
+        self.fromline, self.before = trimcontext(fromline, before)
+        self.toline, self.after = trimcontext(toline, after)
+        self.proc = proc
+        self.changedLines = [HunkLine(line, self) for line in hunk]
+        self.added, self.removed = self.countchanges()
+        # used at end for detecting how many removed lines were un-applied
+        self.originalremoved = self.removed
+
+        # flag to indicate whether to display as folded/unfolded to user
+        self.folded = True
+        # flag to indicate whether to apply this chunk
+        self.applied = True
+        # flag which only affects the status display indicating if a node's
+        # children are partially applied (i.e. some applied, some not).
+        self.partial = False
+
+    def nextSibling(self):
+        numHunksInHeader = len(self.header.hunks)
+        indexOfThisHunk = self.header.hunks.index(self)
+
+        if (indexOfThisHunk < numHunksInHeader - 1):
+            nextHunk = self.header.hunks[indexOfThisHunk + 1]
+            return nextHunk
+        else:
+            return None
+
+    def prevSibling(self):
+        indexOfThisHunk = self.header.hunks.index(self)
+        if indexOfThisHunk > 0:
+            previousHunk = self.header.hunks[indexOfThisHunk - 1]
+            return previousHunk
+        else:
+            return None
+
+    def parentItem(self):
+        "Return the parent to the current item"
+        return self.header
+
+    def firstChild(self):
+        "Return the first child of this item, if one exists.  Otherwise None."
+        if len(self.changedLines) > 0:
+            return self.changedLines[0]
+        else:
+            return None
+
+    def lastChild(self):
+        "Return the last child of this item, if one exists.  Otherwise None."
+        if len(self.changedLines) > 0:
+            return self.changedLines[-1]
+        else:
+            return None
+
+    def allChildren(self):
+        "Return a list of all of the direct children of this node"
+        return self.changedLines
+    def countchanges(self):
+        """changedLines -> (n+,n-)"""
+        add = len([l for l in self.changedLines if l.applied and l.prettyStr()[0] == '+'])
+        rem = len([l for l in self.changedLines if l.applied and l.prettyStr()[0] == '-'])
+        return add, rem
+
+    def getFromToLine(self):
+        # calculate the number of removed lines converted to context lines
+        removedConvertedToContext = self.originalremoved - self.removed
+        delta = len(self.before) + len(self.after) + removedConvertedToContext
+        if self.after and self.after[-1] == '\\ No newline at end of file\n':
+            delta -= 1
+        fromlen = delta + self.removed
+        tolen = delta + self.added
+        fromToLine = '@@ -%d,%d +%d,%d @@%s\n' % \
+                 (self.fromline, fromlen, self.toline, tolen,
+                  self.proc and (' ' + self.proc))
+        return fromToLine
+
+    def write(self, fp):
+        # updated self.added/removed, which are used by getFromToLine()
+        self.added, self.removed = self.countchanges()
+        fp.write(self.getFromToLine())
+
+        hunkLineList = []
+        # add the following to the list: (1) all applied lines, and
+        # (2) all unapplied removal lines (convert these to context lines)
+        for changedLine in self.changedLines:
+            changedLineStr = changedLine.prettyStr()
+            if changedLine.applied:
+                hunkLineList.append(changedLineStr)
+            elif changedLineStr[0] == "-":
+                hunkLineList.append(" " + changedLineStr[1:])
+
+        fp.write(''.join(self.before + hunkLineList + self.after))
+
+    pretty = write
+
+    def filename(self):
+        return self.header.filename()
+
+    def prettyStr(self):
+        x = cStringIO.StringIO()
+        self.pretty(x)
+        return x.getvalue()
+
+    def __repr__(self):
+        return '<hunk %r@%d>' % (self.filename(), self.fromline)
+
+
+
+def parsepatch(changes, fp):
+    "Parse a patch, returning a list of header and hunk objects."
+    class parser(object):
+        """patch parsing state machine"""
+        def __init__(self):
+            self.fromline = 0
+            self.toline = 0
+            self.proc = ''
+            self.header = None
+            self.context = []
+            self.before = []
+            self.changedlines = []
+            self.stream = []
+            self.modified, self.added, self.removed = changes
+
+        def _range(self, (fromstart, fromend, tostart, toend, proc)):
+            "Store range line info to associated instance variables."
+            self.fromline = int(fromstart)
+            self.toline = int(tostart)
+            self.proc = proc
+
+        def add_new_hunk(self):
+            """
+            Create a new complete hunk object, adding it to the latest header
+            and to self.stream.
+
+            Add all of the previously collected information about
+            the hunk to the new hunk object.  This information includes
+            header, from/to-lines, function (self.proc), preceding context
+            lines, changed lines, as well as the current context lines (which
+            follow the changed lines).
+
+            The size of the from/to lines are updated to be correct for the
+            next hunk we parse.
+
+            """
+            h = hunk(self.header, self.fromline, self.toline, self.proc,
+                     self.before, self.changedlines, self.context)
+            self.header.hunks.append(h)
+            self.stream.append(h)
+            self.fromline += len(self.before) + h.removed
+            self.toline += len(self.before) + h.added
+            self.before = []
+            self.changedlines = []
+            self.proc = ''
+
+        def _context(self, context):
+            """
+            Set the value of self.context.
+
+            Also, if an unprocessed set of changelines was previously
+            encountered, this is the condition for creating a complete
+            hunk object.  In this case, we create and add a new hunk object to
+            the most recent header object, and to self.strem. 
+
+            """
+            self.context = context
+            # if there have been changed lines encountered that haven't yet
+            # been add to a hunk.
+            if self.changedlines:
+                self.add_new_hunk()
+
+        def _changedlines(self, changedlines):
+            """
+            Store the changed lines in self.changedlines.
+
+            Mark any context lines in the context-line buffer (self.context) as
+            lines preceding the changed-lines (i.e. stored in self.before), and
+            clear the context-line buffer.
+
+            """
+            self.changedlines = changedlines
+            self.before = self.context
+            self.context = []
+
+        def add_new_header(self, hdr):
+            """
+            Create a header object containing the header lines, and the
+            filename the header applies to.  Add the header to self.stream.
+
+            """
+            # if there are any lines in the unchanged-lines buffer, create a 
+            # new hunk using them, and add it to the last header.
+            if self.changedlines:
+                self.add_new_hunk()
+
+            # create a new header and add it to self.stream
+            self.header = header(hdr)
+            fileName = self.header.filename()
+            if fileName in self.modified:
+                self.header.changetype = "M"
+            elif fileName in self.added:
+                self.header.changetype = "A"
+            elif fileName in self.removed:
+                self.header.changetype = "R"
+            self.stream.append(self.header)
+
+        def finished(self):
+            # if there are any lines in the unchanged-lines buffer, create a 
+            # new hunk using them, and add it to the last header.
+            if self.changedlines:
+                self.add_new_hunk()
+
+            return self.stream
+
+        transitions = {
+            'file': {'context': _context,
+                     'file': add_new_header,
+                     'hunk': _changedlines,
+                     'range': _range},
+            'context': {'file': add_new_header,
+                        'hunk': _changedlines,
+                        'range': _range},
+            'hunk': {'context': _context,
+                     'file': add_new_header,
+                     'range': _range},
+            'range': {'context': _context,
+                      'hunk': _changedlines},
+            }
+
+    p = parser()
+
+    # run the state-machine
+    state = 'context'
+    for newstate, data in scanpatch(fp):
+        try:
+            p.transitions[state][newstate](p, data)
+        except KeyError:
+            raise patch.PatchError('unhandled transition: %s -> %s' %
+                                   (state, newstate))
+        state = newstate
+    return p.finished()
+
+def filterpatch(opts, chunks, chunk_selector):
+    """Interactively filter patch chunks into applied-only chunks"""
+    chunks = list(chunks)
+    # convert chunks list into structure suitable for displaying/modifying
+    # with curses.  Create a list of headers only.
+    headers = [c for c in chunks if isinstance(c, header)]
+
+    # if there are no changed files
+    if len(headers) == 0:
+        return []
+
+    # let user choose headers/hunks/lines, and mark their applied flags accordingly
+    chunk_selector(opts, headers)
+
+    appliedHunkList = []
+    for hdr in headers:
+        if hdr.applied and (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0):
+            appliedHunkList.append(hdr)
+            fixoffset = 0
+            for hnk in hdr.hunks:
+                if hnk.applied:
+                    appliedHunkList.append(hnk)
+                    # adjust the 'to'-line offset of the hunk to be correct
+                    # after de-activating some of the other hunks for this file
+                    if fixoffset:
+                        #hnk = copy.copy(hnk) # necessary??
+                        hnk.toline += fixoffset
+                else:
+                    fixoffset += hnk.removed - hnk.added
+
+    return appliedHunkList