Merge lp:~waldner/terminator/plugin-ng into lp:terminator/gtk3

Proposed by Waldner
Status: Needs review
Proposed branch: lp:~waldner/terminator/plugin-ng
Merge into: lp:terminator/gtk3
Diff against target: 626 lines (+398/-94)
5 files modified
plugin-ng.md (+129/-0)
terminatorlib/plugin.py (+38/-2)
terminatorlib/terminal.py (+129/-70)
terminatorlib/terminal_popup_menu.py (+68/-21)
terminatorlib/terminator.py (+34/-1)
To merge this branch: bzr merge lp:~waldner/terminator/plugin-ng
Reviewer Review Type Date Requested Status
Terminator Pending
Review via email: mp+341537@code.launchpad.net

Commit message

New plugin framework "plugin_ng"

Description of the change

New plugin framework "plugin_ng". See https://bazaar.launchpad.net/~waldner/terminator/plugin-ng/view/head:/plugin-ng.md for a description.

To post a comment you must log in.
Revision history for this message
Waldner (waldner) wrote :

Is this too invasive? Bad style? All comments welcome.

Revision history for this message
Bryce Harrington (bryce) wrote :

I like the idea of generalizing the handling of selected patterns and being able to extend it to show different kinds of context menus. Conceptually seems quite interesting. Style looks fine to me, in fact it refactors things to look a bit cleaner. Needs a better name (I don't have suggestions myself, sorry), and I would like to see a couple use case/example ideas at the top of the docs (i.e. what are some useful kinds of things people could expect do with this functionality). A test/example plugin should be included. Those three things fixed I'd give this a +1.

Revision history for this message
Waldner (waldner) wrote :

For an example plugin, see https://github.com/waldner/terminator-runwith-plugin (needs the plugin-ng patched terminator, of course). There are some (admittedly simplistic and a bit silly) usage examples there as well.

I'll think about the name (suggestions welcome). What I use it for is to extract patterns from the terminal text (regex can include context, later thrown away, to avoid false matches) and use them as arguments to an external program or command. Think: you have a filename, and want to open it with your editor of choice. You have a github URL, and you want to run "git clone" with it. (more to be added)
Right-click on the element, runwith -> yourcommand and you're done. I'll try to come up with more use cases. Perhaps it's not a popular thing to do, but it definitely helped me.

Revision history for this message
Waldner (waldner) wrote :

Probably the most notable use case is the ability to create true context-sensitive menus, which was plain impossible with the old plugin system.

Unmerged revisions

1795. By Waldner

New plugin framework "plugin_ng".

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'plugin-ng.md'
2--- plugin-ng.md 1970-01-01 00:00:00 +0000
3+++ plugin-ng.md 2018-03-16 17:20:15 +0000
4@@ -0,0 +1,129 @@
5+#### plugin_ng
6+
7+- Better plugin support. The new system (called **plugin_ng** for lack of a better
8+ name) has the following features:
9+
10+ - A plugin can provide various services, and there is a callback for each (see below
11+ for the details)
12+ - A plugin can inject multiple patterns it's interested in (not just one as before),
13+ and when a match is present in the terminal, the plugin is told which of its
14+ matches it comes from, so it can take appropriate actions. This allows for eg the
15+ creation of true context-sensitive popup menus (show different entries depending
16+ on what's currently matching)
17+ - Backwards-compatible with old-style plugins (not at the API level, but the code
18+ paths that manage old-style plugins have been preserved)
19+
20+#### Getting started with plugin_ng plugins
21+
22+A plugin\_ng plugin must extend the `plugin.PluginNg` class and must declare the
23+`plugin_ng` capability, so it can be correctly loaded by the plugin registry. The
24+plugin must also declare its name in the `plugin_name` variable. Sample code:
25+
26+ # Every plugin you want Terminator to load *must* be listed
27+ # in 'AVAILABLE'
28+ AVAILABLE = [ 'MyNgPlugin' ]
29+
30+ class MyNgPlugin(plugin.PluginNg):
31+ capabilities = ['plugin_ng']
32+ plugin_name = 'my_ng_plugin' # must be unique
33+ ...
34+
35+Next, the plugin has to implement the callback methods for the services it wants to
36+provide, which might be all of them, although the plugin\_ng API was created
37+specifically to be able to have better control upon contextual popup menus (eg, right
38+click). Here are the available methods for the plugin\_ng callback API, in the order
39+they may be called by Terminator.
40+
41+##### `callback_get_matches(self)`
42+
43+This method gets called when Terminator wants to know the patterns (ie, regexes) that
44+this plugin wants to register. This method must return a dictionary containing
45+key/pattern pairs, which represent all the patterns (regular expressions) the plugin
46+wants to register. Keys must be unique, patterns are normal python regexes. In
47+particular, it's possible to use named capture groups in patterns, eg
48+`(?P<NAME>foobar)`, so when the plugin is called later and a match is present, parts
49+of the matched text can be extracted.
50+
51+Example:
52+
53+```
54+return { 'c_file': '\b(?<FILENAME>[^ ]+\.c)\b',
55+ 'py_file': '\b(?<FILENAME>[^ ]+\.py)\b' }
56+```
57+
58+When other callbacks are run, the plugin will be passed the matched text so it can
59+use `\g<FILENAME>` to extract the filename (this is just an example).
60+
61+##### `def callback_nameopen(self, matched_text, match_name)`
62+
63+This method is called by Terminator when the popup menu is about to be displayed and
64+the context has a match for a pattern provided by this plugin. The purpose of this call
65+is to give the plugin a chance to provide custom descriptions for the first two popup
66+menu entries (usually "Open"/"Copy", like for example "Open link", "Copy address" when
67+the menu pops up on a URI). The plugin is told which one of its registered patterns is
68+matching (via the `match_name` variable) and what the text that actually is producing
69+the match is (via the `matched_text` variable), so the plugin can take appropriate
70+actions. If implemented, the callback must return a `nameopen, namecopy` pair, for
71+example:
72+
73+```
74+return "Open file", "Copy filename"
75+```
76+
77+It is worth noting that these are just descriptions, and can in no way influence the
78+way Terminator will open or copy the link.
79+
80+
81+##### `def callback_popup_menu(self, menuitems, terminal, matched_text, match_name)`
82+
83+This callback is called by Terminator just before showing the popup menu, so the plugin
84+has a chance to add its own entries. If there is a match for the plugin, `matched_text`
85+and `match_name` can help the plugin determine which one it is. The `menuitems` argument
86+is provided by Terminator, and it's where the plugin must append its own entries. The
87+`terminal` argument represents the terminal in which the popup menu is being invoked; it
88+can be used to determine whether it's part of a terminal group and perhaps run the
89+command(s) associated with our menu entries in all terminals.
90+
91+Example:
92+
93+```
94+ ...
95+ my_command = [ 'ls', '-al', matched_text ]
96+ my_entry = Gtk.MenuItem(my_command)
97+
98+ terminals = terminal.terminator.get_target_terms(terminal)
99+
100+ my_entry.connect("activate", self.run_command, {'terminals' : terminals, 'command' : my_command})
101+ menuitems.append(my_entry)
102+
103+ ...
104+
105+def run_command(self, widget, data):
106+ for terminal in data['terminals']:
107+ command = ' '.join(data['command']) + '\n'
108+ terminal.vte.feed_child(command, len(command))
109+```
110+
111+
112+
113+##### `def callback_open_transform(self, matched_text, match_name)`
114+
115+This method is called by Terminator when the popup menu was shown and the user clicked
116+on the first menu entry (eg, "Open link" or "Open file" etc) and the current link
117+matches a pattern that has been registered by this plugin. In this method the plugin
118+has a chance to mangle the URL that will be eventually opened (but again, no way to set
119+the way it will be opened, unless it does more than what it's supposed to do).
120+As in other callbacks, the plugin is told which of its pattern has matched, so it can
121+take appropriate action. The URL that terminator should open must be returned (it can
122+also be the `matched_text` unchanged, though). Example:
123+
124+```
125+# matched_text is "#1234"
126+return "http://bugtracker.example.com/issue/%s" % matched_text[1:]
127+```
128+
129+For a complete plugin example, see the
130+[terminator-runwith-plugin](https://github.com/waldner/terminator-runwith-plugin)
131+project, which creates a context-sensitive menu to run arbitrary commands on arbitrary
132+text matches.
133+
134
135=== modified file 'terminatorlib/plugin.py'
136--- terminatorlib/plugin.py 2016-11-25 01:29:55 +0000
137+++ terminatorlib/plugin.py 2018-03-16 17:20:15 +0000
138@@ -106,7 +106,7 @@
139 self.instances[item] = func()
140 except Exception, ex:
141 err('PluginRegistry::load_plugins: Importing plugin %s \
142-failed: %s' % (plugin, ex))
143+ failed: %s' % (plugin, ex))
144
145 self.done = True
146
147@@ -114,7 +114,7 @@
148 """Return a list of plugins with a particular capability"""
149 result = []
150 dbg('PluginRegistry::get_plugins_by_capability: searching %d plugins \
151-for %s' % (len(self.instances), capability))
152+ for %s' % (len(self.instances), capability))
153 for plugin in self.instances:
154 if capability in self.instances[plugin].capabilities:
155 result.append(self.instances[plugin])
156@@ -190,3 +190,39 @@
157 """Callback to transform the enclosed URL"""
158 raise NotImplementedError
159
160+# PluginNg - Can add arbitrary number of regexes to the Terminal widget and provides a
161+# callback to run arbitrary actions
162+class PluginNg(Plugin):
163+ """Base class for Plugin NG"""
164+
165+ capabilities = ['plugin_ng']
166+ plugin_name = None
167+
168+ def __init__(self):
169+ """Class initialiser"""
170+ Plugin.__init__(self)
171+ terminator = Terminator()
172+ for terminal in terminator.terminals:
173+ terminal.pluginng_matches_add(self)
174+
175+ def callback_get_matches(self):
176+ return {}
177+
178+ def callback_open_transform(self, matched_text, match_name):
179+ """Callback to transform the matched text before opening it"""
180+ return None
181+
182+ def callback_nameopen(self, matched_text, match_name):
183+ """Callback to add custom open/copy description entries to popup menu"""
184+ return None, None
185+
186+ def callback_popup_menu(self, menuitems, terminal, matched_text, match_name):
187+ """Callback to edit the popup menu"""
188+ raise NotImplementedError
189+
190+ def unload(self):
191+ """Handle being removed"""
192+ terminator = Terminator()
193+ for terminal in terminator.terminals:
194+ terminal.pluginng_matches_remove(self)
195+
196
197=== modified file 'terminatorlib/terminal.py'
198--- terminatorlib/terminal.py 2017-06-24 02:02:38 +0000
199+++ terminatorlib/terminal.py 2018-03-16 17:20:15 +0000
200@@ -89,6 +89,7 @@
201 pid = None
202
203 matches = None
204+ ng_matches = None
205 regex_flags = None
206 config = None
207 default_encoding = None
208@@ -255,50 +256,26 @@
209
210 return(terminalbox)
211
212+ def get_match_name(self, match_tag):
213+
214+ for match in self.matches:
215+ if self.matches[match] == match_tag:
216+ return match
217+
218+ return None
219+
220+
221 def update_url_matches(self):
222- """Update the regexps used to match URLs"""
223- userchars = "-A-Za-z0-9"
224- passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
225- hostchars = "-A-Za-z0-9:\[\]"
226- pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'"
227- schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
228- user = "[" + userchars + "]+(:[" + passchars + "]+)?"
229- urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
230-
231- lboundry = "\\b"
232- rboundry = "\\b"
233-
234- re = (lboundry + schemes +
235- "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" +
236- urlpath + ")?" + rboundry + "/?")
237- reg = GLib.Regex.new(re, self.regex_flags, 0)
238- self.matches['full_uri'] = self.vte.match_add_gregex(reg, 0)
239-
240- if self.matches['full_uri'] == -1:
241+
242+ terminator = Terminator()
243+
244+ self.match_add('@builtin', 'full_uri', terminator.builtin_re['full_uri'])
245+
246+ if self.matches['@builtin']['full_uri'] == -1:
247 err ('Terminal::update_url_matches: Failed adding URL matches')
248 else:
249- re = (lboundry +
250- '(callto:|h323:|sip:)' + "[" + userchars + "+][" +
251- userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" +
252- rboundry)
253- reg = GLib.Regex.new(re, self.regex_flags, 0)
254- self.matches['voip'] = self.vte.match_add_gregex(reg, 0)
255- re = (lboundry +
256- "(www|ftp)[" + hostchars + "]*\.[" + hostchars +
257- ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
258- reg = GLib.Regex.new(re, self.regex_flags, 0)
259- self.matches['addr_only'] = self.vte.match_add_gregex(reg, 0)
260- re = (lboundry +
261- "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" +
262- "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" +
263- "[.a-zA-Z0-9-]*" + rboundry)
264- reg = GLib.Regex.new(re, self.regex_flags, 0)
265- self.matches['email'] = self.vte.match_add_gregex(reg, 0)
266- re = (lboundry +
267- """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" +
268- "[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry)
269- reg = GLib.Regex.new(re, self.regex_flags, 0)
270- self.matches['nntp'] = self.vte.match_add_gregex(reg, 0)
271+ for rex in [ 'voip', 'addr_only', 'email', 'nntp' ]:
272+ self.match_add('@builtin', rex, terminator.builtin_re[rex])
273
274 # Now add any matches from plugins
275 try:
276@@ -309,32 +286,85 @@
277 for urlplugin in plugins:
278 name = urlplugin.handler_name
279 match = urlplugin.match
280- if name in self.matches:
281- dbg('refusing to add duplicate match %s' % name)
282- continue
283- reg = GLib.Regex.new(match, self.regex_flags, 0)
284- self.matches[name] = self.vte.match_add_gregex(reg, 0)
285- dbg('added plugin URL handler for %s (%s) as %d' %
286- (name, urlplugin.__class__.__name__,
287- self.matches[name]))
288+ self.match_add(name, '@default', match, urlplugin.__class__.__name__)
289+
290 except Exception, ex:
291 err('Exception occurred adding plugin URL match: %s' % ex)
292
293- def match_add(self, name, match):
294- """Register a URL match"""
295- if name in self.matches:
296- err('Terminal::match_add: Refusing to create duplicate match %s' % name)
297+ # Matches for new-style plugins
298+ try:
299+ registry = plugin.PluginRegistry()
300+ registry.load_plugins()
301+ plugins = registry.get_plugins_by_capability('plugin_ng')
302+
303+ for ngplugin in plugins:
304+ self.pluginng_matches_add(ngplugin)
305+
306+ except Exception, ex:
307+ err('Exception occurred adding plugin NG match: %s' % ex)
308+
309+ def pluginng_matches_add(self, plugin):
310+
311+ for key, pattern in plugin.callback_get_matches().items():
312+
313+ if str.find(pattern, '@builtin%') != 0:
314+ self.match_add( plugin.plugin_name, key, pattern, plugin.plugin_name)
315+ else:
316+ # just copy the ID
317+ builtin_name = str.split(pattern, '%')[1]
318+ self.matches[plugin.plugin_name][key] = self.matches['@builtin'][builtin_name]
319+
320+
321+ def match_add(self, plugin_name, match_name, match, origin = None):
322+ """Register a match"""
323+ if plugin_name in self.matches and match_name in self.matches[plugin_name]:
324+ err('Terminal::match_add: Refusing to create duplicate match %s/%s' % (plugin_name, match_name))
325 return
326+
327+ if not plugin_name in self.matches:
328+ self.matches[plugin_name] = {}
329+
330 reg = GLib.Regex.new(match, self.regex_flags, 0)
331- self.matches[name] = self.vte.match_add_gregex(reg, 0)
332-
333- def match_remove(self, name):
334+ self.matches[plugin_name][match_name] = self.vte.match_add_gregex(reg, 0)
335+
336+ if origin:
337+ dbg('Terminal::match_add: added match regexp "%s" for %s/%s (%s) as %d' %
338+ (match, plugin_name, match_name, origin, self.matches[plugin_name][match_name]))
339+
340+ def pluginng_matches_remove(self, plugin):
341+ self.match_remove( plugin.plugin_name, plugin.plugin_name)
342+
343+ def match_remove(self, plugin_name, origin = None):
344 """Remove a previously registered URL match"""
345- if name not in self.matches:
346- err('Terminal::match_remove: Unable to remove non-existent match %s' % name)
347+ if plugin_name not in self.matches:
348+ err('Terminal::match_remove: Unable to remove non-existent match %s' % plugin_name)
349 return
350- self.vte.match_remove(self.matches[name])
351- del(self.matches[name])
352+
353+ for match_name in self.matches[plugin_name]:
354+
355+ if not self.is_builtin(self.matches[plugin_name][match_name]):
356+ self.vte.match_remove(self.matches[plugin_name][match_name])
357+
358+ if origin:
359+ dbg('Terminal::match_remove: removed match for %s/%s (%s) as %d' %
360+ (plugin_name, match_name, origin, self.matches[plugin_name][match_name]))
361+
362+ del(self.matches[plugin_name])
363+
364+ def is_builtin(self, match_id):
365+ for builtin_id in self.matches['@builtin'].keys():
366+ if builtin_id == match_id:
367+ return True
368+ return False
369+
370+ def match_inverse_lookup(self, match_index):
371+ for plugin_name in self.matches:
372+ if plugin_name == '@builtin':
373+ continue
374+ for match_name in self.matches[plugin_name]:
375+ if self.matches[plugin_name][match_name] == match_index:
376+ return plugin_name, match_name
377+ return None, None
378
379 def maybe_copy_clipboard(self):
380 if self.config['copy_on_selection'] and self.vte.get_has_selection():
381@@ -1447,30 +1477,59 @@
382 url = urlmatch[0]
383 match = urlmatch[1]
384
385- if match == self.matches['email'] and url[0:7] != 'mailto:':
386+ if match == self.matches['@builtin']['email'] and url[0:7] != 'mailto:':
387 url = 'mailto:' + url
388- elif match == self.matches['addr_only'] and url[0:3] == 'ftp':
389+ elif match == self.matches['@builtin']['addr_only'] and url[0:3] == 'ftp': # ????
390 url = 'ftp://' + url
391- elif match == self.matches['addr_only']:
392+ elif match == self.matches['@builtin']['addr_only']:
393 url = 'http://' + url
394- elif match in self.matches.values():
395+ else:
396 # We have a match, but it's not a hard coded one, so it's a plugin
397+
398+ handled = False
399+
400+ dbg("*************************** PREPARE_URL " + url + ", " + str(match))
401+ plugin_name, match_name = self.match_inverse_lookup(match)
402+
403 try:
404 registry = plugin.PluginRegistry()
405 registry.load_plugins()
406- plugins = registry.get_plugins_by_capability('url_handler')
407+ plugins = registry.get_plugins_by_capability('plugin_ng')
408
409- for urlplugin in plugins:
410- if match == self.matches[urlplugin.handler_name]:
411- newurl = urlplugin.callback(url)
412+ for ngplugin in plugins:
413+ if ngplugin.plugin_name == plugin_name:
414+ newurl = ngplugin.callback_open_transform(url, match_name)
415 if newurl is not None:
416- dbg('Terminal::prepare_url: URL prepared by \
417-%s plugin' % urlplugin.handler_name)
418+ dbg('Terminal::prepare_url: match prepared by \
419+%s ng-plugin (%s)' % (ngplugin.plugin_name, match_name))
420 url = newurl
421+ handled = True
422 break
423 except Exception, ex:
424 err('Exception occurred preparing URL: %s' % ex)
425
426+ if handled == False:
427+ try:
428+ registry = plugin.PluginRegistry()
429+ registry.load_plugins()
430+ plugins = registry.get_plugins_by_capability('url_handler')
431+
432+ for urlplugin in plugins:
433+
434+ if not urlplugin.handler_name in self.matches:
435+ continue
436+
437+ if match == self.matches[urlplugin.handler_name]['@default']:
438+ newurl = urlplugin.callback(url)
439+ if newurl is not None:
440+ dbg('Terminal::prepare_url: URL prepared by \
441+ %s URL plugin' % urlplugin.handler_name)
442+ url = newurl
443+ break
444+ except Exception, ex:
445+ err('Exception occurred preparing URL: %s' % ex)
446+
447+
448 return(url)
449
450 def open_url(self, url, prepare=False):
451
452=== modified file 'terminatorlib/terminal_popup_menu.py'
453--- terminatorlib/terminal_popup_menu.py 2017-02-19 15:57:36 +0000
454+++ terminatorlib/terminal_popup_menu.py 2018-03-16 17:20:15 +0000
455@@ -49,38 +49,58 @@
456 time = 0
457 button = 3
458
459+ all_match_values = [ e for value in terminal.matches for e in terminal.matches[value].values() ]
460+
461 if url and url[0]:
462 dbg("URL matches id: %d" % url[1])
463- if not url[1] in terminal.matches.values():
464+ if not url[1] in all_match_values:
465 err("Unknown URL match id: %d" % url[1])
466 dbg("Available matches: %s" % terminal.matches)
467
468 nameopen = None
469 namecopy = None
470- if url[1] == terminal.matches['email']:
471+ if url[1] == terminal.matches['@builtin']['email']:
472 nameopen = _('_Send email to...')
473 namecopy = _('_Copy email address')
474- elif url[1] == terminal.matches['voip']:
475+ elif url[1] == terminal.matches['@builtin']['voip']:
476 nameopen = _('Ca_ll VoIP address')
477 namecopy = _('_Copy VoIP address')
478- elif url[1] in terminal.matches.values():
479- # This is a plugin match
480- for pluginname in terminal.matches:
481- if terminal.matches[pluginname] == url[1]:
482- break
483-
484- dbg("Found match ID (%d) in terminal.matches plugin %s" %
485- (url[1], pluginname))
486- registry = plugin.PluginRegistry()
487- registry.load_plugins()
488- plugins = registry.get_plugins_by_capability('url_handler')
489- for urlplugin in plugins:
490- if urlplugin.handler_name == pluginname:
491- dbg("Identified matching plugin: %s" %
492- urlplugin.handler_name)
493- nameopen = _(urlplugin.nameopen)
494- namecopy = _(urlplugin.namecopy)
495- break
496+ else:
497+ # See whether some plugin provides custom open/copy description for menu entries
498+
499+ # NOTE: this assumed, and continues to assume, that only one plugin
500+ # can "own" the current match. Not sure this is entirely right.
501+
502+ plugin_name, match_name = terminal.match_inverse_lookup(url[1])
503+
504+ if plugin_name != None and match_name != None:
505+
506+ registry = plugin.PluginRegistry()
507+ registry.load_plugins()
508+
509+ if match_name == '@default':
510+ # old style URL plugin
511+ plugins = registry.get_plugins_by_capability('url_handler')
512+
513+ for urlplugin in plugins:
514+ if urlplugin.handler_name == plugin_name:
515+ dbg("Identified matching plugin: %s" %
516+ urlplugin.handler_name)
517+ nameopen = _(urlplugin.nameopen)
518+ namecopy = _(urlplugin.namecopy)
519+ break
520+ else:
521+ # NG plugin
522+ plugins = registry.get_plugins_by_capability('plugin_ng')
523+
524+ for ngplugin in plugins:
525+ if ngplugin.plugin_name == plugin_name:
526+ dbg("Identified matching plugin: %s" %
527+ ngplugin.plugin_name)
528+ nameopen, namecopy = ngplugin.callback_nameopen(url[0], match_name)
529+ nameopen = _(nameopen)
530+ namecopy = _(namecopy)
531+ break
532
533 if not nameopen:
534 nameopen = _('_Open link')
535@@ -215,7 +235,34 @@
536
537 self.add_encoding_items(menu)
538
539+ # multiple plugins can all add their entries at once
540 try:
541+
542+ registry = plugin.PluginRegistry()
543+ registry.load_plugins()
544+
545+ menuitems = []
546+ plugins = registry.get_plugins_by_capability('plugin_ng')
547+
548+ if url and url[0]:
549+ plugin_name, match_name = terminal.match_inverse_lookup(url[1])
550+ matched_text = url[0]
551+ else:
552+ plugin_name, match_name = None, None
553+ matched_text = None
554+
555+ for ngplugin in plugins:
556+ if ngplugin.plugin_name == plugin_name:
557+ ngplugin.callback_popup_menu(menuitems, terminal, matched_text, match_name)
558+ else:
559+ ngplugin.callback_popup_menu(menuitems, terminal, matched_text, None)
560+
561+ if len(menuitems) > 0:
562+ menu.append(Gtk.SeparatorMenuItem())
563+
564+ for menuitem in menuitems:
565+ menu.append(menuitem)
566+
567 menuitems = []
568 registry = plugin.PluginRegistry()
569 registry.load_plugins()
570
571=== modified file 'terminatorlib/terminator.py'
572--- terminatorlib/terminator.py 2017-02-28 19:48:11 +0000
573+++ terminatorlib/terminator.py 2018-03-16 17:20:15 +0000
574@@ -65,11 +65,13 @@
575 cur_gtk_theme_name = None
576 gtk_settings = None
577
578+ builtin_re = None
579+
580 def __init__(self):
581 """Class initialiser"""
582-
583 Borg.__init__(self, self.__class__.__name__)
584 self.prepare_attributes()
585+ self.create_builtin_re()
586
587 def prepare_attributes(self):
588 """Initialise anything that isn't already"""
589@@ -99,6 +101,37 @@
590 self.attempt_gnome_client()
591 self.connect_signals()
592
593+ def create_builtin_re(self):
594+
595+
596+ if self.builtin_re:
597+ return
598+
599+ # built-in RE matches
600+ self.builtin_re = {}
601+
602+ userchars = "-A-Za-z0-9"
603+ passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
604+ hostchars = "-A-Za-z0-9:\[\]"
605+ pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'"
606+ schemes = "(news:|telnet:|nntp:|file:/|https?:|ftps?:|webcal:)"
607+ user = "[" + userchars + "]+(:[" + passchars + "]+)?"
608+ urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
609+
610+ lboundry = "\\b"
611+ rboundry = "\\b"
612+
613+ self.builtin_re['full_uri'] = (lboundry + schemes + "//(" + user + "@)?[" + hostchars +".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
614+ self.builtin_re['voip'] = (lboundry + '(callto:|h323:|sip:)' + "[" + userchars + "+][" + userchars + ".]*(:[0-9]+)?@?[" + pathchars + "]+" + rboundry)
615+ self.builtin_re['addr_only'] = (lboundry + "(www|ftp)[" + hostchars + "]*\.[" + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?" + rboundry + "/?")
616+ self.builtin_re['email'] = (lboundry + "(mailto:)?[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9]" + "[a-zA-Z0-9-]*\.[a-zA-Z0-9][a-zA-Z0-9-]+" + "[.a-zA-Z0-9-]*" + rboundry)
617+ self.builtin_re['nntp'] = (lboundry + """news:[-A-Z\^_a-z{|}~!"#$%&'()*+,./0-9;:=?`]+@""" + "[-A-Za-z0-9.]+(:[0-9]+)?" + rboundry)
618+
619+ #for name in [ 'full_uri', 'voip', 'addr_only', 'email', 'nntp' ]:
620+ # print "%s: %s" % (name, self.builtin_re[name])
621+
622+
623+
624 def connect_signals(self):
625 """Connect all the gtk signals"""
626 self.gtk_settings=Gtk.Settings().get_default()

Subscribers

People subscribed via source and target branches

to status/vote changes: