Add custom palettes and commentlinks

Allow the user to modify the palette and create entire custom palettes
in the config file.

Add support for commentlinks, where matching text can be reformatted
and color added.  A future possibility is adding clickable buttons to
the text.  The following snippet in .gertty.yaml will format and colorize
the Zuul messages:

palettes:
  - name: default
    test-SUCCESS: ['light green', '', '']
    test-FAILURE: ['light red', '', '']

commentlinks:
  - match: "^- (?P<job>.*?) (?P<url>.*?) : (?P<result>.*?) (?P<rest>.*)$"
    replacements:
      - text: "{job:<42}"
      - text:
          color: "test-{result}"
          text: "{result} "
      - text: "{rest}"

Change-Id: Ib4644edd42333f2ef252a6318182efeff16ce1e1
This commit is contained in:
James E. Blair 2014-05-18 07:42:53 -07:00
parent 06ef6e54c2
commit 1c97e81578
5 changed files with 197 additions and 69 deletions

View File

@ -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()

62
gertty/commentlink.py Normal file
View File

@ -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

View File

@ -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']:

88
gertty/palette.py Normal file
View File

@ -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

View File

@ -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