inotify: guard against non-existent paths
[git-cola.git] / cola / qtutils.py
blob160bb90a6ffe1898934c13c699242332abf802cd
1 # Copyright (c) 2008 David Aguilar
2 """This module provides miscellaneous Qt utility functions.
3 """
4 import os
5 import re
7 from PyQt4 import QtGui
8 from PyQt4 import QtCore
9 from PyQt4.QtCore import Qt
10 from PyQt4.QtCore import SIGNAL
12 from cola import core
13 from cola import gitcfg
14 from cola import utils
15 from cola import settings
16 from cola import resources
17 from cola.compat import set
18 from cola.decorators import memoize
19 from cola.i18n import N_
20 from cola.interaction import Interaction
21 from cola.models.prefs import FONTDIFF
22 from cola.widgets import defs
25 def connect_action(action, fn):
26 action.connect(action, SIGNAL('triggered()'), fn)
29 def connect_action_bool(action, fn):
30 action.connect(action, SIGNAL('triggered(bool)'), fn)
33 def connect_button(button, fn):
34 button.connect(button, SIGNAL('clicked()'), fn)
37 def connect_toggle(toggle, fn):
38 toggle.connect(toggle, SIGNAL('toggled(bool)'), fn)
41 def active_window():
42 return QtGui.QApplication.activeWindow()
45 def prompt(msg, title=None, text=''):
46 """Presents the user with an input widget and returns the input."""
47 if title is None:
48 title = msg
49 result = QtGui.QInputDialog.getText(active_window(), msg, title,
50 QtGui.QLineEdit.Normal, text)
51 return (unicode(result[0]), result[1])
54 def create_listwidget_item(text, filename):
55 """Creates a QListWidgetItem with text and the icon at filename."""
56 item = QtGui.QListWidgetItem()
57 item.setIcon(QtGui.QIcon(filename))
58 item.setText(text)
59 return item
62 def create_treewidget_item(text, filename):
63 """Creates a QTreeWidgetItem with text and the icon at filename."""
64 icon = cached_icon_from_path(filename)
65 item = QtGui.QTreeWidgetItem()
66 item.setIcon(0, icon)
67 item.setText(0, text)
68 return item
71 @memoize
72 def cached_icon_from_path(filename):
73 return QtGui.QIcon(filename)
76 def confirm(title, text, informative_text, ok_text,
77 icon=None, default=True):
78 """Confirm that an action should take place"""
79 if icon is None:
80 icon = ok_icon()
81 elif icon and isinstance(icon, basestring):
82 icon = QtGui.QIcon(icon)
83 msgbox = QtGui.QMessageBox(active_window())
84 msgbox.setWindowTitle(title)
85 msgbox.setText(text)
86 msgbox.setInformativeText(informative_text)
87 ok = msgbox.addButton(ok_text, QtGui.QMessageBox.ActionRole)
88 ok.setIcon(icon)
89 cancel = msgbox.addButton(QtGui.QMessageBox.Cancel)
90 if default:
91 msgbox.setDefaultButton(ok)
92 else:
93 msgbox.setDefaultButton(cancel)
94 msgbox.exec_()
95 return msgbox.clickedButton() == ok
98 def critical(title, message=None, details=None):
99 """Show a warning with the provided title and message."""
100 if message is None:
101 message = title
102 mbox = QtGui.QMessageBox(active_window())
103 mbox.setWindowTitle(title)
104 mbox.setTextFormat(QtCore.Qt.PlainText)
105 mbox.setText(message)
106 mbox.setIcon(QtGui.QMessageBox.Critical)
107 mbox.setStandardButtons(QtGui.QMessageBox.Close)
108 mbox.setDefaultButton(QtGui.QMessageBox.Close)
109 if details:
110 mbox.setDetailedText(details)
111 mbox.exec_()
114 def information(title, message=None, details=None, informative_text=None):
115 """Show information with the provided title and message."""
116 if message is None:
117 message = title
118 mbox = QtGui.QMessageBox(active_window())
119 mbox.setStandardButtons(QtGui.QMessageBox.Close)
120 mbox.setDefaultButton(QtGui.QMessageBox.Close)
121 mbox.setWindowTitle(title)
122 mbox.setWindowModality(QtCore.Qt.WindowModal)
123 mbox.setTextFormat(QtCore.Qt.PlainText)
124 mbox.setText(message)
125 if informative_text:
126 mbox.setInformativeText(informative_text)
127 if details:
128 mbox.setDetailedText(details)
129 # Render git.svg into a 1-inch wide pixmap
130 pixmap = QtGui.QPixmap(resources.icon('git.svg'))
131 xres = pixmap.physicalDpiX()
132 pixmap = pixmap.scaledToHeight(xres, QtCore.Qt.SmoothTransformation)
133 mbox.setIconPixmap(pixmap)
134 mbox.exec_()
137 def question(title, msg, default=True):
138 """Launches a QMessageBox question with the provided title and message.
139 Passing "default=False" will make "No" the default choice."""
140 yes = QtGui.QMessageBox.Yes
141 no = QtGui.QMessageBox.No
142 buttons = yes | no
143 if default:
144 default = yes
145 else:
146 default = no
147 result = (QtGui.QMessageBox
148 .question(active_window(), title, msg, buttons, default))
149 return result == QtGui.QMessageBox.Yes
152 def selected_treeitem(tree_widget):
153 """Returns a(id_number, is_selected) for a QTreeWidget."""
154 id_number = None
155 selected = False
156 item = tree_widget.currentItem()
157 if item:
158 id_number = item.data(0, QtCore.Qt.UserRole).toInt()[0]
159 selected = True
160 return(id_number, selected)
163 def selected_row(list_widget):
164 """Returns a(row_number, is_selected) tuple for a QListWidget."""
165 items = list_widget.selectedItems()
166 if not items:
167 return (-1, False)
168 item = items[0]
169 return (list_widget.row(item), True)
172 def selection_list(listwidget, items):
173 """Returns an array of model items that correspond to
174 the selected QListWidget indices."""
175 selected = []
176 itemcount = listwidget.count()
177 widgetitems = [ listwidget.item(idx) for idx in range(itemcount) ]
179 for item, widgetitem in zip(items, widgetitems):
180 if widgetitem.isSelected():
181 selected.append(item)
182 return selected
185 def tree_selection(treeitem, items):
186 """Returns model items that correspond to selected widget indices"""
187 itemcount = treeitem.childCount()
188 widgetitems = [ treeitem.child(idx) for idx in range(itemcount) ]
189 selected = []
190 for item, widgetitem in zip(items[:len(widgetitems)], widgetitems):
191 if widgetitem.isSelected():
192 selected.append(item)
194 return selected
197 def selected_item(list_widget, items):
198 """Returns the selected item in a QListWidget."""
199 widget_items = list_widget.selectedItems()
200 if not widget_items:
201 return None
202 widget_item = widget_items[0]
203 row = list_widget.row(widget_item)
204 if row < len(items):
205 return items[row]
206 else:
207 return None
210 def selected_items(list_widget, items):
211 """Returns the selected item in a QListWidget."""
212 selection = []
213 widget_items = list_widget.selectedItems()
214 if not widget_items:
215 return selection
216 for widget_item in widget_items:
217 row = list_widget.row(widget_item)
218 if row < len(items):
219 selection.append(items[row])
220 return selection
223 def open_dialog(title, filename=None):
224 """Creates an Open File dialog and returns a filename."""
225 return unicode(QtGui.QFileDialog
226 .getOpenFileName(active_window(), title, filename))
229 def opendir_dialog(title, path):
230 """Prompts for a directory path"""
232 flags = (QtGui.QFileDialog.ShowDirsOnly |
233 QtGui.QFileDialog.DontResolveSymlinks)
234 return unicode(QtGui.QFileDialog
235 .getExistingDirectory(active_window(),
236 title, path, flags))
239 def save_as(filename, title='Save As...'):
240 """Creates a Save File dialog and returns a filename."""
241 return unicode(QtGui.QFileDialog
242 .getSaveFileName(active_window(), title, filename))
245 def icon(basename):
246 """Given a basename returns a QIcon from the corresponding cola icon."""
247 return QtGui.QIcon(resources.icon(basename))
250 def set_clipboard(text):
251 """Sets the copy/paste buffer to text."""
252 if not text:
253 return
254 clipboard = QtGui.QApplication.instance().clipboard()
255 clipboard.setText(text, QtGui.QClipboard.Clipboard)
256 clipboard.setText(text, QtGui.QClipboard.Selection)
259 def add_action_bool(widget, text, fn, checked, *shortcuts):
260 action = _add_action(widget, text, fn, connect_action_bool, *shortcuts)
261 action.setCheckable(True)
262 action.setChecked(checked)
263 return action
266 def add_action(widget, text, fn, *shortcuts):
267 return _add_action(widget, text, fn, connect_action, *shortcuts)
270 def _add_action(widget, text, fn, connect, *shortcuts):
271 action = QtGui.QAction(text, widget)
272 connect(action, fn)
273 if shortcuts:
274 shortcuts = list(set(shortcuts))
275 action.setShortcuts(shortcuts)
276 action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
277 widget.addAction(action)
278 return action
280 def set_selected_item(widget, idx):
281 """Sets a the currently selected item to the item at index idx."""
282 if type(widget) is QtGui.QTreeWidget:
283 item = widget.topLevelItem(idx)
284 if item:
285 widget.setItemSelected(item, True)
286 widget.setCurrentItem(item)
289 def add_items(widget, items):
290 """Adds items to a widget."""
291 for item in items:
292 widget.addItem(item)
295 def set_items(widget, items):
296 """Clear the existing widget contents and set the new items."""
297 widget.clear()
298 add_items(widget, items)
301 def icon_file(filename, staged=False, untracked=False):
302 """Returns a file path representing a corresponding file path."""
303 if staged:
304 if core.exists(filename):
305 ifile = resources.icon('staged-item.png')
306 else:
307 ifile = resources.icon('removed.png')
308 elif untracked:
309 ifile = resources.icon('untracked.png')
310 else:
311 ifile = utils.file_icon(filename)
312 return ifile
315 def icon_for_file(filename, staged=False, untracked=False):
316 """Returns a QIcon for a particular file path."""
317 ifile = icon_file(filename, staged=staged, untracked=untracked)
318 return icon(ifile)
321 def create_treeitem(filename, staged=False, untracked=False, check=True):
322 """Given a filename, return a QListWidgetItem suitable
323 for adding to a QListWidget. "staged" and "untracked"
324 controls whether to use the appropriate icons."""
325 if check:
326 ifile = icon_file(filename, staged=staged, untracked=untracked)
327 else:
328 ifile = resources.icon('staged.png')
329 return create_treewidget_item(filename, ifile)
332 def update_file_icons(widget, items, staged=True,
333 untracked=False, offset=0):
334 """Populate a QListWidget with custom icon items."""
335 for idx, model_item in enumerate(items):
336 item = widget.item(idx+offset)
337 if item:
338 item.setIcon(icon_for_file(model_item, staged, untracked))
340 @memoize
341 def cached_icon(key):
342 """Maintain a cache of standard icons and return cache entries."""
343 style = QtGui.QApplication.instance().style()
344 return style.standardIcon(key)
347 def dir_icon():
348 """Return a standard icon for a directory."""
349 return cached_icon(QtGui.QStyle.SP_DirIcon)
352 def file_icon():
353 """Return a standard icon for a file."""
354 return cached_icon(QtGui.QStyle.SP_FileIcon)
357 def apply_icon():
358 """Return a standard Apply icon"""
359 return cached_icon(QtGui.QStyle.SP_DialogApplyButton)
362 def new_icon():
363 return cached_icon(QtGui.QStyle.SP_FileDialogNewFolder)
366 def save_icon():
367 """Return a standard Save icon"""
368 return cached_icon(QtGui.QStyle.SP_DialogSaveButton)
371 def ok_icon():
372 """Return a standard Ok icon"""
373 return cached_icon(QtGui.QStyle.SP_DialogOkButton)
376 def open_icon():
377 """Return a standard open directory icon"""
378 return cached_icon(QtGui.QStyle.SP_DirOpenIcon)
381 def help_icon():
382 """Return a standard open directory icon"""
383 return cached_icon(QtGui.QStyle.SP_DialogHelpButton)
386 def open_file_icon():
387 return icon('open.svg')
390 def options_icon():
391 """Return a standard open directory icon"""
392 return icon('options.svg')
395 def dir_close_icon():
396 """Return a standard closed directory icon"""
397 return cached_icon(QtGui.QStyle.SP_DirClosedIcon)
400 def titlebar_close_icon():
401 """Return a dock widget close icon"""
402 return cached_icon(QtGui.QStyle.SP_TitleBarCloseButton)
405 def titlebar_normal_icon():
406 """Return a dock widget close icon"""
407 return cached_icon(QtGui.QStyle.SP_TitleBarNormalButton)
410 def git_icon():
411 return icon('git.svg')
414 def reload_icon():
415 """Returna standard Refresh icon"""
416 return cached_icon(QtGui.QStyle.SP_BrowserReload)
419 def discard_icon():
420 """Return a standard Discard icon"""
421 return cached_icon(QtGui.QStyle.SP_DialogDiscardButton)
424 def close_icon():
425 """Return a standard Close icon"""
426 return cached_icon(QtGui.QStyle.SP_DialogCloseButton)
429 def add_close_action(widget):
430 """Adds close action and shortcuts to a widget."""
431 return add_action(widget, N_('Close...'),
432 widget.close, QtGui.QKeySequence.Close, 'Ctrl+Q')
435 def center_on_screen(widget):
436 """Move widget to the center of the default screen"""
437 desktop = QtGui.QApplication.instance().desktop()
438 rect = desktop.screenGeometry(QtGui.QCursor().pos())
439 cy = rect.height()/2
440 cx = rect.width()/2
441 widget.move(cx - widget.width()/2, cy - widget.height()/2)
444 def save_state(widget, handler=None):
445 if handler is None:
446 handler = settings.Settings()
447 if gitcfg.instance().get('cola.savewindowsettings', True):
448 handler.save_gui_state(widget)
451 def export_window_state(widget, state, version):
452 # Save the window state
453 windowstate = widget.saveState(version)
454 state['windowstate'] = unicode(windowstate.toBase64().data())
455 return state
458 def apply_window_state(widget, state, version):
459 # Restore the dockwidget, etc. window state
460 try:
461 windowstate = state['windowstate']
462 return widget.restoreState(QtCore.QByteArray.fromBase64(str(windowstate)),
463 version)
464 except KeyError:
465 return False
468 def apply_state(widget):
469 state = settings.Settings().get_gui_state(widget)
470 return bool(state) and widget.apply_state(state)
473 @memoize
474 def theme_icon(name):
475 """Grab an icon from the current theme with a fallback
477 Support older versions of Qt by catching AttributeError and
478 falling back to our default icons.
481 try:
482 base, ext = os.path.splitext(name)
483 qicon = QtGui.QIcon.fromTheme(base)
484 if not qicon.isNull():
485 return qicon
486 except AttributeError:
487 pass
488 return icon(name)
491 def default_monospace_font():
492 font = QtGui.QFont()
493 family = 'Monospace'
494 if utils.is_darwin():
495 family = 'Monaco'
496 font.setFamily(family)
497 return font
500 def diff_font_str():
501 font_str = gitcfg.instance().get(FONTDIFF)
502 if font_str is None:
503 font = default_monospace_font()
504 font_str = unicode(font.toString())
505 return font_str
508 def diff_font():
509 font_str = diff_font_str()
510 font = QtGui.QFont()
511 font.fromString(font_str)
512 return font
515 def create_button(text='', layout=None, tooltip=None, icon=None):
516 """Create a button, set its title, and add it to the parent."""
517 button = QtGui.QPushButton()
518 button.setCursor(Qt.PointingHandCursor)
519 if text:
520 button.setText(text)
521 if icon:
522 button.setIcon(icon)
523 if tooltip is not None:
524 button.setToolTip(tooltip)
525 if layout is not None:
526 layout.addWidget(button)
527 return button
530 def create_action_button(tooltip, icon):
531 button = QtGui.QPushButton()
532 button.setCursor(QtCore.Qt.PointingHandCursor)
533 button.setFlat(True)
534 button.setIcon(icon)
535 button.setFixedSize(QtCore.QSize(16, 16))
536 button.setToolTip(tooltip)
537 return button
540 class DockTitleBarWidget(QtGui.QWidget):
542 def __init__(self, parent, title, stretch=True):
543 QtGui.QWidget.__init__(self, parent)
544 self.label = label = QtGui.QLabel()
545 font = label.font()
546 font.setCapitalization(QtGui.QFont.SmallCaps)
547 label.setFont(font)
548 label.setText(title)
550 self.setCursor(QtCore.Qt.OpenHandCursor)
552 self.close_button = create_action_button(
553 N_('Close'), titlebar_close_icon())
555 self.toggle_button = create_action_button(
556 N_('Detach'), titlebar_normal_icon())
558 self.corner_layout = QtGui.QHBoxLayout()
559 self.corner_layout.setMargin(defs.no_margin)
560 self.corner_layout.setSpacing(defs.spacing)
562 self.main_layout = QtGui.QHBoxLayout()
563 self.main_layout.setMargin(defs.small_margin)
564 self.main_layout.setSpacing(defs.spacing)
565 self.main_layout.addWidget(label)
566 self.main_layout.addSpacing(defs.spacing)
567 if stretch:
568 self.main_layout.addStretch()
569 self.main_layout.addLayout(self.corner_layout)
570 self.main_layout.addSpacing(defs.spacing)
571 self.main_layout.addWidget(self.toggle_button)
572 self.main_layout.addWidget(self.close_button)
574 self.setLayout(self.main_layout)
576 connect_button(self.toggle_button, self.toggle_floating)
577 connect_button(self.close_button, self.toggle_visibility)
579 def toggle_floating(self):
580 self.parent().setFloating(not self.parent().isFloating())
581 self.update_tooltips()
583 def toggle_visibility(self):
584 self.parent().toggleViewAction().trigger()
586 def set_title(self, title):
587 self.label.setText(title)
589 def add_corner_widget(self, widget):
590 self.corner_layout.addWidget(widget)
592 def update_tooltips(self):
593 if self.parent().isFloating():
594 tooltip = N_('Attach')
595 else:
596 tooltip = N_('Detach')
597 self.toggle_button.setToolTip(tooltip)
600 def create_dock(title, parent, stretch=True):
601 """Create a dock widget and set it up accordingly."""
602 dock = QtGui.QDockWidget(parent)
603 dock.setWindowTitle(title)
604 dock.setObjectName(title)
605 titlebar = DockTitleBarWidget(dock, title, stretch=stretch)
606 dock.setTitleBarWidget(titlebar)
607 if hasattr(parent, 'dockwidgets'):
608 parent.dockwidgets.append(dock)
609 return dock
612 def create_menu(title, parent):
613 """Create a menu and set its title."""
614 qmenu = QtGui.QMenu(parent)
615 qmenu.setTitle(title)
616 return qmenu
619 def create_toolbutton(text=None, layout=None, tooltip=None, icon=None):
620 button = QtGui.QToolButton()
621 button.setAutoRaise(True)
622 button.setAutoFillBackground(True)
623 button.setCursor(Qt.PointingHandCursor)
624 if icon:
625 button.setIcon(icon)
626 if text:
627 button.setText(text)
628 button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
629 if tooltip:
630 button.setToolTip(tooltip)
631 if layout is not None:
632 layout.addWidget(button)
633 return button
636 # Syntax highlighting
638 def TERMINAL(pattern):
640 Denotes that a pattern is the final pattern that should
641 be matched. If this pattern matches no other formats
642 will be applied, even if they would have matched.
644 return '__TERMINAL__:%s' % pattern
646 # Cache the results of re.compile so that we don't keep
647 # rebuilding the same regexes whenever stylesheets change
648 _RGX_CACHE = {}
650 def rgba(r, g, b, a=255):
651 c = QtGui.QColor()
652 c.setRgb(r, g, b)
653 c.setAlpha(a)
654 return c
656 default_colors = {
657 'color_text': rgba(0x00, 0x00, 0x00),
658 'color_add': rgba(0xcd, 0xff, 0xe0),
659 'color_remove': rgba(0xff, 0xd0, 0xd0),
660 'color_header': rgba(0xbb, 0xbb, 0xbb),
664 class GenericSyntaxHighligher(QtGui.QSyntaxHighlighter):
665 def __init__(self, doc, *args, **kwargs):
666 QtGui.QSyntaxHighlighter.__init__(self, doc)
667 for attr, val in default_colors.items():
668 setattr(self, attr, val)
669 self._rules = []
670 self.enabled = True
671 self.generate_rules()
673 def generate_rules(self):
674 pass
676 def set_enabled(self, enabled):
677 self.enabled = enabled
679 def create_rules(self, *rules):
680 if len(rules) % 2:
681 raise Exception('create_rules requires an even '
682 'number of arguments.')
683 for idx, rule in enumerate(rules):
684 if idx % 2:
685 continue
686 formats = rules[idx+1]
687 terminal = rule.startswith(TERMINAL(''))
688 if terminal:
689 rule = rule[len(TERMINAL('')):]
690 try:
691 regex = _RGX_CACHE[rule]
692 except KeyError:
693 regex = _RGX_CACHE[rule] = re.compile(rule)
694 self._rules.append((regex, formats, terminal,))
696 def formats(self, line):
697 matched = []
698 for regex, fmts, terminal in self._rules:
699 match = regex.match(line)
700 if not match:
701 continue
702 matched.append([match, fmts])
703 if terminal:
704 return matched
705 return matched
707 def mkformat(self, fg=None, bg=None, bold=False):
708 fmt = QtGui.QTextCharFormat()
709 if fg:
710 fmt.setForeground(fg)
711 if bg:
712 fmt.setBackground(bg)
713 if bold:
714 fmt.setFontWeight(QtGui.QFont.Bold)
715 return fmt
717 def highlightBlock(self, qstr):
718 if not self.enabled:
719 return
720 ascii = unicode(qstr)
721 if not ascii:
722 return
723 formats = self.formats(ascii)
724 if not formats:
725 return
726 for match, fmts in formats:
727 start = match.start()
728 groups = match.groups()
730 # No groups in the regex, assume this is a single rule
731 # that spans the entire line
732 if not groups:
733 self.setFormat(0, len(ascii), fmts)
734 continue
736 # Groups exist, rule is a tuple corresponding to group
737 for grpidx, group in enumerate(groups):
738 # allow empty matches
739 if not group:
740 continue
741 # allow None as a no-op format
742 length = len(group)
743 if fmts[grpidx]:
744 self.setFormat(start, start+length,
745 fmts[grpidx])
746 start += length
748 def set_colors(self, colordict):
749 for attr, val in colordict.items():
750 setattr(self, attr, val)
753 class DiffSyntaxHighlighter(GenericSyntaxHighligher):
754 """Implements the diff syntax highlighting
756 This class is used by widgets that display diffs.
759 def __init__(self, doc, whitespace=True):
760 self.whitespace = whitespace
761 GenericSyntaxHighligher.__init__(self, doc)
763 def generate_rules(self):
764 diff_head = self.mkformat(fg=self.color_header)
765 diff_head_bold = self.mkformat(fg=self.color_header, bold=True)
767 diff_add = self.mkformat(fg=self.color_text, bg=self.color_add)
768 diff_remove = self.mkformat(fg=self.color_text, bg=self.color_remove)
770 if self.whitespace:
771 bad_ws = self.mkformat(fg=Qt.black, bg=Qt.red)
773 # We specify the whitespace rule last so that it is
774 # applied after the diff addition/removal rules.
775 # The rules for the header
776 diff_old_rgx = TERMINAL(r'^--- ')
777 diff_new_rgx = TERMINAL(r'^\+\+\+ ')
778 diff_ctx_rgx = TERMINAL(r'^@@ ')
780 diff_hd1_rgx = TERMINAL(r'^diff --git a/.*b/.*')
781 diff_hd2_rgx = TERMINAL(r'^index \S+\.\.\S+')
782 diff_hd3_rgx = TERMINAL(r'^new file mode')
783 diff_hd4_rgx = TERMINAL(r'^deleted file mode')
784 diff_add_rgx = TERMINAL(r'^\+')
785 diff_rmv_rgx = TERMINAL(r'^-')
786 diff_bar_rgx = TERMINAL(r'^([ ]+.*)(\|[ ]+\d+[ ]+[+-]+)$')
787 diff_sts_rgx = (r'(.+\|.+?)(\d+)(.+?)([\+]*?)([-]*?)$')
788 diff_sum_rgx = (r'(\s+\d+ files changed[^\d]*)'
789 r'(:?\d+ insertions[^\d]*)'
790 r'(:?\d+ deletions.*)$')
792 self.create_rules(diff_old_rgx, diff_head,
793 diff_new_rgx, diff_head,
794 diff_ctx_rgx, diff_head_bold,
795 diff_bar_rgx, (diff_head_bold, diff_head),
796 diff_hd1_rgx, diff_head,
797 diff_hd2_rgx, diff_head,
798 diff_hd3_rgx, diff_head,
799 diff_hd4_rgx, diff_head,
800 diff_add_rgx, diff_add,
801 diff_rmv_rgx, diff_remove,
802 diff_sts_rgx, (None, diff_head,
803 None, diff_head,
804 diff_head),
805 diff_sum_rgx, (diff_head,
806 diff_head,
807 diff_head))
808 if self.whitespace:
809 self.create_rules('(..*?)(\s+)$', (None, bad_ws))
812 def install():
813 Interaction.critical = staticmethod(critical)
814 Interaction.confirm = staticmethod(confirm)
815 Interaction.question = staticmethod(question)
816 Interaction.information = staticmethod(information)