def __init__(self, stdscr, lineObjs): curses.use_default_colors() self.stdscr = stdscr self.colorPrinter = ColorPrinter(self.stdscr) 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 self.simpleLines = [] self.lineMatches = [] # lets loop through and split for key, lineObj in self.lineObjs.items(): lineObj.setController(self) if (lineObj.isSimple()): self.simpleLines.append(lineObj) else: self.lineMatches.append(lineObj) self.numLines = len(lineObjs.keys()) self.numMatches = len(self.lineMatches) 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 __init__(self, flags, keyBindings, stdscr, lineObjs, cursesAPI): self.stdscr = stdscr self.cursesAPI = cursesAPI self.cursesAPI.useDefaultColors() self.colorPrinter = ColorPrinter(self.stdscr, cursesAPI) self.flags = flags self.keyBindings = keyBindings self.lineObjs = lineObjs self.hoverIndex = 0 self.scrollOffset = 0 self.scrollBar = ScrollBar(self.colorPrinter, lineObjs, self) self.helperChrome = HelperChrome(self.colorPrinter, self, flags) (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) # begin tracking dirty state self.resetDirty() if self.flags.args.all: self.toggleSelectAll() self.numLines = len(lineObjs.keys()) self.numMatches = len(self.lineMatches) 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')
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, flags) (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( ) or self.mode == X_MODE 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): paths = set() for line in self.lineMatches: if line.getPath() not in paths: paths.add(line.getPath()) line.toggleSelect() def setSelect(self, val): self.lineMatches[self.hoverIndex].setSelect(val) def describeFile(self): self.helperChrome.outputDescription(self.lineMatches[self.hoverIndex]) 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) # also clear the description pane if necessary self.helperChrome.clearDescriptionPane() 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 == 'x': self.toggleXMode() elif key == 'c': self.beginEnterCommand() elif key == ' ' or key == 'NPAGE': self.pageDown() elif key == 'b' or key == 'PPAGE': self.pageUp() elif key == 'g' or key == 'HOME': self.jumpToIndex(0) elif (key == 'G' and not self.mode == X_MODE) or key == 'END': self.jumpToIndex(self.numMatches - 1) elif key == 'd': self.describeFile() elif key == 'f': self.toggleSelect() elif key == 'F': self.toggleSelect() self.moveIndex(1) elif key == 'A' and not self.mode == X_MODE: self.toggleSelectAll() elif key == 'ENTER' and (not self.flags.getAllInput() or len(self.flags.getPresetCommand())): # it does not make sense to process an 'ENTER' keypress if we're in the allInput # mode and there is not a preset command. 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.getPathsToUse() self.cursesAPI.exit() elif self.mode == X_MODE and key in lbls: self.selectXMode(key) pass def getPathsToUse(self): # if we have selected paths, those, otherwise hovered toUse = self.getSelectedPaths() if not toUse: toUse = self.getHoveredPaths() # save the selection we are using if self.cursesAPI.allowFileOutput(): output.outputSelection(toUse) return toUse def getSelectedPaths(self): return [ lineObj for (index, lineObj) in enumerate(self.lineMatches) if lineObj.getSelected() ] def getHoveredPaths(self): return [ lineObj for (index, lineObj) in enumerate(self.lineMatches) if index == self.hoverIndex ] def showAndGetCommand(self): pathObjs = self.getPathsToUse() paths = [pathObj.getPath() for pathObj in pathObjs] (maxy, maxx) = self.getScreenDimensions() # Alright this is a bit tricy -- for tall screens, we try to aim # the command prompt right at the middle of the screen so you dont # have to shift your eyes down or up a bunch beginHeight = int(round(maxy / 2) - len(paths) / 2.0) # but if you have a TON of paths, we are going to start printing # way off screen. in this case lets just slap the prompt # at the bottom so we can fit as much as possible. # # There could better option here to slowly increase the prompt # height to the bottom, but this is good enough for now... if beginHeight <= 1: beginHeight = maxy - 6 borderLine = '=' * len(SHORT_COMMAND_PROMPT) promptLine = '.' * len(SHORT_COMMAND_PROMPT) # from helper chrome code maxPathLength = maxx - 5 if self.helperChrome.getIsSidebarMode(): # need to be shorter to not go into side bar maxPathLength = len(SHORT_COMMAND_PROMPT) + 18 # first lets print all the paths startHeight = beginHeight - 1 - len(paths) try: self.colorPrinter.addstr(startHeight - 3, 0, borderLine) self.colorPrinter.addstr(startHeight - 2, 0, SHORT_PATHS_HEADER) self.colorPrinter.addstr(startHeight - 1, 0, borderLine) except curses.error: pass for index, path in enumerate(paths): try: self.colorPrinter.addstr(startHeight + index, 0, path[0:maxPathLength]) except curses.error: pass # first print prompt try: self.colorPrinter.addstr(beginHeight, 0, SHORT_COMMAND_PROMPT) self.colorPrinter.addstr(beginHeight + 1, 0, SHORT_COMMAND_PROMPT2) except curses.error: pass # then line to distinguish and prompt line try: self.colorPrinter.addstr(beginHeight - 1, 0, borderLine) self.colorPrinter.addstr(beginHeight + 2, 0, borderLine) self.colorPrinter.addstr(beginHeight + 3, 0, promptLine) except curses.error: pass self.stdscr.refresh() self.cursesAPI.echo() maxX = int(round(maxx - 1)) command = self.stdscr.getstr(beginHeight + 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) (minX, minY, _, maxY) = self.getChromeBoundaries() yStart = (maxY + minY) / 2 - 3 self.printProvidedCommandWarning(yStart, minX) 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.getPathsToUse() output.execComposedCommand(command, lineObjs) sys.exit(0) def onEnter(self): lineObjs = self.getPathsToUse() if not lineObjs: # nothing selected, assume we want hovered lineObjs = self.getHoveredPaths() 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 and 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.printXMode() 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, xStart): self.colorPrinter.addstr( yStart, xStart, 'Oh no! You already provided a command so ' + 'you cannot enter command mode.', self.colorPrinter.getAttributes(curses.COLOR_WHITE, curses.COLOR_RED, 0)) self.colorPrinter.addstr( yStart + 1, xStart, 'The command you provided was "%s" ' % self.flags.getPresetCommand()) self.colorPrinter.addstr( yStart + 2, xStart, 'Press any key to go back to selecting paths.') 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, '') def toggleXMode(self): self.mode = X_MODE if self.mode != X_MODE else SELECT_MODE self.printAll() def printXMode(self): if self.mode == X_MODE: (maxy, _) = self.scrollBar.screenControl.getScreenDimensions() topY = maxy - 2 minY = self.scrollBar.getMinY() - 1 for i in range(minY, topY + 1): idx = i - minY if idx < len(lbls): self.colorPrinter.addstr(i, 1, lbls[idx]) def selectXMode(self, key): lineObj = self.lineObjs[lbls.index(key) - self.scrollOffset] if type(lineObj) == format.LineMatch: lineMatchIndex = self.lineMatches.index(lineObj) self.hoverIndex = lineMatchIndex self.toggleSelect()
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, '')
class Controller(object): def __init__(self, flags, keyBindings, stdscr, lineObjs, cursesAPI): self.stdscr = stdscr self.cursesAPI = cursesAPI self.cursesAPI.useDefaultColors() self.colorPrinter = ColorPrinter(self.stdscr, cursesAPI) self.flags = flags self.keyBindings = keyBindings self.lineObjs = lineObjs self.hoverIndex = 0 self.scrollOffset = 0 self.scrollBar = ScrollBar(self.colorPrinter, lineObjs, self) self.helperChrome = HelperChrome(self.colorPrinter, self, flags) (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) # begin tracking dirty state self.resetDirty() if self.flags.args.all: self.toggleSelectAll() self.numLines = len(lineObjs.keys()) self.numMatches = len(self.lineMatches) 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( ) or self.mode == X_MODE 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): paths = set() for line in self.lineMatches: if line.getPath() not in paths: paths.add(line.getPath()) line.toggleSelect() def setSelect(self, val): self.lineMatches[self.hoverIndex].setSelect(val) def describeFile(self): self.helperChrome.outputDescription(self.lineMatches[self.hoverIndex]) def control(self): executeKeys = self.flags.getExecuteKeys() # we start out by printing everything we need to self.printAll() self.resetDirty() self.moveCursor() while True: if len(executeKeys) > 0: inKey = executeKeys.pop(0) else: 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.updateScrollOffset() 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) # also clear the description pane if necessary self.helperChrome.clearDescriptionPane() 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 == 'x': self.toggleXMode() elif key == 'c': self.beginEnterCommand() elif key == ' ' or key == 'NPAGE': self.pageDown() elif key == 'b' or key == 'PPAGE': self.pageUp() elif key == 'g' or key == 'HOME': self.jumpToIndex(0) elif (key == 'G' and not self.mode == X_MODE) or key == 'END': self.jumpToIndex(self.numMatches - 1) elif key == 'd': self.describeFile() elif key == 'f': self.toggleSelect() elif key == 'F': self.toggleSelect() self.moveIndex(1) elif key == 'A' and not self.mode == X_MODE: self.toggleSelectAll() elif key == 'ENTER' and (not self.flags.getAllInput() or len(self.flags.getPresetCommand())): # it does not make sense to process an 'ENTER' keypress if we're in the allInput # mode and there is not a preset command. 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.getPathsToUse() self.cursesAPI.exit() elif self.mode == X_MODE and key in lbls: self.selectXMode(key) for boundKey, command in self.keyBindings.bindings: if key == boundKey: self.executePreconfiguredCommand(command) def getPathsToUse(self): # if we have selected paths, those, otherwise hovered toUse = self.getSelectedPaths() if not toUse: toUse = self.getHoveredPaths() # save the selection we are using if self.cursesAPI.allowFileOutput(): output.outputSelection(toUse) return toUse def getSelectedPaths(self): return [lineObj for (index, lineObj) in enumerate(self.lineMatches) if lineObj.getSelected()] def getHoveredPaths(self): return [lineObj for (index, lineObj) in enumerate(self.lineMatches) if index == self.hoverIndex] def showAndGetCommand(self): pathObjs = self.getPathsToUse() paths = [pathObj.getPath() for pathObj in pathObjs] (maxy, maxx) = self.getScreenDimensions() # Alright this is a bit tricy -- for tall screens, we try to aim # the command prompt right at the middle of the screen so you dont # have to shift your eyes down or up a bunch beginHeight = int(round(maxy / 2) - len(paths) / 2.0) # but if you have a TON of paths, we are going to start printing # way off screen. in this case lets just slap the prompt # at the bottom so we can fit as much as possible. # # There could better option here to slowly increase the prompt # height to the bottom, but this is good enough for now... if beginHeight <= 1: beginHeight = maxy - 6 borderLine = '=' * len(SHORT_COMMAND_PROMPT) promptLine = '.' * len(SHORT_COMMAND_PROMPT) # from helper chrome code maxPathLength = maxx - 5 if self.helperChrome.getIsSidebarMode(): # need to be shorter to not go into side bar maxPathLength = len(SHORT_COMMAND_PROMPT) + 18 # first lets print all the paths startHeight = beginHeight - 1 - len(paths) try: self.colorPrinter.addstr(startHeight - 3, 0, borderLine) self.colorPrinter.addstr(startHeight - 2, 0, SHORT_PATHS_HEADER) self.colorPrinter.addstr(startHeight - 1, 0, borderLine) except curses.error: pass for index, path in enumerate(paths): try: self.colorPrinter.addstr( startHeight + index, 0, path[0:maxPathLength] ) except curses.error: pass # first print prompt try: self.colorPrinter.addstr(beginHeight, 0, SHORT_COMMAND_PROMPT) self.colorPrinter.addstr(beginHeight + 1, 0, SHORT_COMMAND_PROMPT2) except curses.error: pass # then line to distinguish and prompt line try: self.colorPrinter.addstr(beginHeight - 1, 0, borderLine) self.colorPrinter.addstr(beginHeight + 2, 0, borderLine) self.colorPrinter.addstr(beginHeight + 3, 0, promptLine) except curses.error: pass self.stdscr.refresh() self.cursesAPI.echo() maxX = int(round(maxx - 1)) command = self.stdscr.getstr(beginHeight + 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) (minX, minY, _, maxY) = self.getChromeBoundaries() yStart = (maxY + minY) / 2 - 3 self.printProvidedCommandWarning(yStart, minX) 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.getPathsToUse() output.execComposedCommand(command, lineObjs) sys.exit(0) def executePreconfiguredCommand(self, command): lineObjs = self.getPathsToUse() output.execComposedCommand(command, lineObjs) sys.exit(0) def onEnter(self): lineObjs = self.getPathsToUse() if not lineObjs: # nothing selected, assume we want hovered lineObjs = self.getHoveredPaths() 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 and 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.printXMode() 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, xStart): self.colorPrinter.addstr(yStart, xStart, 'Oh no! You already provided a command so ' + 'you cannot enter command mode.', self.colorPrinter.getAttributes(curses.COLOR_WHITE, curses.COLOR_RED, 0)) self.colorPrinter.addstr( yStart + 1, xStart, 'The command you provided was "%s" ' % self.flags.getPresetCommand()) self.colorPrinter.addstr( yStart + 2, xStart, 'Press any key to go back to selecting paths.') 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, '') def toggleXMode(self): self.mode = X_MODE if self.mode != X_MODE else SELECT_MODE self.printAll() def printXMode(self): if self.mode == X_MODE: (maxy, _) = self.scrollBar.screenControl.getScreenDimensions() topY = maxy - 2 minY = self.scrollBar.getMinY() - 1 for i in range(minY, topY + 1): idx = i - minY if idx < len(lbls): self.colorPrinter.addstr(i, 1, lbls[idx]) def selectXMode(self, key): if (lbls.index(key) >= len(self.lineObjs)): return lineObj = self.lineObjs[ lbls.index(key) - self.scrollOffset] if type(lineObj) == format.LineMatch: lineMatchIndex = self.lineMatches.index(lineObj) self.hoverIndex = lineMatchIndex self.toggleSelect()