Merge lp:~waldner/terminator/plugin-ng into lp:terminator/gtk3
- plugin-ng
- Merge into gtk3
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 |
Related bugs: |
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:/
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.
Waldner (waldner) wrote : | # |
For an example plugin, see https:/
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.
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.
Preview Diff
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() |
Is this too invasive? Bad style? All comments welcome.