diff --git a/gertty/app.py b/gertty/app.py index bc3107d..7367bd9 100644 --- a/gertty/app.py +++ b/gertty/app.py @@ -28,65 +28,6 @@ from gertty import sync from gertty.view import project_list as view_project_list from gertty.view import change as view_change -palette=[('focused', 'default,standout', ''), - ('header', 'white,bold', 'dark blue'), - ('error', 'light red', 'dark blue'), - ('table-header', 'white,bold', ''), - ('filename', 'light cyan', ''), - ('positive-label', 'dark green', ''), - ('negative-label', 'dark red', ''), - ('max-label', 'light green', ''), - ('min-label', 'light red', ''), - # Diff - ('context-button', 'dark magenta', ''), - ('focused-context-button', 'light magenta', ''), - ('removed-line', 'dark red', ''), - ('removed-word', 'light red', ''), - ('added-line', 'dark green', ''), - ('added-word', 'light green', ''), - ('nonexistent', 'default', ''), - ('focused-removed-line', 'dark red,standout', ''), - ('focused-removed-word', 'light red,standout', ''), - ('focused-added-line', 'dark green,standout', ''), - ('focused-added-word', 'light green,standout', ''), - ('focused-nonexistent', 'default,standout', ''), - ('draft-comment', 'default', 'dark gray'), - ('comment', 'light gray', 'dark gray'), - ('comment-name', 'white', 'dark gray'), - ('line-number', 'dark gray', ''), - ('focused-line-number', 'dark gray,standout', ''), - # Change view - ('change-data', 'light cyan', ''), - ('change-header', 'light blue', ''), - ('revision-name', 'light blue', ''), - ('revision-commit', 'dark blue', ''), - ('revision-comments', 'default', ''), - ('revision-drafts', 'dark red', ''), - ('focused-revision-name', 'light blue,standout', ''), - ('focused-revision-commit', 'dark blue,standout', ''), - ('focused-revision-comments', 'default,standout', ''), - ('focused-revision-drafts', 'dark red,standout', ''), - ('change-message-name', 'yellow', ''), - ('change-message-header', 'brown', ''), - ('revision-button', 'dark magenta', ''), - ('focused-revision-button', 'light magenta', ''), - ('lines-added', 'light green', ''), - ('lines-removed', 'light red', ''), - ('reviewer-name', 'yellow', ''), - # project list - ('unreviewed-project', 'white', ''), - ('subscribed-project', 'default', ''), - ('unsubscribed-project', 'dark gray', ''), - ('focused-unreviewed-project', 'white,standout', ''), - ('focused-subscribed-project', 'default,standout', ''), - ('focused-unsubscribed-project', 'dark gray,standout', ''), - # change list - ('unreviewed-change', 'default', ''), - ('reviewed-change', 'dark gray', ''), - ('focused-unreviewed-change', 'default,standout', ''), - ('focused-reviewed-change', 'dark gray,standout', ''), - ] - WELCOME_TEXT = """\ Welcome to Gertty! @@ -149,9 +90,9 @@ class OpenChangeDialog(mywid.ButtonDialog): return r class App(object): - def __init__(self, server=None, debug=False, disable_sync=False): + def __init__(self, server=None, palette='default', debug=False, disable_sync=False): self.server = server - self.config = config.Config(server) + self.config = config.Config(server, palette) if debug: level = logging.DEBUG else: @@ -169,7 +110,7 @@ class App(object): self.header = urwid.AttrMap(self.status, 'header') screen = view_project_list.ProjectListView(self) self.status.update(title=screen.title) - self.loop = urwid.MainLoop(screen, palette=palette, + self.loop = urwid.MainLoop(screen, palette=self.config.palette.getPalette(), unhandled_input=self.unhandledInput) if screen.isEmpty(): self.welcome() @@ -318,10 +259,12 @@ def main(): help='enable debug logging') parser.add_argument('--no-sync', dest='no_sync', action='store_true', help='disable remote syncing') + parser.add_argument('-p', dest='palette', default='default', + help='Color palette to use') parser.add_argument('server', nargs='?', help='the server to use (as specified in config file)') args = parser.parse_args() - g = App(args.server, args.debug, args.no_sync) + g = App(args.server, args.palette, args.debug, args.no_sync) g.run() diff --git a/gertty/commentlink.py b/gertty/commentlink.py new file mode 100644 index 0000000..7d120d1 --- /dev/null +++ b/gertty/commentlink.py @@ -0,0 +1,62 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +class TextReplacement(object): + def __init__(self, config): + if isinstance(config, basestring): + self.color = None + self.text = config + else: + self.color = config.get('color') + self.text = config['text'] + + def replace(self, data): + if self.color: + return (self.color.format(**data), self.text.format(**data)) + return (None, self.text.format(**data)) + +class CommentLink(object): + def __init__(self, config): + self.match = re.compile(config['match'], re.M) + self.replacements = [] + for r in config['replacements']: + if 'text' in r: + self.replacements.append(TextReplacement(r['text'])) + + def run(self, chunks): + ret = [] + for chunk in chunks: + if not isinstance(chunk, basestring): + # We don't currently support nested commentlinks; if + # we have something that isn't a string, just append + # it to the output. + ret.append(chunk) + continue + if not chunk: + ret += [chunk] + while chunk: + m = self.match.search(chunk) + if not m: + ret.append(chunk) + break + before = chunk[:m.start()] + after = chunk[m.end():] + if before: + ret.append(before) + ret += [r.replace(m.groupdict()) for r in self.replacements] + chunk = after + return ret + diff --git a/gertty/config.py b/gertty/config.py index 138129f..3f4c903 100644 --- a/gertty/config.py +++ b/gertty/config.py @@ -18,6 +18,9 @@ import yaml import voluptuous as v +import gertty.commentlink +import gertty.palette + DEFAULT_CONFIG_PATH='~/.gertty.yaml' class ConfigSchema(object): @@ -33,12 +36,32 @@ class ConfigSchema(object): servers = [server] + text_replacement = {'text': v.Any(str, + {'color': str, + v.Required('text'): str})} + + replacement = v.Any(text_replacement) + + palette = {v.Required('name'): str, + v.Match('(?!name)'): [str]} + + palettes = [palette] + + commentlink = {v.Required('match'): str, + v.Required('replacements'): [replacement]} + + commentlinks = [commentlink] + def getSchema(self, data): - schema = v.Schema({v.Required('servers'): self.servers}) + schema = v.Schema({v.Required('servers'): self.servers, + 'palettes': self.palettes, + 'commentlinks': self.commentlinks, + }) return schema class Config(object): - def __init__(self, server=None, path=DEFAULT_CONFIG_PATH): + def __init__(self, server=None, palette='default', + path=DEFAULT_CONFIG_PATH): self.path = os.path.expanduser(path) if not os.path.exists(self.path): @@ -68,6 +91,16 @@ class Config(object): log_file = server.get('log_file', '~/.gertty.log') self.log_file = os.path.expanduser(log_file) + self.palettes = {} + for p in self.config.get('palettes', []): + self.palettes[p['name']] = gertty.palette.Palette(p) + if not self.palettes: + self.palettes['default'] = gertty.palette.Palette({}) + self.palette = self.palettes[palette] + + self.commentlinks = [gertty.commentlink.CommentLink(c) + for c in self.config.get('commentlinks', [])] + def getServer(self, name=None): for server in self.config['servers']: if name is None or name == server['name']: diff --git a/gertty/palette.py b/gertty/palette.py new file mode 100644 index 0000000..f1d2559 --- /dev/null +++ b/gertty/palette.py @@ -0,0 +1,88 @@ +# Copyright 2014 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +DEFAULT_PALETTE={ + 'focused': ['default,standout', ''], + 'header': ['white,bold', 'dark blue'], + 'error': ['light red', 'dark blue'], + 'table-header': ['white,bold', ''], + 'filename': ['light cyan', ''], + 'positive-label': ['dark green', ''], + 'negative-label': ['dark red', ''], + 'max-label': ['light green', ''], + 'min-label': ['light red', ''], + # Diff + 'context-button': ['dark magenta', ''], + 'focused-context-button': ['light magenta', ''], + 'removed-line': ['dark red', ''], + 'removed-word': ['light red', ''], + 'added-line': ['dark green', ''], + 'added-word': ['light green', ''], + 'nonexistent': ['default', ''], + 'focused-removed-line': ['dark red,standout', ''], + 'focused-removed-word': ['light red,standout', ''], + 'focused-added-line': ['dark green,standout', ''], + 'focused-added-word': ['light green,standout', ''], + 'focused-nonexistent': ['default,standout', ''], + 'draft-comment': ['default', 'dark gray'], + 'comment': ['light gray', 'dark gray'], + 'comment-name': ['white', 'dark gray'], + 'line-number': ['dark gray', ''], + 'focused-line-number': ['dark gray,standout', ''], + # Change view + 'change-data': ['light cyan', ''], + 'change-header': ['light blue', ''], + 'revision-name': ['light blue', ''], + 'revision-commit': ['dark blue', ''], + 'revision-comments': ['default', ''], + 'revision-drafts': ['dark red', ''], + 'focused-revision-name': ['light blue,standout', ''], + 'focused-revision-commit': ['dark blue,standout', ''], + 'focused-revision-comments': ['default,standout', ''], + 'focused-revision-drafts': ['dark red,standout', ''], + 'change-message-name': ['yellow', ''], + 'change-message-header': ['brown', ''], + 'revision-button': ['dark magenta', ''], + 'focused-revision-button': ['light magenta', ''], + 'lines-added': ['light green', ''], + 'lines-removed': ['light red', ''], + 'reviewer-name': ['yellow', ''], + # project list + 'unreviewed-project': ['white', ''], + 'subscribed-project': ['default', ''], + 'unsubscribed-project': ['dark gray', ''], + 'focused-unreviewed-project': ['white,standout', ''], + 'focused-subscribed-project': ['default,standout', ''], + 'focused-unsubscribed-project': ['dark gray,standout', ''], + # change list + 'unreviewed-change': ['default', ''], + 'reviewed-change': ['dark gray', ''], + 'focused-unreviewed-change': ['default,standout', ''], + 'focused-reviewed-change': ['dark gray,standout', ''], + } + +class Palette(object): + def __init__(self, config): + self.palette = {} + self.palette.update(DEFAULT_PALETTE) + d = config.copy() + if 'name' in d: + del d['name'] + self.palette.update(d) + + def getPalette(self): + ret = [] + for k,v in self.palette.items(): + ret.append(tuple([k]+v)) + return ret diff --git a/gertty/view/change.py b/gertty/view/change.py index b7de655..a860348 100644 --- a/gertty/view/change.py +++ b/gertty/view/change.py @@ -259,7 +259,7 @@ class RevisionRow(urwid.WidgetWrap): self.app.popup(dialog, min_height=min_height) class ChangeMessageBox(urwid.Text): - def __init__(self, message): + def __init__(self, message, commentlinks): super(ChangeMessageBox, self).__init__(u'') lines = message.message.split('\n') text = [('change-message-name', message.name), @@ -268,8 +268,10 @@ class ChangeMessageBox(urwid.Text): message.created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))] if lines and lines[-1]: lines.append('') - text += '\n'.join(lines) - self.set_text(text) + comment_text = ['\n'.join(lines)] + for commentlink in commentlinks: + comment_text = commentlink.run(comment_text) + self.set_text(text+comment_text) class ChangeView(urwid.WidgetWrap): help = mywid.GLOBAL_HELP + """ @@ -427,7 +429,7 @@ This Screen for message in change.messages: row = self.message_rows.get(message.key) if not row: - row = ChangeMessageBox(message) + row = ChangeMessageBox(message, self.app.config.commentlinks) self.listbox.body.insert(listbox_index, row) self.message_rows[message.key] = row # Messages are extremely unlikely to be deleted, skip