Esempio n. 1
0
class Controller(object):

    def __init__(self, flags, stdscr, lineObjs, cursesAPI):
        self.stdscr = stdscr
        self.cursesAPI = cursesAPI
        self.cursesAPI.useDefaultColors()
        self.colorPrinter = ColorPrinter(self.stdscr, cursesAPI)
        self.flags = flags

        self.lineObjs = lineObjs
        self.hoverIndex = 0
        self.scrollOffset = 0
        self.scrollBar = ScrollBar(self.colorPrinter, lineObjs, self)
        self.helperChrome = HelperChrome(self.colorPrinter, self)
        (self.oldmaxy, self.oldmaxx) = self.getScreenDimensions()
        self.mode = SELECT_MODE

        # lets loop through and split
        self.lineMatches = []

        for lineObj in self.lineObjs.values():
            lineObj.controller = self
            if not lineObj.isSimple():
                self.lineMatches.append(lineObj)

        self.numLines = len(lineObjs.keys())
        self.numMatches = len(self.lineMatches)

        # begin tracking dirty state
        self.resetDirty()

        self.setHover(self.hoverIndex, True)

        # the scroll offset might not start off
        # at 0 if our first real match is WAY
        # down the screen -- so lets init it to
        # a valid value after we have all our line objects
        self.updateScrollOffset()

        logger.addEvent('init')

    def getScrollOffset(self):
        return self.scrollOffset

    def getScreenDimensions(self):
        return self.stdscr.getmaxyx()

    def getChromeBoundaries(self):
        (maxy, maxx) = self.stdscr.getmaxyx()
        minx = CHROME_MIN_X if self.scrollBar.getIsActivated() else 0
        maxy = self.helperChrome.reduceMaxY(maxy)
        maxx = self.helperChrome.reduceMaxX(maxx)
        # format of (MINX, MINY, MAXX, MAXY)
        return (minx, CHROME_MIN_Y, maxx, maxy)

    def getViewportHeight(self):
        (minx, miny, maxx, maxy) = self.getChromeBoundaries()
        return maxy - miny

    def setHover(self, index, val):
        self.lineMatches[index].setHover(val)

    def toggleSelect(self):
        self.lineMatches[self.hoverIndex].toggleSelect()

    def toggleSelectAll(self):
        files = set()
        for line in self.lineMatches:
            if line.getFile() not in files:
                files.add(line.getFile())
                line.toggleSelect()

    def setSelect(self, val):
        self.lineMatches[self.hoverIndex].setSelect(val)

    def control(self):
        # we start out by printing everything we need to
        self.printAll()
        self.resetDirty()
        self.moveCursor()
        while True:
            inKey = self.getKey()
            self.checkResize()
            self.processInput(inKey)
            self.processDirty()
            self.resetDirty()
            self.moveCursor()
            self.stdscr.refresh()

    def checkResize(self):
        (maxy, maxx) = self.getScreenDimensions()
        if (maxy is not self.oldmaxy or maxx is not self.oldmaxx):
            # we resized so print all!
            self.printAll()
            self.resetDirty()
            self.stdscr.refresh()
            logger.addEvent('resize')
        (self.oldmaxy, self.oldmaxx) = self.getScreenDimensions()

    def updateScrollOffset(self):
        """
          yay scrolling logic! we will start simple here
          and basically just center the viewport to current
          matched line
      """
        windowHeight = self.getViewportHeight()
        halfHeight = int(round(windowHeight / 2.0))

        # important, we need to get the real SCREEN position
        # of the hover index, not its index within our matches
        hovered = self.lineMatches[self.hoverIndex]
        desiredTopRow = hovered.getScreenIndex() - halfHeight

        oldOffset = self.scrollOffset
        desiredTopRow = max(desiredTopRow, 0)
        newOffset = -desiredTopRow
        # lets add in some leeway -- dont bother repositioning
        # if the old offset is within 1/2 of the window height
        # of our desired (unless we absolutely have to)
        if abs(newOffset -
               oldOffset) > halfHeight / 2 or self.hoverIndex + oldOffset < 0:
            # need to reassign now we have gone too far
            self.scrollOffset = newOffset
        if oldOffset is not self.scrollOffset:
            self.dirtyAll()

        # also update our scroll bar
        self.scrollBar.calcBoxFractions()

    def pageDown(self):
        pageHeight = (int)(self.getViewportHeight() * 0.5)
        self.moveIndex(pageHeight)

    def pageUp(self):
        pageHeight = (int)(self.getViewportHeight() * 0.5)
        self.moveIndex(-pageHeight)

    def moveIndex(self, delta):
        newIndex = (self.hoverIndex + delta) % self.numMatches
        self.jumpToIndex(newIndex)

    def jumpToIndex(self, newIndex):
        self.setHover(self.hoverIndex, False)
        self.hoverIndex = newIndex
        self.setHover(self.hoverIndex, True)
        self.updateScrollOffset()

    def processInput(self, key):
        if key == 'UP' or key == 'k':
            self.moveIndex(-1)
        elif key == 'DOWN' or key == 'j':
            self.moveIndex(1)
        elif key == 'c':
            self.beginEnterCommand()
        elif key == ' ' or key == 'PAGE_DOWN':
            self.pageDown()
        elif key == 'b' or key == 'PAGE_UP':
            self.pageUp()
        elif key == 'g':
            self.jumpToIndex(0)
        elif key == 'G':
            self.jumpToIndex(self.numMatches - 1)
        elif key == 'f':
            self.toggleSelect()
        elif key == 'A':
            self.toggleSelectAll()
        elif key == 'ENTER':
            self.onEnter()
        elif key == 'q':
            output.outputNothing()
            # this will get the appropriate selection and save it to a file for reuse
            # before exiting the program
            self.getFilesToUse()
            self.cursesAPI.exit()
        pass

    def getFilesToUse(self):
        # if we have select files, those, otherwise hovered
        toUse = self.getSelectedFiles()
        if not toUse:
            toUse = self.getHoveredFiles()

        # save the selection we are using
        if self.cursesAPI.allowFileOutput():
            output.outputSelection(toUse)
        return toUse

    def getSelectedFiles(self):
        return [lineObj for (index, lineObj) in enumerate(self.lineMatches)
                if lineObj.getSelected()]

    def getHoveredFiles(self):
        return [lineObj for (index, lineObj) in enumerate(self.lineMatches)
                if index == self.hoverIndex]

    def showAndGetCommand(self):
        fileObjs = self.getFilesToUse()
        files = [fileObj.getFile() for fileObj in fileObjs]
        (maxy, maxx) = self.getScreenDimensions()
        halfHeight = int(round(maxy / 2) - len(files) / 2.0)

        borderLine = '=' * len(SHORT_COMMAND_PROMPT)
        promptLine = '.' * len(SHORT_COMMAND_PROMPT)
        # from helper chrome code
        maxFileLength = maxx - 5
        if self.helperChrome.getIsSidebarMode():
            # need to be shorter to not go into side bar
            maxFileLength = len(SHORT_COMMAND_PROMPT) + 18

        # first lets print all the files
        startHeight = halfHeight - 1 - len(files)
        try:
            self.stdscr.addstr(startHeight - 3, 0, borderLine)
            self.stdscr.addstr(startHeight - 2, 0, SHORT_FILES_HEADER)
            self.stdscr.addstr(startHeight - 1, 0, borderLine)
            for index, file in enumerate(files):
                self.stdscr.addstr(startHeight + index, 0,
                                   file[0:maxFileLength])
        except curses.error:
            pass

        # first print prompt
        try:
            self.stdscr.addstr(halfHeight, 0, SHORT_COMMAND_PROMPT)
            self.stdscr.addstr(halfHeight + 1, 0, SHORT_COMMAND_PROMPT2)
        except curses.error:
            pass
        # then line to distinguish and prompt line
        try:
            self.stdscr.addstr(halfHeight - 1, 0, borderLine)
            self.stdscr.addstr(halfHeight + 2, 0, borderLine)
            self.stdscr.addstr(halfHeight + 3, 0, promptLine)
        except curses.error:
            pass

        self.stdscr.refresh()
        self.cursesAPI.echo()
        maxX = int(round(maxx - 1))

        command = self.stdscr.getstr(halfHeight + 3, 0, maxX)
        return command

    def beginEnterCommand(self):
        self.stdscr.erase()
        # first check if they are trying to enter command mode
        # but already have a command...
        if len(self.flags.getPresetCommand()):
            self.helperChrome.output(self.mode)
            (_, minY, _, maxY) = self.getChromeBoundaries()
            yStart = (maxY + minY) / 2 - 3
            self.printProvidedCommandWarning(yStart)
            self.stdscr.refresh()
            self.getKey()
            self.mode = SELECT_MODE
            self.dirtyAll()
            return

        self.mode = COMMAND_MODE
        self.helperChrome.output(self.mode)
        logger.addEvent('enter_command_mode')

        command = self.showAndGetCommand()
        if len(command) == 0:
            # go back to selection mode and repaint
            self.mode = SELECT_MODE
            self.cursesAPI.noecho()
            self.dirtyAll()
            logger.addEvent('exit_command_mode')
            return
        lineObjs = self.getFilesToUse()
        output.execComposedCommand(command, lineObjs)
        sys.exit(0)

    def onEnter(self):
        lineObjs = self.getFilesToUse()
        if not lineObjs:
            # nothing selected, assume we want hovered
            lineObjs = self.getHoveredFiles()
        logger.addEvent('selected_num_files', len(lineObjs))

        # commands passed from the command line get used immediately
        presetCommand = self.flags.getPresetCommand()
        if len(presetCommand) > 0:
            output.execComposedCommand(presetCommand, lineObjs)
        else:
            output.editFiles(lineObjs)

        sys.exit(0)

    def resetDirty(self):
        # reset all dirty state for our components
        self.dirty = False
        self.dirtyIndexes = []

    def dirtyLine(self, index):
        self.dirtyIndexes.append(index)

    def dirtyAll(self):
        self.dirty = True

    def processDirty(self):
        if self.dirty:
            self.printAll()
            return
        (minx, miny, maxx, maxy) = self.getChromeBoundaries()
        didClearLine = False
        for index in self.dirtyIndexes:
            y = miny + index + self.getScrollOffset()
            if y >= miny or y < maxy:
                didClearLine = True
                self.clearLine(y)
                self.lineObjs[index].output(self.colorPrinter)
        if didClearLine and self.helperChrome.getIsSidebarMode():
            # now we need to output the chrome again since on wide
            # monitors we will have cleared out a line of the chrome
            self.helperChrome.output(self.mode)

    def clearLine(self, y):
        '''Clear a line of content, excluding the chrome'''
        (minx, _, _, _) = self.getChromeBoundaries()
        (_, maxx) = self.stdscr.getmaxyx()
        charsToDelete = range(minx, maxx)
        # we go in the **reverse** order since the original documentation
        # of delchar (http://dell9.ma.utexas.edu/cgi-bin/man-cgi?delch+3)
        # mentions that delchar actually moves all the characters to the right
        # of the cursor
        for x in reversed(charsToDelete):
            self.stdscr.delch(y, x)

    def printAll(self):
        self.stdscr.erase()
        self.printLines()
        self.printScroll()
        self.printChrome()

    def printLines(self):
        for lineObj in self.lineObjs.values():
            lineObj.output(self.colorPrinter)

    def printScroll(self):
        self.scrollBar.output()

    def printProvidedCommandWarning(self, yStart):
        self.colorPrinter.setAttributes(
            curses.COLOR_WHITE, curses.COLOR_RED, 0)
        self.stdscr.addstr(yStart, 0, 'Oh no! You already provided a command so ' +
                           'you cannot enter command mode.')
        self.stdscr.attrset(0)
        self.stdscr.addstr(
            yStart + 1, 0, 'The command you provided was "%s" ' % self.flags.getPresetCommand())
        self.stdscr.addstr(
            yStart + 2, 0, 'Press any key to go back to selecting files.')

    def printChrome(self):
        self.helperChrome.output(self.mode)

    def moveCursor(self):
        x = CHROME_MIN_X if self.scrollBar.getIsActivated() else 0
        y = self.lineMatches[
            self.hoverIndex].getScreenIndex() + self.scrollOffset
        self.stdscr.move(y, x)

    def getKey(self):
        charCode = self.stdscr.getch()
        return CODE_TO_CHAR.get(charCode, '')
Esempio n. 2
0
class Controller(object):
    def __init__(self, flags, stdscr, lineObjs, cursesAPI):
        self.stdscr = stdscr
        self.cursesAPI = cursesAPI
        self.cursesAPI.useDefaultColors()
        self.colorPrinter = ColorPrinter(self.stdscr, cursesAPI)
        self.flags = flags

        self.lineObjs = lineObjs
        self.hoverIndex = 0
        self.scrollOffset = 0
        self.scrollBar = ScrollBar(self.colorPrinter, lineObjs, self)
        self.helperChrome = HelperChrome(self.colorPrinter, self)
        (self.oldmaxy, self.oldmaxx) = self.getScreenDimensions()
        self.mode = SELECT_MODE

        # lets loop through and split
        self.lineMatches = []

        for lineObj in self.lineObjs.values():
            lineObj.controller = self
            if not lineObj.isSimple():
                self.lineMatches.append(lineObj)

        self.numLines = len(lineObjs.keys())
        self.numMatches = len(self.lineMatches)

        # begin tracking dirty state
        self.resetDirty()

        self.setHover(self.hoverIndex, True)

        # the scroll offset might not start off
        # at 0 if our first real match is WAY
        # down the screen -- so lets init it to
        # a valid value after we have all our line objects
        self.updateScrollOffset()

        logger.addEvent('init')

    def getScrollOffset(self):
        return self.scrollOffset

    def getScreenDimensions(self):
        return self.stdscr.getmaxyx()

    def getChromeBoundaries(self):
        (maxy, maxx) = self.stdscr.getmaxyx()
        minx = CHROME_MIN_X if self.scrollBar.getIsActivated() else 0
        maxy = self.helperChrome.reduceMaxY(maxy)
        maxx = self.helperChrome.reduceMaxX(maxx)
        # format of (MINX, MINY, MAXX, MAXY)
        return (minx, CHROME_MIN_Y, maxx, maxy)

    def getViewportHeight(self):
        (minx, miny, maxx, maxy) = self.getChromeBoundaries()
        return maxy - miny

    def setHover(self, index, val):
        self.lineMatches[index].setHover(val)

    def toggleSelect(self):
        self.lineMatches[self.hoverIndex].toggleSelect()

    def toggleSelectAll(self):
        files = set()
        for line in self.lineMatches:
            if line.getFile() not in files:
                files.add(line.getFile())
                line.toggleSelect()

    def setSelect(self, val):
        self.lineMatches[self.hoverIndex].setSelect(val)

    def control(self):
        # we start out by printing everything we need to
        self.printAll()
        self.resetDirty()
        self.moveCursor()
        while True:
            inKey = self.getKey()
            self.checkResize()
            self.processInput(inKey)
            self.processDirty()
            self.resetDirty()
            self.moveCursor()
            self.stdscr.refresh()

    def checkResize(self):
        (maxy, maxx) = self.getScreenDimensions()
        if (maxy is not self.oldmaxy or maxx is not self.oldmaxx):
            # we resized so print all!
            self.printAll()
            self.resetDirty()
            self.stdscr.refresh()
            logger.addEvent('resize')
        (self.oldmaxy, self.oldmaxx) = self.getScreenDimensions()

    def updateScrollOffset(self):
        """
          yay scrolling logic! we will start simple here
          and basically just center the viewport to current
          matched line
      """
        windowHeight = self.getViewportHeight()
        halfHeight = int(round(windowHeight / 2.0))

        # important, we need to get the real SCREEN position
        # of the hover index, not its index within our matches
        hovered = self.lineMatches[self.hoverIndex]
        desiredTopRow = hovered.getScreenIndex() - halfHeight

        oldOffset = self.scrollOffset
        desiredTopRow = max(desiredTopRow, 0)
        newOffset = -desiredTopRow
        # lets add in some leeway -- dont bother repositioning
        # if the old offset is within 1/2 of the window height
        # of our desired (unless we absolutely have to)
        if abs(newOffset -
               oldOffset) > halfHeight / 2 or self.hoverIndex + oldOffset < 0:
            # need to reassign now we have gone too far
            self.scrollOffset = newOffset
        if oldOffset is not self.scrollOffset:
            self.dirtyAll()

        # also update our scroll bar
        self.scrollBar.calcBoxFractions()

    def pageDown(self):
        pageHeight = (int)(self.getViewportHeight() * 0.5)
        self.moveIndex(pageHeight)

    def pageUp(self):
        pageHeight = (int)(self.getViewportHeight() * 0.5)
        self.moveIndex(-pageHeight)

    def moveIndex(self, delta):
        newIndex = (self.hoverIndex + delta) % self.numMatches
        self.jumpToIndex(newIndex)

    def jumpToIndex(self, newIndex):
        self.setHover(self.hoverIndex, False)
        self.hoverIndex = newIndex
        self.setHover(self.hoverIndex, True)
        self.updateScrollOffset()

    def processInput(self, key):
        if key == 'UP' or key == 'k':
            self.moveIndex(-1)
        elif key == 'DOWN' or key == 'j':
            self.moveIndex(1)
        elif key == 'c':
            self.beginEnterCommand()
        elif key == ' ' or key == 'PAGE_DOWN':
            self.pageDown()
        elif key == 'b' or key == 'PAGE_UP':
            self.pageUp()
        elif key == 'g':
            self.jumpToIndex(0)
        elif key == 'G':
            self.jumpToIndex(self.numMatches - 1)
        elif key == 'f':
            self.toggleSelect()
        elif key == 'A':
            self.toggleSelectAll()
        elif key == 'ENTER':
            self.onEnter()
        elif key == 'q':
            output.outputNothing()
            # this will get the appropriate selection and save it to a file for reuse
            # before exiting the program
            self.getFilesToUse()
            self.cursesAPI.exit()
        pass

    def getFilesToUse(self):
        # if we have select files, those, otherwise hovered
        toUse = self.getSelectedFiles()
        if not toUse:
            toUse = self.getHoveredFiles()

        # save the selection we are using
        if self.cursesAPI.allowFileOutput():
            output.outputSelection(toUse)
        return toUse

    def getSelectedFiles(self):
        return [
            lineObj for (index, lineObj) in enumerate(self.lineMatches)
            if lineObj.getSelected()
        ]

    def getHoveredFiles(self):
        return [
            lineObj for (index, lineObj) in enumerate(self.lineMatches)
            if index == self.hoverIndex
        ]

    def showAndGetCommand(self):
        fileObjs = self.getFilesToUse()
        files = [fileObj.getFile() for fileObj in fileObjs]
        (maxy, maxx) = self.getScreenDimensions()
        halfHeight = int(round(maxy / 2) - len(files) / 2.0)

        borderLine = '=' * len(SHORT_COMMAND_PROMPT)
        promptLine = '.' * len(SHORT_COMMAND_PROMPT)
        # from helper chrome code
        maxFileLength = maxx - 5
        if self.helperChrome.getIsSidebarMode():
            # need to be shorter to not go into side bar
            maxFileLength = len(SHORT_COMMAND_PROMPT) + 18

        # first lets print all the files
        startHeight = halfHeight - 1 - len(files)
        try:
            self.colorPrinter.addstr(startHeight - 3, 0, borderLine)
            self.colorPrinter.addstr(startHeight - 2, 0, SHORT_FILES_HEADER)
            self.colorPrinter.addstr(startHeight - 1, 0, borderLine)
            for index, file in enumerate(files):
                self.colorPrinter.addstr(startHeight + index, 0,
                                         file[0:maxFileLength])
        except curses.error:
            pass

        # first print prompt
        try:
            self.colorPrinter.addstr(halfHeight, 0, SHORT_COMMAND_PROMPT)
            self.colorPrinter.addstr(halfHeight + 1, 0, SHORT_COMMAND_PROMPT2)
        except curses.error:
            pass
        # then line to distinguish and prompt line
        try:
            self.colorPrinter.addstr(halfHeight - 1, 0, borderLine)
            self.colorPrinter.addstr(halfHeight + 2, 0, borderLine)
            self.colorPrinter.addstr(halfHeight + 3, 0, promptLine)
        except curses.error:
            pass

        self.stdscr.refresh()
        self.cursesAPI.echo()
        maxX = int(round(maxx - 1))

        command = self.stdscr.getstr(halfHeight + 3, 0, maxX)
        return command

    def beginEnterCommand(self):
        self.stdscr.erase()
        # first check if they are trying to enter command mode
        # but already have a command...
        if len(self.flags.getPresetCommand()):
            self.helperChrome.output(self.mode)
            (_, minY, _, maxY) = self.getChromeBoundaries()
            yStart = (maxY + minY) / 2 - 3
            self.printProvidedCommandWarning(yStart)
            self.stdscr.refresh()
            self.getKey()
            self.mode = SELECT_MODE
            self.dirtyAll()
            return

        self.mode = COMMAND_MODE
        self.helperChrome.output(self.mode)
        logger.addEvent('enter_command_mode')

        command = self.showAndGetCommand()
        if len(command) == 0:
            # go back to selection mode and repaint
            self.mode = SELECT_MODE
            self.cursesAPI.noecho()
            self.dirtyAll()
            logger.addEvent('exit_command_mode')
            return
        lineObjs = self.getFilesToUse()
        output.execComposedCommand(command, lineObjs)
        sys.exit(0)

    def onEnter(self):
        lineObjs = self.getFilesToUse()
        if not lineObjs:
            # nothing selected, assume we want hovered
            lineObjs = self.getHoveredFiles()
        logger.addEvent('selected_num_files', len(lineObjs))

        # commands passed from the command line get used immediately
        presetCommand = self.flags.getPresetCommand()
        if len(presetCommand) > 0:
            output.execComposedCommand(presetCommand, lineObjs)
        else:
            output.editFiles(lineObjs)

        sys.exit(0)

    def resetDirty(self):
        # reset all dirty state for our components
        self.dirty = False
        self.dirtyIndexes = []

    def dirtyLine(self, index):
        self.dirtyIndexes.append(index)

    def dirtyAll(self):
        self.dirty = True

    def processDirty(self):
        if self.dirty:
            self.printAll()
            return
        (minx, miny, maxx, maxy) = self.getChromeBoundaries()
        didClearLine = False
        for index in self.dirtyIndexes:
            y = miny + index + self.getScrollOffset()
            if y >= miny or y < maxy:
                didClearLine = True
                self.clearLine(y)
                self.lineObjs[index].output(self.colorPrinter)
        if didClearLine and self.helperChrome.getIsSidebarMode():
            # now we need to output the chrome again since on wide
            # monitors we will have cleared out a line of the chrome
            self.helperChrome.output(self.mode)

    def clearLine(self, y):
        '''Clear a line of content, excluding the chrome'''
        (minx, _, _, _) = self.getChromeBoundaries()
        (_, maxx) = self.stdscr.getmaxyx()
        charsToDelete = range(minx, maxx)
        # we go in the **reverse** order since the original documentation
        # of delchar (http://dell9.ma.utexas.edu/cgi-bin/man-cgi?delch+3)
        # mentions that delchar actually moves all the characters to the right
        # of the cursor
        for x in reversed(charsToDelete):
            self.stdscr.delch(y, x)

    def printAll(self):
        self.stdscr.erase()
        self.printLines()
        self.printScroll()
        self.printChrome()

    def printLines(self):
        for lineObj in self.lineObjs.values():
            lineObj.output(self.colorPrinter)

    def printScroll(self):
        self.scrollBar.output()

    def printProvidedCommandWarning(self, yStart):
        self.colorPrinter.setAttributes(curses.COLOR_WHITE, curses.COLOR_RED,
                                        0)
        self.stdscr.addstr(
            yStart, 0, 'Oh no! You already provided a command so ' +
            'you cannot enter command mode.')
        self.stdscr.attrset(0)
        self.stdscr.addstr(
            yStart + 1, 0, 'The command you provided was "%s" ' %
            self.flags.getPresetCommand())
        self.stdscr.addstr(yStart + 2, 0,
                           'Press any key to go back to selecting files.')

    def printChrome(self):
        self.helperChrome.output(self.mode)

    def moveCursor(self):
        x = CHROME_MIN_X if self.scrollBar.getIsActivated() else 0
        y = self.lineMatches[
            self.hoverIndex].getScreenIndex() + self.scrollOffset
        self.stdscr.move(y, x)

    def getKey(self):
        charCode = self.stdscr.getch()
        return CODE_TO_CHAR.get(charCode, '')