← Back to team overview

clicompanion-devs team mailing list archive

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

 

David Caro has proposed merging lp:~dcaro/clicompanion/fix-923535 into lp:clicompanion with lp:~dcaro/clicompanion/fix-623475 as a prerequisite.

Requested reviews:
  CLI Companion Development Team (clicompanion-devs)
Related bugs:
  Bug #923535 in CLI Companion: "Make urls clickable (and extendible with plugins)"
  https://bugs.launchpad.net/clicompanion/+bug/923535

For more details, see:
https://code.launchpad.net/~dcaro/clicompanion/fix-923535/+merge/90633

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.

-- 
https://code.launchpad.net/~dcaro/clicompanion/fix-923535/+merge/90633
Your team CLI Companion Development Team is requested to review the proposed merge of lp:~dcaro/clicompanion/fix-923535 into lp:clicompanion.
=== modified file 'clicompanionlib/plugins.py'
--- clicompanionlib/plugins.py	2012-01-29 23:59:23 +0000
+++ clicompanionlib/plugins.py	2012-01-29 23:59:23 +0000
@@ -47,6 +47,7 @@
 import sys
 import os
 import inspect
+import webbrowser
 from clicompanionlib.utils import dbg
 
 
@@ -97,7 +98,7 @@
                 if capability in capabilities \
                 and plugin in self.allowed:
                     plugins.append((plugin, pclass))
-                    dbg('Matching plugin %s for %s' % (plugin, capability))
+                    dbg('Matching plugin %s for capability %s' % (plugin, capability))
         return plugins
 
     def get_plugin_conf(self, plugin):
@@ -207,3 +208,42 @@
     def __init__(self, config):
         Plugin.__init__(self)
         self.config = config
+
+class URLPlugin(Plugin):
+    '''
+    Plugion that matches an url in the screen and executes some action.
+    '''
+    __capabilities__ = [ 'URL' ]
+
+
+    def __init__(self, config):
+        Plugin.__init__(self)
+        self.config = config
+        ## This is the regexp that will trigger the callback
+        matches = ['']
+
+    def callback(self, url, matchnum):
+        ## When the regexp is found, this function will be called
+        pass
+
+    def open_url(self, url):
+        """
+        Open a given URL, generic for all the URL plugins to use
+        """
+        oldstyle = False
+        if gtk.gtk_version < (2, 14, 0) or \
+           not hasattr(gtk, 'show_uri') or \
+           not hasattr(gtk.gdk, 'CURRENT_TIME'):
+            oldstyle = True
+        if not oldstyle:
+            try:
+                gtk.show_uri(None, url, gtk.gdk.CURRENT_TIME)
+            except:
+                oldstyle = True
+        if oldstyle:
+            dbg('Old gtk (%s,%s,%s), calling xdg-open' % gtk.gtk_version)
+            try:
+                subprocess.Popen(["xdg-open", url])
+            except:
+                dbg('xdg-open did not work, falling back to webbrowser.open')
+                webbrowser.open(url)

=== modified file 'clicompanionlib/tabs.py'
--- clicompanionlib/tabs.py	2012-01-29 23:59:23 +0000
+++ clicompanionlib/tabs.py	2012-01-29 23:59:23 +0000
@@ -56,12 +56,15 @@
                              ()),
     }
 
-    def __init__(self, title, config, profile='default', directory=None):
+    def __init__(self, title, config, profile='default', directory=None,
+                    pluginloader=None):
         gtk.ScrolledWindow.__init__(self)
         self.config = config
+        self.pluginloader = pluginloader
         self.title = title
         self.profile = 'profile::' + profile
         self.vte = vte.Terminal()
+        self.matches = {}
         self.add(self.vte)
         self.vte.connect("child-exited", lambda *x: self.emit('quit'))
         self.update_records = self.config.getboolean(self.profile,
@@ -78,8 +81,9 @@
                                          logutmp=self.update_records,
                                          logwtmp=self.update_records,
                                          loglastlog=self.update_records)
-        self.vte.connect("button_press_event", self.copy_paste_menu)
+        self.vte.connect("button_press_event", self.on_click)
         self.update_config()
+        self.load_url_plugins()
         self.show_all()
 
     def update_config(self, config=None, preview=False):
@@ -206,7 +210,31 @@
         self.vte.set_allow_bold(config.getboolean(self.profile, 'bold_text'))
         self.vte.set_word_chars(config.get(self.profile, 'sel_word'))
 
-    def copy_paste_menu(self, vte, event):
+    def check_for_match(self, event):
+        """
+        Check if the mouse is over a URL
+        """
+        return (self.vte.match_check(int(event.x / self.vte.get_char_width()),
+            int(event.y / self.vte.get_char_height())))
+
+    def run_match_callback(self, match):
+        url = match[0]
+        match = match[1]
+        for plg, m_plg in self.matches.items():
+            if match in m_plg[1]:
+                dbg('Matched %s for url %s' % (plg, url))
+                matchnum = m_plg[1].index(match)
+                m_plg[0].callback(url, matchnum)
+
+    def on_click(self, vte, event):
+        ## left click
+        if event.button == 1:
+            # Ctrl+leftclick on a URL should open it
+            if event.state & gtk.gdk.CONTROL_MASK == gtk.gdk.CONTROL_MASK:
+                match = self.check_for_match(event)
+                if match:
+                    self.run_match_callback(match)
+        ## Rght click menu
         if event.button == 3:
             time = event.time
             ## right-click popup menu Copy
@@ -366,6 +394,13 @@
         self.profile = 'profile::' + profile
         self.update_config()
 
+    def load_url_plugins(self):
+        for pg_name, pg_class in self.pluginloader.get_plugins(['URL']):
+            self.matches[pg_name] = (pg_class(self.config), [])
+            for match in self.matches[pg_name][0].matches:
+                dbg('Adding match %s for plugin %s' % (match, pg_name))
+                self.matches[pg_name][1].append(self.vte.match_add(match))
+
 
 class TerminalsNotebook(gtk.Notebook):
     __gsignals__ = {
@@ -375,11 +410,12 @@
                              ()),
     }
 
-    def __init__(self, config):
+    def __init__(self, config, pluginloader):
         gtk.Notebook.__init__(self)
         #definition gcp - global page count, how many pages have been created
         self.gcp = 0
         self.global_config = config
+        self.pluginloader = pluginloader
         ## The "Add Tab" tab
         add_tab_button = gtk.Button("+")
         ## tooltip for "Add Tab" tab
@@ -428,9 +464,11 @@
             current_page = self.get_nth_page(self.get_current_page())
             cwd = cc_utils.get_pid_cwd(current_page.pid)
         if cwd:
-            newtab = TerminalTab(title, self.global_config, directory=cwd)
+            newtab = TerminalTab(title, self.global_config, directory=cwd,
+                                    pluginloader=self.pluginloader)
         else:
-            newtab = TerminalTab(title, self.global_config)
+            newtab = TerminalTab(title, self.global_config,
+                                    pluginloader=self.pluginloader)
         label = self.create_tab_label(title, newtab)
         self.insert_page(newtab, label, self.get_n_pages() - 1)
         self.set_current_page(self.get_n_pages() - 2)

=== modified file 'clicompanionlib/view.py'
--- clicompanionlib/view.py	2012-01-29 23:59:23 +0000
+++ clicompanionlib/view.py	2012-01-29 23:59:23 +0000
@@ -223,7 +223,8 @@
 
         ## set various parameters on the main window (size, etc)
         self.init_config()
-        self.term_notebook = cc_tabs.TerminalsNotebook(self.config)
+        self.term_notebook = cc_tabs.TerminalsNotebook(self.config,
+                                                        self.pluginloader)
 
         ###########################
         #### Here we create the commands notebook for the expander

=== added file 'plugins/LaunchpadURL.py'
--- plugins/LaunchpadURL.py	1970-01-01 00:00:00 +0000
+++ plugins/LaunchpadURL.py	2012-01-29 23:59:23 +0000
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# LaunchpadURL.py - URL plugin for launchpad bugs, repos and code
+#
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import pygtk
+pygtk.require('2.0')
+import gobject
+import webbrowser
+
+try:
+    import gtk
+except:
+    ## do not use gtk, just print
+    print _("You need to install the python gtk bindings package"
+            "'python-gtk2'")
+    sys.exit(1)
+
+from clicompanionlib.utils import dbg
+import clicompanionlib.plugins as plugins
+
+
+class LaunchpadURL(plugins.URLPlugin):
+    '''
+    Match launchpad urls and open them on the browser
+    '''
+    __authors__ = 'David Caro <david.caro.estevez@xxxxxxxxx>'
+    __info__ = ('This plugins enables launchpad urls to be matched.')
+    __title__ = 'Launchpad URLS'
+
+    def __init__(self, config):
+        plugins.URLPlugin.__init__(self, config)
+        self.matches = ['lp:[0-9]+',
+                        'lp:.*']
+
+    def callback(self, url, matchnum):
+        dbg('Openeing launchpad url ' + url)
+        if matchnum == 0:
+            url = 'http://bugs.launchpad.net/bugs/' + url[3:]
+        else:
+            url = 'http://code.launchpad.net/+branch/' + url[3:]
+        self.open_url(url)

=== added file 'plugins/StandardURLs.py'
--- plugins/StandardURLs.py	1970-01-01 00:00:00 +0000
+++ plugins/StandardURLs.py	2012-01-29 23:59:23 +0000
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# StandardURLs.py - URL plugin for common urls (http, ftp, etc.)
+#
+# Copyright 2012 David Caro <david.caro.estevez@xxxxxxxxx>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import pygtk
+pygtk.require('2.0')
+import gobject
+import webbrowser
+
+try:
+    import gtk
+except:
+    ## do not use gtk, just print
+    print _("You need to install the python gtk bindings package"
+            "'python-gtk2'")
+    sys.exit(1)
+
+from clicompanionlib.utils import dbg
+import clicompanionlib.plugins as plugins
+
+
+class StandardURLs(plugins.URLPlugin):
+    '''
+    Match launchpad urls and open them on the browser
+    '''
+    __authors__ = 'David Caro <david.caro.estevez@xxxxxxxxx>'
+    __info__ = ('This plugins enables some common urls to be matched.')
+    __title__ = 'Standard URLS'
+
+    def __init__(self, config):
+        plugins.URLPlugin.__init__(self, config)
+        self.matches = []
+
+        userchars = "-A-Za-z0-9"
+        passchars = "-A-Za-z0-9,?;.:/!%$^*&~\"#'"
+        hostchars = "-A-Za-z0-9"
+        pathchars = "-A-Za-z0-9_$.+!*(),;:@&=?/~#%'\""
+        schemes = ("(news:|telnet:|nntp:|https?:|ftps?:|webcal:)//")
+        user = "([" + userchars + "]+(:[" + passchars + "]+)?)?"
+        urlpath = "/[" + pathchars + "]*[^]'.}>) \t\r\n,\\\"]"
+        email = ("[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-]*")
+
+        lboundry = "\\<"
+        rboundry = "\\>"
+
+        ## http/https/ftp/ftps/webcal/nntp/telnet urls
+        self.matches.append(schemes + user + "[" + hostchars + "]*\.["
+                            + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?")
+        ## file
+        self.matches.append('file:///[' + pathchars + "]*")
+        ## SIP
+        self.matches.append('(callto:|h323:|sip:)'
+                + "[" + userchars + "+]["
+                + userchars + ".]*(:[0-9]+)?@?["
+                + pathchars + "]+"
+                + rboundry)
+        ## mail
+        self.matches.append("(mailto:)?" + email)
+        ## news
+        self.matches.append('news:[-A-Z\^_a-z{|}~!"#$%&\'()*+'
+                        + ',./0-9;:=?`]+@' + "[-A-Za-z0-9.]+(:[0-9]+)?")
+        ## General url (www.host.com or ftp.host.com)
+        self.matches.append("(www|ftp)[" + hostchars + "]*\.["
+                + hostchars + ".]+(:[0-9]+)?(" + urlpath + ")?/?")
+
+    def callback(self, url, matchnum):
+        dbg('Opening common url ' + url)
+        if matchnum == 5:
+            if url[:3] == 'www':
+                url = 'http://' + url
+            else:
+                url = 'ftp://' + url
+        self.open_url(url)