Add hyperlinks
Add a 'link' commentlink substitution type that is a hyperlink which will open the link in the user's web browser. A built-in configuration will apply this automaticlly to most http(s)?:// links (using a very simple and not entirely correct regex). My current commentlink section in my config is as follows: commentlinks: - match: "^- (?P<job>.*?) (?P<url>.*?) : (?P<result>.*?) (?P<rest>.*)$" replacements: - link: text: "{job:<42}" url: "{url}" - text: color: "test-{result}" text: "{result} " - text: "{rest}" In order to support the Zuul commentlink syntax. Change-Id: Ifceee547c116fdcc15b50a2f73a0ddfe2e98af84
This commit is contained in:
parent
39890234c3
commit
844634b2f6
@ -17,6 +17,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
|
||||
import urwid
|
||||
|
||||
@ -251,6 +252,9 @@ class App(object):
|
||||
return gitrepo.Repo(self.config.url+'p/'+project_name,
|
||||
local_path)
|
||||
|
||||
def openURL(self, url):
|
||||
self.log.debug("Open URL %s" % url)
|
||||
webbrowser.open_new_tab(url)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
|
@ -14,6 +14,10 @@
|
||||
|
||||
import re
|
||||
|
||||
import urwid
|
||||
|
||||
import mywid
|
||||
|
||||
class TextReplacement(object):
|
||||
def __init__(self, config):
|
||||
if isinstance(config, basestring):
|
||||
@ -23,11 +27,23 @@ class TextReplacement(object):
|
||||
self.color = config.get('color')
|
||||
self.text = config['text']
|
||||
|
||||
def replace(self, data):
|
||||
def replace(self, app, data):
|
||||
if self.color:
|
||||
return (self.color.format(**data), self.text.format(**data))
|
||||
return (None, self.text.format(**data))
|
||||
|
||||
class LinkReplacement(object):
|
||||
def __init__(self, config):
|
||||
self.url = config['url']
|
||||
self.text = config['text']
|
||||
|
||||
def replace(self, app, data):
|
||||
link = mywid.Link(self.text.format(**data), 'link', 'focused-link')
|
||||
urwid.connect_signal(link, 'selected',
|
||||
lambda link:app.openURL(self.url.format(**data)))
|
||||
app.log.debug("link %s" % link)
|
||||
return link
|
||||
|
||||
class CommentLink(object):
|
||||
def __init__(self, config):
|
||||
self.match = re.compile(config['match'], re.M)
|
||||
@ -35,8 +51,10 @@ class CommentLink(object):
|
||||
for r in config['replacements']:
|
||||
if 'text' in r:
|
||||
self.replacements.append(TextReplacement(r['text']))
|
||||
if 'link' in r:
|
||||
self.replacements.append(LinkReplacement(r['link']))
|
||||
|
||||
def run(self, chunks):
|
||||
def run(self, app, chunks):
|
||||
ret = []
|
||||
for chunk in chunks:
|
||||
if not isinstance(chunk, basestring):
|
||||
@ -56,7 +74,7 @@ class CommentLink(object):
|
||||
after = chunk[m.end():]
|
||||
if before:
|
||||
ret.append(before)
|
||||
ret += [r.replace(m.groupdict()) for r in self.replacements]
|
||||
ret += [r.replace(app, m.groupdict()) for r in self.replacements]
|
||||
chunk = after
|
||||
return ret
|
||||
|
||||
|
@ -40,7 +40,10 @@ class ConfigSchema(object):
|
||||
{'color': str,
|
||||
v.Required('text'): str})}
|
||||
|
||||
replacement = v.Any(text_replacement)
|
||||
link_replacement = {'link': {v.Required('url'): str,
|
||||
v.Required('text'): str}}
|
||||
|
||||
replacement = v.Any(text_replacement, link_replacement)
|
||||
|
||||
palette = {v.Required('name'): str,
|
||||
v.Match('(?!name)'): [str]}
|
||||
@ -100,6 +103,13 @@ class Config(object):
|
||||
|
||||
self.commentlinks = [gertty.commentlink.CommentLink(c)
|
||||
for c in self.config.get('commentlinks', [])]
|
||||
self.commentlinks.append(
|
||||
gertty.commentlink.CommentLink(dict(
|
||||
match="(?P<url>https?://\\S*)",
|
||||
replacements=[
|
||||
dict(link=dict(
|
||||
text="{url}",
|
||||
url="{url}"))])))
|
||||
|
||||
def getServer(self, name=None):
|
||||
for server in self.config['servers']:
|
||||
|
158
gertty/mywid.py
158
gertty/mywid.py
@ -103,3 +103,161 @@ class YesNoDialog(ButtonDialog):
|
||||
self._emit('no')
|
||||
return None
|
||||
return r
|
||||
|
||||
class HyperText(urwid.Text):
|
||||
_selectable = True
|
||||
|
||||
def __init__(self, markup, align=urwid.LEFT, wrap=urwid.SPACE, layout=None):
|
||||
self._mouse_press_item = None
|
||||
self.selectable_items = []
|
||||
self.focused_index = None
|
||||
super(HyperText, self).__init__(markup, align, wrap, layout)
|
||||
|
||||
def focusFirstItem(self):
|
||||
if len(self.selectable_items) == 0:
|
||||
return False
|
||||
self.focusItem(0)
|
||||
return True
|
||||
|
||||
def focusLastItem(self):
|
||||
if len(self.selectable_items) == 0:
|
||||
return False
|
||||
self.focusItem(len(self.selectable_items)-1)
|
||||
return True
|
||||
|
||||
def focusPreviousItem(self):
|
||||
if len(self.selectable_items) == 0:
|
||||
return False
|
||||
item = max(0, self.focused_index-1)
|
||||
if item != self.focused_index:
|
||||
self.focusItem(item)
|
||||
return True
|
||||
return False
|
||||
|
||||
def focusNextItem(self):
|
||||
if len(self.selectable_items) == 0:
|
||||
return False
|
||||
item = min(len(self.selectable_items)-1, self.focused_index+1)
|
||||
if item != self.focused_index:
|
||||
self.focusItem(item)
|
||||
return True
|
||||
return False
|
||||
|
||||
def focusItem(self, item):
|
||||
self.focused_index = item
|
||||
self.set_text(self._markup)
|
||||
self._invalidate()
|
||||
|
||||
def select(self):
|
||||
if self.focused_index is not None:
|
||||
self.selectable_items[self.focused_index][0].select()
|
||||
|
||||
def keypress(self, size, key):
|
||||
if self._command_map[key] == urwid.CURSOR_UP:
|
||||
if self.focusPreviousItem():
|
||||
return False
|
||||
return key
|
||||
elif self._command_map[key] == urwid.CURSOR_DOWN:
|
||||
if self.focusNextItem():
|
||||
return False
|
||||
return key
|
||||
elif key == 'enter':
|
||||
self.select()
|
||||
return False
|
||||
return key
|
||||
|
||||
def getPosAtCoords(self, maxcol, col, row):
|
||||
trans = self.get_line_translation(maxcol)
|
||||
colpos = 0
|
||||
line = trans[row]
|
||||
for t in line:
|
||||
if len(t) == 2:
|
||||
width, pos = t
|
||||
if colpos <= col < colpos + width:
|
||||
return pos
|
||||
else:
|
||||
width, start, end = t
|
||||
if colpos <= col < colpos + width:
|
||||
return start + (col - colpos)
|
||||
colpos += width
|
||||
return None
|
||||
|
||||
def getItemAtCoords(self, maxcol, col, row):
|
||||
pos = self.getPosAtCoords(maxcol, col, row)
|
||||
index = 0
|
||||
for item, start, end in self.selectable_items:
|
||||
if start <= pos <= end:
|
||||
return index
|
||||
index += 1
|
||||
return None
|
||||
|
||||
def mouse_event(self, size, event, button, col, row, focus):
|
||||
if ((button not in [0, 1]) or
|
||||
(event not in ['mouse press', 'mouse release'])):
|
||||
return False
|
||||
item = self.getItemAtCoords(size[0], col, row)
|
||||
if item is None:
|
||||
if self.focused_index is None:
|
||||
self.focusItemLeft()
|
||||
return False
|
||||
if event == 'mouse press':
|
||||
self.focusItem(item)
|
||||
self._mouse_press_item = item
|
||||
if event == 'mouse release':
|
||||
if self._mouse_press_item == item:
|
||||
self.select()
|
||||
self._mouse_press_item = None
|
||||
return True
|
||||
|
||||
def processLinks(self, markup, data=None):
|
||||
if data is None:
|
||||
data = dict(pos=0)
|
||||
if isinstance(markup, list):
|
||||
return [self.processLinks(i, data) for i in markup]
|
||||
if isinstance(markup, tuple):
|
||||
return (markup[0], self.processLinks(markup[1], data))
|
||||
if isinstance(markup, Link):
|
||||
self.selectable_items.append((markup, data['pos'], data['pos']+len(markup.text)))
|
||||
data['pos'] += len(markup.text)
|
||||
focused = len(self.selectable_items)-1 == self.focused_index
|
||||
link_attr = markup.getAttr(focused)
|
||||
if link_attr:
|
||||
return (link_attr, markup.text)
|
||||
else:
|
||||
return markup.text
|
||||
data['pos'] += len(markup)
|
||||
return markup
|
||||
|
||||
def set_text(self, markup):
|
||||
self._markup = markup
|
||||
self.selectable_items = []
|
||||
super(HyperText, self).set_text(self.processLinks(markup))
|
||||
|
||||
def move_cursor_to_coords(self, size, col, row):
|
||||
if self.focused_index is None:
|
||||
if row:
|
||||
self.focusLastItem()
|
||||
else:
|
||||
self.focusFirstItem()
|
||||
return True
|
||||
|
||||
def render(self, size, focus=False):
|
||||
if (not focus) and (self.focused_index is not None):
|
||||
self.focusItem(None)
|
||||
return super(HyperText, self).render(size, focus)
|
||||
|
||||
class Link(urwid.Widget):
|
||||
signals = ['selected']
|
||||
|
||||
def __init__(self, text, attr=None, focused_attr=None):
|
||||
self.text = text
|
||||
self.attr = attr
|
||||
self.focused_attr = focused_attr
|
||||
|
||||
def select(self):
|
||||
self._emit('selected')
|
||||
|
||||
def getAttr(self, focus):
|
||||
if focus:
|
||||
return self.focused_attr
|
||||
return self.attr
|
||||
|
@ -22,6 +22,8 @@ DEFAULT_PALETTE={
|
||||
'negative-label': ['dark red', ''],
|
||||
'max-label': ['light green', ''],
|
||||
'min-label': ['light red', ''],
|
||||
'link': ['dark blue', ''],
|
||||
'focused-link': ['light blue', ''],
|
||||
# Diff
|
||||
'context-button': ['dark magenta', ''],
|
||||
'focused-context-button': ['light magenta', ''],
|
||||
|
@ -177,7 +177,7 @@ class RevisionRow(urwid.WidgetWrap):
|
||||
removed = 0
|
||||
total_added += added
|
||||
total_removed += removed
|
||||
table.addRow([urwid.Text(('filename', filename)),
|
||||
table.addRow([urwid.Text(('filename', filename), wrap='clip'),
|
||||
urwid.Text([('lines-added', '+%i' % (added,)), ', '],
|
||||
align=urwid.RIGHT),
|
||||
urwid.Text(('lines-removed', '-%i' % (removed,)))])
|
||||
@ -258,8 +258,8 @@ class RevisionRow(urwid.WidgetWrap):
|
||||
lambda button: self.app.backScreen())
|
||||
self.app.popup(dialog, min_height=min_height)
|
||||
|
||||
class ChangeMessageBox(urwid.Text):
|
||||
def __init__(self, message, commentlinks):
|
||||
class ChangeMessageBox(mywid.HyperText):
|
||||
def __init__(self, app, message):
|
||||
super(ChangeMessageBox, self).__init__(u'')
|
||||
lines = message.message.split('\n')
|
||||
text = [('change-message-name', message.name),
|
||||
@ -269,8 +269,8 @@ class ChangeMessageBox(urwid.Text):
|
||||
if lines and lines[-1]:
|
||||
lines.append('')
|
||||
comment_text = ['\n'.join(lines)]
|
||||
for commentlink in commentlinks:
|
||||
comment_text = commentlink.run(comment_text)
|
||||
for commentlink in app.config.commentlinks:
|
||||
comment_text = commentlink.run(app, comment_text)
|
||||
self.set_text(text+comment_text)
|
||||
|
||||
class ChangeView(urwid.WidgetWrap):
|
||||
@ -429,7 +429,7 @@ This Screen
|
||||
for message in change.messages:
|
||||
row = self.message_rows.get(message.key)
|
||||
if not row:
|
||||
row = ChangeMessageBox(message, self.app.config.commentlinks)
|
||||
row = ChangeMessageBox(self.app, message)
|
||||
self.listbox.body.insert(listbox_index, row)
|
||||
self.message_rows[message.key] = row
|
||||
# Messages are extremely unlikely to be deleted, skip
|
||||
|
Loading…
x
Reference in New Issue
Block a user