diff --git a/examples/reference-gertty.yaml b/examples/reference-gertty.yaml index 6f79c6d..5a33fdd 100644 --- a/examples/reference-gertty.yaml +++ b/examples/reference-gertty.yaml @@ -99,6 +99,10 @@ keymaps: review: ['r', 'R'] - name: osx #OS X blocks ctrl+o change-search: 'ctrl s' +# To specify a sequence of keys, they must be a list of keystrokes +# within a list of key series. For example: + - name: vi + quit: [[':', 'q']] # The default keymap may be selected with the '-k KEYMAP' command line # option, or with the following line: diff --git a/gertty/app.py b/gertty/app.py index 0e6d196..4b74dff 100644 --- a/gertty/app.py +++ b/gertty/app.py @@ -77,18 +77,23 @@ class StatusHeader(urwid.WidgetWrap): self.error = None self.offline = None self.title = None + self.message = None self.sync = None self.held = None self._error = False self._offline = False self._title = '' + self._message = '' self._sync = 0 self._held = 0 self.held_key = self.app.config.keymap.formatKeys(keymap.LIST_HELD) - def update(self, title=None, error=None, offline=None, refresh=True, held=None): + def update(self, title=None, message=None, error=None, + offline=None, refresh=True, held=None): if title is not None: self.title = title + if message is not None: + self.message = message if error is not None: self.error = error if offline is not None: @@ -100,9 +105,11 @@ class StatusHeader(urwid.WidgetWrap): self.refresh() def refresh(self): - if self._title != self.title: + if (self._title != self.title or self._message != self.message): self._title = self.title - self.title_widget.set_text(self._title) + self._message = self.message + t = self.message or self.title + self.title_widget.set_text(t) if self._held != self.held: self._held = self.held if self._held: @@ -145,12 +152,14 @@ class SearchDialog(mywid.ButtonDialog): ring=app.ring) def keypress(self, size, key): - r = super(SearchDialog, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(SearchDialog, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) if keymap.ACTIVATE in commands: self._emit('search') return None - return r + return key # From: cpython/file/2.7/Lib/webbrowser.py with modification to # redirect stdin/out/err. @@ -209,6 +218,7 @@ class App(object): self.log.debug("Starting") self.ring = mywid.KillRing() + self.input_buffer = [] webbrowser.register('xdg-open', None, BackgroundBrowser("xdg-open")) self.fetch_missing_refs = fetch_missing_refs @@ -268,11 +278,17 @@ class App(object): self.popup(dialog) + def clearInputBuffer(self): + if self.input_buffer: + self.input_buffer = [] + self.status.update(message='') + def changeScreen(self, widget, push=True): self.log.debug("Changing screen to %s" % (widget,)) self.status.update(error=False, title=widget.title) if push: self.screens.append(self.loop.widget) + self.clearInputBuffer() self.loop.widget = widget def backScreen(self, target_widget=None): @@ -285,6 +301,7 @@ class App(object): self.log.debug("Popping screen to %s" % (widget,)) if hasattr(widget, 'title'): self.status.update(title=widget.title) + self.clearInputBuffer() self.loop.widget = widget self.refresh(force=True) @@ -298,6 +315,7 @@ class App(object): self.log.debug("Clearing screen history") while self.screens: widget = self.screens.pop() + self.clearInputBuffer() self.loop.widget = widget def refresh(self, data=None, force=False): @@ -329,6 +347,7 @@ class App(object): def popup(self, widget, relative_width=50, relative_height=25, min_width=20, min_height=8): + self.clearInputBuffer() overlay = urwid.Overlay(widget, self.loop.widget, 'center', ('relative', relative_width), 'middle', ('relative', relative_height), @@ -506,7 +525,9 @@ class App(object): return None def unhandledInput(self, key): - commands = self.config.keymap.getCommands(key) + # get commands from buffer + keys = self.input_buffer + [key] + commands = self.config.keymap.getCommands(keys) if keymap.PREV_SCREEN in commands: self.backScreen() elif keymap.TOP_SCREEN in commands: @@ -524,6 +545,11 @@ class App(object): d = self.config.dashboards[key] view = view_change_list.ChangeListView(self, d['query'], d['name']) self.changeScreen(view) + elif keymap.FURTHER_INPUT in commands: + self.input_buffer.append(key) + self.status.update(message=''.join(self.input_buffer)) + return + self.clearInputBuffer() def openURL(self, url): self.log.debug("Open URL %s" % url) diff --git a/gertty/keymap.py b/gertty/keymap.py index b51f805..fd3d3ce 100644 --- a/gertty/keymap.py +++ b/gertty/keymap.py @@ -15,6 +15,7 @@ import re import string +import logging import urwid @@ -29,10 +30,10 @@ CURSOR_PAGE_DOWN = urwid.CURSOR_PAGE_DOWN CURSOR_MAX_LEFT = urwid.CURSOR_MAX_LEFT CURSOR_MAX_RIGHT = urwid.CURSOR_MAX_RIGHT ACTIVATE = urwid.ACTIVATE +# Global gertty commands: KILL = 'kill' YANK = 'yank' YANK_POP = 'yank pop' -# Global gertty commands: PREV_SCREEN = 'previous screen' TOP_SCREEN = 'top screen' HELP = 'help' @@ -74,6 +75,8 @@ SELECT_PATCHSETS = 'select patchsets' NEXT_SELECTABLE = 'next selectable' PREV_SELECTABLE = 'prev selectable' INTERACTIVE_SEARCH = 'interactive search' +# Special: +FURTHER_INPUT = 'further input' DEFAULT_KEYMAP = { REDRAW_SCREEN: 'ctrl l', @@ -93,7 +96,7 @@ DEFAULT_KEYMAP = { PREV_SCREEN: 'esc', TOP_SCREEN: 'meta home', HELP: ['f1', '?'], - QUIT: 'ctrl q', + QUIT: ['ctrl q'], CHANGE_SEARCH: 'ctrl o', REFINE_CHANGE_SEARCH: 'meta o', LIST_HELD: 'f12', @@ -157,15 +160,32 @@ FORMAT_SUBS = ( ) def formatKey(key): + if type(key) == type([]): + return ''.join([formatKey(k) for k in key]) for subre, repl in FORMAT_SUBS: key = subre.sub(repl, key) return key +class Key(object): + def __init__(self, key): + self.key = key + self.keys = {} + self.commands = [] + + def addKey(self, key): + if key not in self.keys: + self.keys[key] = Key(key) + return self.keys[key] + + def __repr__(self): + return '%s %s %s' % (self.__class__.__name__, self.key, self.keys.keys()) + class KeyMap(object): def __init__(self, config): # key -> [commands] - self.keymap = {} + self.keytree = Key(None) self.commandmap = {} + self.multikeys = '' self.update(DEFAULT_KEYMAP) self.update(config) @@ -178,26 +198,42 @@ class KeyMap(object): if type(keys) != type([]): keys = [keys] self.commandmap[command] = keys - self.keymap = {} + self.keytree = Key(None) for command, keys in self.commandmap.items(): for key in keys: - if key in self.keymap: - self.keymap[key].append(command) + if isinstance(key, list): + # This is a command series + tree = self.keytree + for i, innerkey in enumerate(key): + tree = tree.addKey(innerkey) + if i+1 == len(key): + tree.commands.append(command) else: - self.keymap[key] = [command] + tree = self.keytree.addKey(key) + tree.commands.append(command) - def getCommands(self, key): - return self.keymap.get(key, []) + def getCommands(self, keys): + if not keys: + return [] + tree = self.keytree + for key in keys: + tree = tree.keys.get(key) + if not tree: + return [] + ret = tree.commands[:] + if tree.keys: + ret.append(FURTHER_INPUT) + return ret def getKeys(self, command): return self.commandmap.get(command, []) def updateCommandMap(self): "Update the urwid command map with this keymap" - for key, commands in self.keymap.items(): - for command in commands: + for key in self.keytree.keys.values(): + for command in key.commands: if command in URWID_COMMANDS: - urwid.command_map[key]=command + urwid.command_map[key.key]=command def formatKeys(self, command): keys = self.getKeys(command) diff --git a/gertty/view/change.py b/gertty/view/change.py index abffde2..c39e1af 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -47,12 +47,14 @@ class EditTopicDialog(mywid.ButtonDialog): ring=app.ring) def keypress(self, size, key): - r = super(EditTopicDialog, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(EditTopicDialog, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) if keymap.ACTIVATE in commands: self._emit('save') return None - return r + return key class CherryPickDialog(urwid.WidgetWrap): signals = ['save', 'cancel'] @@ -187,12 +189,14 @@ class ReviewDialog(urwid.WidgetWrap): return (approvals, message) def keypress(self, size, key): - r = super(ReviewDialog, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(ReviewDialog, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) if keymap.PREV_SCREEN in commands: self._emit('cancel') return None - return r + return key class ReviewButton(mywid.FixedButton): def __init__(self, revision_row): @@ -786,8 +790,10 @@ class ChangeView(urwid.WidgetWrap): return self.app.toggleHeldChange(self.change_key) def keypress(self, size, key): - r = super(ChangeView, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(ChangeView, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) if keymap.TOGGLE_REVIEWED in commands: self.toggleReviewed() self.refresh() @@ -870,10 +876,10 @@ class ChangeView(urwid.WidgetWrap): if keymap.CHERRY_PICK_CHANGE in commands: self.cherryPickChange() return None - if r in self.app.config.reviewkeys: - self.reviewKey(self.app.config.reviewkeys[r]) + if key in self.app.config.reviewkeys: + self.reviewKey(self.app.config.reviewkeys[key]) return None - return r + return key def diff(self, revision_key): if self.app.config.diff_view == 'unified': diff --git a/gertty/view/change_list.py b/gertty/view/change_list.py index 38a1ef8..3ba5865 100644 --- a/gertty/view/change_list.py +++ b/gertty/view/change_list.py @@ -424,8 +424,10 @@ class ChangeListView(urwid.WidgetWrap): self.listbox.focus_position = pos def keypress(self, size, key): - r = super(ChangeListView, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(ChangeListView, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) if keymap.TOGGLE_LIST_REVIEWED in commands: self.unreviewed = not self.unreviewed self.refresh() diff --git a/gertty/view/diff.py b/gertty/view/diff.py index fa83db4..6286dac 100644 --- a/gertty/view/diff.py +++ b/gertty/view/diff.py @@ -451,7 +451,7 @@ class BaseDiffView(urwid.WidgetWrap): self.interactiveSearch(self.search) return None else: - commands = self.app.config.keymap.getCommands(key) + commands = self.app.config.keymap.getCommands([key]) if keymap.INTERACTIVE_SEARCH in commands: self.nextSearchResult() return None @@ -464,8 +464,11 @@ class BaseDiffView(urwid.WidgetWrap): return None old_focus = self.listbox.focus - r = super(BaseDiffView, self).keypress(size, key) + if not self.app.input_buffer: + r = super(BaseDiffView, self).keypress(size, key) new_focus = self.listbox.focus + keys = self.app.input_buffer + [r] + commands = self.app.config.keymap.getCommands(keys) context = self.getContextAtTop(size) if context: @@ -474,7 +477,6 @@ class BaseDiffView(urwid.WidgetWrap): else: self.file_reminder.set('', '') - commands = self.app.config.keymap.getCommands(r) if (isinstance(old_focus, BaseDiffCommentEdit) and (old_focus != new_focus or (keymap.PREV_SCREEN in commands))): self.cleanupEdit(old_focus) diff --git a/gertty/view/project_list.py b/gertty/view/project_list.py index 49d788a..0537a1b 100644 --- a/gertty/view/project_list.py +++ b/gertty/view/project_list.py @@ -152,8 +152,12 @@ class ProjectListView(urwid.WidgetWrap): project_name, project_key=project_key, unreviewed=True)) def keypress(self, size, key): - r = super(ProjectListView, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(ProjectListView, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) + if not self.app.input_buffer and keymap.FURTHER_INPUT not in commands: + self.app.clearInputBuffer() if keymap.TOGGLE_LIST_REVIEWED in commands: self.unreviewed = not self.unreviewed self.refresh() @@ -177,4 +181,4 @@ class ProjectListView(urwid.WidgetWrap): sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY)) self.app.status.update() return None - return r + return key diff --git a/gertty/view/side_diff.py b/gertty/view/side_diff.py index f31dbcd..5b65033 100644 --- a/gertty/view/side_diff.py +++ b/gertty/view/side_diff.py @@ -49,8 +49,11 @@ class SideDiffCommentEdit(BaseDiffCommentEdit): self.focus_position = 1 def keypress(self, size, key): - r = super(SideDiffCommentEdit, self).keypress(size, key) - commands = self.app.config.keymap.getCommands(r) + if not self.app.input_buffer: + key = super(SideDiffCommentEdit, self).keypress(size, key) + keys = self.app.input_buffer + [key] + commands = self.app.config.keymap.getCommands(keys) + if ((keymap.NEXT_SELECTABLE in commands) or (keymap.PREV_SELECTABLE in commands)): if ((self.context.old_ln is not None and @@ -61,7 +64,7 @@ class SideDiffCommentEdit(BaseDiffCommentEdit): else: self.focus_position = 3 return None - return r + return key class SideDiffComment(BaseDiffComment): def __init__(self, context, old, new):