Merge lp:~dcaro/clicompanion/fix-923535 into lp:clicompanion

Proposed by David Caro
Status: Merged
Approved by: Marek Bardoński
Approved revision: 112
Merged at revision: 108
Proposed branch: lp:~dcaro/clicompanion/fix-923535
Merge into: lp:clicompanion
Prerequisite: lp:~dcaro/clicompanion/fix-623475
Diff against target: 382 lines (+247/-10)
7 files modified
MANIFEST (+2/-0)
clicompanionlib/config.py (+1/-1)
clicompanionlib/plugins.py (+41/-1)
clicompanionlib/tabs.py (+48/-7)
clicompanionlib/view.py (+2/-1)
plugins/LaunchpadURL.py (+62/-0)
plugins/StandardURLs.py (+91/-0)
To merge this branch: bzr merge lp:~dcaro/clicompanion/fix-923535
Reviewer Review Type Date Requested Status
Marek Bardoński Approve
Review via email: mp+90633@code.launchpad.net

Description of the change

Added the urls plugins to allow the user to click on urls to open them, added the standard (http/https/ftp/ftps/file/news/sip/etc.) and launchpad (lp:bugcode or lp:branch) to the pluginslist.

To post a comment you must log in.
lp:~dcaro/clicompanion/fix-923535 updated
111. By David Caro "<email address hidden>"

added new url plugins

Revision history for this message
Marek Bardoński (bdfhjk) wrote :

Hi David!

Sorry for long time before review, I have much work with preparation for very important for me contest.

I have applied your path, but I can't see how it work. After typing http://www.google.com in terminal and click [enter], it isn't appear as link and isn't clickable.

Please explain, how your path should work - and thank you for working on it!

Marek

review: Needs Information
Revision history for this message
David Caro (dcaro) wrote :

You have to go to the plugins configuration and enable them first (that's because the config file is not overwrited if it exists).

So for this to work, just go to Preferences -> Plugins and double click the checkbox of the StandardURLs plugin and LaunchpadURL plugin. After this (inmediatly, you don't need to restart) when you pass the mouse over a recognized url, it will be underscored, in that moment you can press ctrl+click and that will open a tab/window of your preferred browser.

I hope you like it!

Revision history for this message
Marek Bardoński (bdfhjk) wrote :

OK, thanks for explain. I will try it now.

Revision history for this message
Marek Bardoński (bdfhjk) wrote :

I found small bugs:

After typing lp:clicompanion and pressing [enter], then created link is '
https://code.launchpad.net/+branch/clicompanion:%20nie%20znaleziono%20poleceniauser@user-GX780-GT780-GT780DX:~/clicompanion/clicompanion$%20%5EC'
created from underlined ' lp:clicompanion: nie znaleziono polecenia \n
user@user-GX780-GT780-GT780DX:~/clicompanion/clicompanion'.

After typing lp:12312 and pressing [enter], I have two links - one correct
to bug, and one '
https://code.launchpad.net/+branch/12122:%20nie%20znaleziono%20poleceniauser@user-GX780-GT780-GT780DX:~/clicompanion/clicompanion$%20%5EC
'

lp:~dcaro/clicompanion/fix-923535 updated
112. By David Caro "<email address hidden>"

The url matching regexp was way too permissive, and incorrect paths were
matched

Revision history for this message
David Caro (dcaro) wrote :

Fixed, the regexp matching launchpad branches was too permissive and false positives were matched. It should be fixed now (it's a lot more strict, so maybe real urls that have some strange chars in them are not matched correctly, but now most of urls will match).

Also you don't need to hit enter for the url to get underscored ;)

Revision history for this message
Marek Bardoński (bdfhjk) wrote :

2012/3/4 David Caro <email address hidden>

> Fixed, the regexp matching launchpad branches was too permissive and false
> positives were matched. It should be fixed now (it's a lot more strict, so
> maybe real urls that have some strange chars in them are not matched
> correctly, but now most of urls will match).
>
> Also you don't need to hit enter for the url to get underscored ;)
>
> --
> https://code.launchpad.net/~dcaro/clicompanion/fix-923535/+merge/90633<https://code.launchpad.net/%7Edcaro/clicompanion/fix-923535/+merge/90633>
> You are reviewing the proposed merge of lp:~dcaro/clicompanion/fix-923535
> into lp:clicompanion.
>

OK, I will test it soon.

Revision history for this message
Marek Bardoński (bdfhjk) wrote :

Tested - problems solved.

Thanks for implementing nice feature!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MANIFEST'
2--- MANIFEST 2012-01-08 01:37:38 +0000
3+++ MANIFEST 2012-03-04 09:53:18 +0000
4@@ -17,4 +17,6 @@
5 plugins/CommandLineFU.py
6 plugins/__init__.py
7 plugins/LocalCommandList.py
8+plugins/StandardURLs.py
9+plugins/LaunchpadURL.py
10
11
12=== modified file 'clicompanionlib/config.py'
13--- clicompanionlib/config.py 2012-01-15 14:23:05 +0000
14+++ clicompanionlib/config.py 2012-03-04 09:53:18 +0000
15@@ -60,7 +60,7 @@
16 "update_login_records": ("True", 'bool'),
17 },
18 'general': {"debug": ('False', 'bool'),
19- "plugins": ('LocalCommandList, CommandLineFU', 'str')
20+ "plugins": ('LocalCommandList, CommandLineFU, StandardURLs', 'str')
21 },
22 'LocalCommandList': {"cheatsheet":
23 (os.path.expanduser("~/.clicompanion2"), 'str'),
24
25=== modified file 'clicompanionlib/plugins.py'
26--- clicompanionlib/plugins.py 2012-01-15 14:23:05 +0000
27+++ clicompanionlib/plugins.py 2012-03-04 09:53:18 +0000
28@@ -47,6 +47,7 @@
29 import sys
30 import os
31 import inspect
32+import webbrowser
33 from clicompanionlib.utils import dbg
34
35
36@@ -97,7 +98,7 @@
37 if capability in capabilities \
38 and plugin in self.allowed:
39 plugins.append((plugin, pclass))
40- dbg('Matching plugin %s for %s' % (plugin, capability))
41+ dbg('Matching plugin %s for capability %s' % (plugin, capability))
42 return plugins
43
44 def get_plugin_conf(self, plugin):
45@@ -207,3 +208,42 @@
46 def __init__(self, config):
47 Plugin.__init__(self)
48 self.config = config
49+
50+class URLPlugin(Plugin):
51+ '''
52+ Plugion that matches an url in the screen and executes some action.
53+ '''
54+ __capabilities__ = [ 'URL' ]
55+
56+
57+ def __init__(self, config):
58+ Plugin.__init__(self)
59+ self.config = config
60+ ## This is the regexp that will trigger the callback
61+ matches = ['']
62+
63+ def callback(self, url, matchnum):
64+ ## When the regexp is found, this function will be called
65+ pass
66+
67+ def open_url(self, url):
68+ """
69+ Open a given URL, generic for all the URL plugins to use
70+ """
71+ oldstyle = False
72+ if gtk.gtk_version < (2, 14, 0) or \
73+ not hasattr(gtk, 'show_uri') or \
74+ not hasattr(gtk.gdk, 'CURRENT_TIME'):
75+ oldstyle = True
76+ if not oldstyle:
77+ try:
78+ gtk.show_uri(None, url, gtk.gdk.CURRENT_TIME)
79+ except:
80+ oldstyle = True
81+ if oldstyle:
82+ dbg('Old gtk (%s,%s,%s), calling xdg-open' % gtk.gtk_version)
83+ try:
84+ subprocess.Popen(["xdg-open", url])
85+ except:
86+ dbg('xdg-open did not work, falling back to webbrowser.open')
87+ webbrowser.open(url)
88
89=== modified file 'clicompanionlib/tabs.py'
90--- clicompanionlib/tabs.py 2012-01-15 20:40:56 +0000
91+++ clicompanionlib/tabs.py 2012-03-04 09:53:18 +0000
92@@ -42,6 +42,7 @@
93 import clicompanionlib.utils as cc_utils
94 import clicompanionlib.helpers as cc_helpers
95 import clicompanionlib.preferences as cc_pref
96+import clicompanionlib.config as cc_conf
97
98
99 class TerminalTab(gtk.ScrolledWindow):
100@@ -56,12 +57,15 @@
101 ()),
102 }
103
104- def __init__(self, title, config, profile='default', directory=None):
105+ def __init__(self, title, config, profile='default', directory=None,
106+ pluginloader=None):
107 gtk.ScrolledWindow.__init__(self)
108 self.config = config
109+ self.pluginloader = pluginloader
110 self.title = title
111 self.profile = 'profile::' + profile
112 self.vte = vte.Terminal()
113+ self.matches = {}
114 self.add(self.vte)
115 self.vte.connect("child-exited", lambda *x: self.emit('quit'))
116 self.update_records = self.config.getboolean(self.profile,
117@@ -78,7 +82,7 @@
118 logutmp=self.update_records,
119 logwtmp=self.update_records,
120 loglastlog=self.update_records)
121- self.vte.connect("button_press_event", self.copy_paste_menu)
122+ self.vte.connect("button_press_event", self.on_click)
123 self.update_config()
124 self.show_all()
125
126@@ -205,8 +209,34 @@
127
128 self.vte.set_allow_bold(config.getboolean(self.profile, 'bold_text'))
129 self.vte.set_word_chars(config.get(self.profile, 'sel_word'))
130-
131- def copy_paste_menu(self, vte, event):
132+ self.vte.match_clear_all()
133+ self.load_url_plugins()
134+
135+ def check_for_match(self, event):
136+ """
137+ Check if the mouse is over a URL
138+ """
139+ return (self.vte.match_check(int(event.x / self.vte.get_char_width()),
140+ int(event.y / self.vte.get_char_height())))
141+
142+ def run_match_callback(self, match):
143+ url = match[0]
144+ match = match[1]
145+ for plg, m_plg in self.matches.items():
146+ if match in m_plg[1]:
147+ dbg('Matched %s for url %s' % (plg, url))
148+ matchnum = m_plg[1].index(match)
149+ m_plg[0].callback(url, matchnum)
150+
151+ def on_click(self, vte, event):
152+ ## left click
153+ if event.button == 1:
154+ # Ctrl+leftclick on a URL should open it
155+ if event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK:
156+ match = self.check_for_match(event)
157+ if match:
158+ self.run_match_callback(match)
159+ ## Rght click menu
160 if event.button == 3:
161 time = event.time
162 ## right-click popup menu Copy
163@@ -366,6 +396,14 @@
164 self.profile = 'profile::' + profile
165 self.update_config()
166
167+ def load_url_plugins(self):
168+ for pg_name, pg_class in self.pluginloader.get_plugins(['URL']):
169+ pg_conf = cc_conf.CLIConfigView(pg_name, self.config)
170+ self.matches[pg_name] = (pg_class(pg_conf), [])
171+ for match in self.matches[pg_name][0].matches:
172+ dbg('Adding match %s for plugin %s' % (match, pg_name))
173+ self.matches[pg_name][1].append(self.vte.match_add(match))
174+
175
176 class TerminalsNotebook(gtk.Notebook):
177 __gsignals__ = {
178@@ -375,11 +413,12 @@
179 ()),
180 }
181
182- def __init__(self, config):
183+ def __init__(self, config, pluginloader):
184 gtk.Notebook.__init__(self)
185 #definition gcp - global page count, how many pages have been created
186 self.gcp = 0
187 self.global_config = config
188+ self.pluginloader = pluginloader
189 ## The "Add Tab" tab
190 add_tab_button = gtk.Button("+")
191 ## tooltip for "Add Tab" tab
192@@ -428,9 +467,11 @@
193 current_page = self.get_nth_page(self.get_current_page())
194 cwd = cc_utils.get_pid_cwd(current_page.pid)
195 if cwd:
196- newtab = TerminalTab(title, self.global_config, directory=cwd)
197+ newtab = TerminalTab(title, self.global_config, directory=cwd,
198+ pluginloader=self.pluginloader)
199 else:
200- newtab = TerminalTab(title, self.global_config)
201+ newtab = TerminalTab(title, self.global_config,
202+ pluginloader=self.pluginloader)
203 label = self.create_tab_label(title, newtab)
204 self.insert_page(newtab, label, self.get_n_pages() - 1)
205 self.set_current_page(self.get_n_pages() - 2)
206
207=== modified file 'clicompanionlib/view.py'
208--- clicompanionlib/view.py 2012-01-28 19:38:19 +0000
209+++ clicompanionlib/view.py 2012-03-04 09:53:18 +0000
210@@ -223,7 +223,8 @@
211
212 ## set various parameters on the main window (size, etc)
213 self.init_config()
214- self.term_notebook = cc_tabs.TerminalsNotebook(self.config)
215+ self.term_notebook = cc_tabs.TerminalsNotebook(self.config,
216+ self.pluginloader)
217
218 ###########################
219 #### Here we create the commands notebook for the expander
220
221=== added file 'plugins/LaunchpadURL.py'
222--- plugins/LaunchpadURL.py 1970-01-01 00:00:00 +0000
223+++ plugins/LaunchpadURL.py 2012-03-04 09:53:18 +0000
224@@ -0,0 +1,62 @@
225+#!/usr/bin/env python
226+# -*- coding: utf-8 -*-
227+#
228+# LaunchpadURL.py - URL plugin for launchpad bugs, repos and code
229+#
230+# Copyright 2012 David Caro <david.caro.estevez@gmail.com>
231+#
232+# This program is free software: you can redistribute it and/or modify it
233+# under the terms of the GNU General Public License version 3, as published
234+# by the Free Software Foundation.
235+#
236+# This program is distributed in the hope that it will be useful, but
237+# WITHOUT ANY WARRANTY; without even the implied warranties of
238+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
239+# PURPOSE. See the GNU General Public License for more details.
240+#
241+# You should have received a copy of the GNU General Public License along
242+# with this program. If not, see <http://www.gnu.org/licenses/>.
243+#
244+
245+
246+import pygtk
247+pygtk.require('2.0')
248+import gobject
249+import webbrowser
250+
251+try:
252+ import gtk
253+except:
254+ ## do not use gtk, just print
255+ print _("You need to install the python gtk bindings package"
256+ "'python-gtk2'")
257+ sys.exit(1)
258+
259+from clicompanionlib.utils import dbg
260+import clicompanionlib.plugins as plugins
261+
262+
263+class LaunchpadURL(plugins.URLPlugin):
264+ '''
265+ Match launchpad urls and open them on the browser
266+ '''
267+ __authors__ = 'David Caro <david.caro.estevez@gmail.com>'
268+ __info__ = ('This plugins enables launchpad urls to be matched.')
269+ __title__ = 'Launchpad URLS'
270+
271+ pathchars = "-A-Za-z0-9_.+?/~#"
272+ nonums = "-A-Za-z_.+?/~#"
273+
274+ def __init__(self, config):
275+ plugins.URLPlugin.__init__(self, config)
276+ self.matches = ['lp:[0-9]+',
277+ 'lp:[' + self.pathchars + ']*[' \
278+ + self.nonums + '][' + self.pathchars + ']*']
279+
280+ def callback(self, url, matchnum):
281+ dbg('Openeing launchpad url ' + url)
282+ if matchnum == 0:
283+ url = 'http://bugs.launchpad.net/bugs/' + url[3:]
284+ else:
285+ url = 'http://code.launchpad.net/+branch/' + url[3:]
286+ self.open_url(url)
287
288=== added file 'plugins/StandardURLs.py'
289--- plugins/StandardURLs.py 1970-01-01 00:00:00 +0000
290+++ plugins/StandardURLs.py 2012-03-04 09:53:18 +0000
291@@ -0,0 +1,91 @@
292+#!/usr/bin/env python
293+# -*- coding: utf-8 -*-
294+#
295+# StandardURLs.py - URL plugin for common urls (http, ftp, etc.)
296+#
297+# Copyright 2012 David Caro <david.caro.estevez@gmail.com>
298+#
299+# This program is free software: you can redistribute it and/or modify it
300+# under the terms of the GNU General Public License version 3, as published
301+# by the Free Software Foundation.
302+#
303+# This program is distributed in the hope that it will be useful, but
304+# WITHOUT ANY WARRANTY; without even the implied warranties of
305+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
306+# PURPOSE. See the GNU General Public License for more details.
307+#
308+# You should have received a copy of the GNU General Public License along
309+# with this program. If not, see <http://www.gnu.org/licenses/>.
310+#
311+
312+
313+import pygtk
314+pygtk.require('2.0')
315+import gobject
316+import webbrowser
317+
318+try:
319+ import gtk
320+except:
321+ ## do not use gtk, just print
322+ print _("You need to install the python gtk bindings package"
323+ "'python-gtk2'")
324+ sys.exit(1)
325+
326+from clicompanionlib.utils import dbg
327+import clicompanionlib.plugins as plugins
328+
329+
330+class StandardURLs(plugins.URLPlugin):
331+ '''
332+ Match launchpad urls and open them on the browser
333+ '''
334+ __authors__ = 'David Caro <david.caro.estevez@gmail.com>'
335+ __info__ = ('This plugins enables some common urls to be matched.')
336+ __title__ = 'Standard URLS'
337+
338+ def __init__(self, config):
339+ plugins.URLPlugin.__init__(self, config)
340+ self.matches = []
341+
342+ userchars = "-A-Za-z0-9"
343+ passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
344+ hostchars = "-A-Za-z0-9"
345+ pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\""
346+ schemes = ("(news:|telnet:|nntp:|https?:|ftps?:|webcal:)//")
347+ user = "([" + userchars + "]+(:[" + passchars + "]+)?)?"
348+ urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
349+ email = ("[a-zA-Z0-9][a-zA-Z0-9.+-]*@[a-zA-Z0-9][a-zA-Z0-9-]*"
350+ "\.[a-zA-Z0-9][a-zA-Z0-9-]+[.a-zA-Z0-9-]*")
351+
352+ lboundry = "\\<"
353+ rboundry = "\\>"
354+
355+ ## http/https/ftp/ftps/webcal/nntp/telnet urls
356+ self.matches.append(schemes + user + "[" + hostchars + "]*\.["
357+ + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?")
358+ ## file
359+ self.matches.append('file:///[' + pathchars + "]*")
360+ ## SIP
361+ self.matches.append('(callto:|h323:|sip:)'
362+ + "[" + userchars + "+]["
363+ + userchars + ".]*(:[0-9]+)?@?["
364+ + pathchars + "]+"
365+ + rboundry)
366+ ## mail
367+ self.matches.append("(mailto:)?" + email)
368+ ## news
369+ self.matches.append('news:[-A-Z\^_a-z{|}~!"#$%&\'()*+'
370+ + ',./0-9;:=?`]+@' + "[-A-Za-z0-9.]+(:[0-9]+)?")
371+ ## General url (www.host.com or ftp.host.com)
372+ self.matches.append("(www|ftp)[" + hostchars + "]*\.["
373+ + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?/?")
374+
375+ def callback(self, url, matchnum):
376+ dbg('Opening common url ' + url)
377+ if matchnum == 5:
378+ if url[:3] == 'www':
379+ url = 'http://' + url
380+ else:
381+ url = 'ftp://' + url
382+ self.open_url(url)

Subscribers

People subscribed via source and target branches