WoX is a launcher for Windows that simply works. It’s an alternative to Alfred and Launchy. You can call it Windows omni-eXecutor if you want a long name.

这是 WoX 官网的简介,作为一个 Windows 下开源的快速启动工具,虽然没有 Launchy 成熟稳定,但是可以方便地写插件这点还是挺值得一试的。到这里获取 WoX :getWox Github

WoX 的插件开发支持 C# 和 Python 3 ,本文以 Python 为基础开发几款小插件。

IPIP

WoX 有一个提供 ip138 的 ip 地址查询的插件,然而在我这似乎并不奏效,索性就借助 ipip.net 的免费 API 来重新开发一个用来快速查询IP信息的小插件。(P.S. ipip 的免费API仅支持每天1000次查询,不过对个人用户来说绝对够用了,此外这个免费 API 貌似不支持 IPV6 和域名查询,所以本插件对这两者也就不做考虑)

首先要定义一个 plugin.json 用来储存插件的相关信息和配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
    "ID":"92p6d94f-4e10-4dw4-b650-8c8aba1d7ace",
    "ActionKeyword":"ipi",
    "Name":"IPIP",
    "Description":"Get IP info from ipip.net",
    "Author":"Eason Yang",
    "Version":"0.0.1",
    "Language":"python",
    "Website":"https://easonyang.com",
    "ExecuteFileName":"ipip.py",
    "IcoPath":"Images\\icon.png"
}

这些参数中需要特别说明的是:

  1. ID 应该是一个与其他插件不相同的UUID
  2. ActionKeyword 定义了执行此插件的关键字,同样也不能与其他插件重叠

随后 WoX 的插件需要编写一个继承 WoX 父类的子类,并且该子类必须重载参数为用户输入内容的 query 方法,插件的逻辑都在 query 方法中完成。

1
2
3
4
5
6
class IpInfo(Wox):    
    def query(self,query):
        """
        sub class need to override this method
        """
        return []

query 方法的返回值是一个由固定格式字典组成的列表,这个列表的元素也会作为 UI 中 item 显示,字典格式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        result = [{
            "Title": "IPIP",
            "SubTitle": data,
            "IcoPath": "app.ico",
            "JsonRPCAction": {
                "method": "detail",
                "parameters": ["http://www.ipip.net/ip.html"],
                "dontHideAfterAction": False
            }
        }]

数据放在副标题中显示即可,JsonRPCAction返回的这个字典用于 WoX 的主程序处理用户点击item后的动作,method 指要调用的方法,parameters 是要向方法中传递的值,dontHideAfterAction 指点击后是否隐藏 UI,根据官方文档,method 除了可以填咱们所创建的这个子类的方法,还可以调用系统自身的一些方法,详情参见文档。这里我们只简单地调用自定义的 detail 方法来在浏览器中打开网页,参数是固定的 ipip 网址。

ipip.net 的查询结果是一个 JSON 字符串,这样我们调用 requests 模块就可以很轻松地处理结果了。

1
2
3
4
5
        data = "Result:"
        url = "http://freeapi.ipip.net/" + query
        result = requests.get(url)
        for item in result.json():
            data += " " + item

但是这个插件在实际使用中会在日志中报错,原因就在于 WoX 在匹配到用户输入的关键字后,就会调用对应的插件处理此后用户的每个输入,所以这些不完整 IP 格式的就造成了插件报错,所以加入正则改进一下(P.S. 鉴于目的并不真的要判断 IP 地址格式是否正确,所以就不写那个一长串的匹配 IP 的正则表达式了):

1
2
3
        pattern = re.compile('(\d{1,3}\.){3}\d', re.S)
        if not re.search(pattern, query):
            return ""

至此一个简单的插件就完成了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class IpInfo(Wox):
    def query(self, query):
        if not query:
            return ""
        pattern = re.compile('(\d{1,3}\.){3}\d', re.S)
        if not re.search(pattern, query):
            return ""
        data = "Result:"
        url = "http://freeapi.ipip.net/" + query
        result = requests.get(url)
        for item in result.json():
            data += " " + item
        result = [{
            "Title": "IPIP",
            "SubTitle": data,
            "IcoPath": "app.ico",
            "JsonRPCAction": {
                "method": "detail",
                "parameters": ["http://www.ipip.net/ip.html"],
                "dontHideAfterAction": False
            }
        }]
        return result
    def detail(self, url):
        webbrowser.open(url)

if __name__ == "__main__":
    IpInfo()

注:在正式使用前,我们还可以通过运行脚本时传参的方式来模拟运行,也就是说我们可以借助这个方法来做 Debug。参数的格式是 "{\"method\":\"\",\"parameters\":[\"\"]}"

PING

接下来写一个利用 Python 调用系统 ping 命令并输出结果的小插件,同样只考虑 IPV4。

定义一个 ping 方法,在该方法中使用 subprocess 模块来执行命令后,将结果转为字符串并存入列表中返回,为了美观,把结果中的无用行忽略掉:

1
2
3
4
5
6
7
8
    def ping(self, ip):
        ping = subprocess.Popen(["ping", ip], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        result = []
        line_to_print = [1, 2, 3, 4, 5, 8, 10]
        for line_number, line in enumerate(ping.stdout.readlines()):
            if line_number in line_to_print:
                result.append(line.strip().decode('utf-8'))
        return result

随后仿照 IPIP 插件编写 query 方法,当 ip 符合格式时,调用 ping 方法,最后把结果处理之后返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    def query(self, query):
        ip = self.regex(query, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
        if not ip:
            return ""
        ping = self.ping(ip)
        result = []
        if len(ping) != 0:
            for item in ping:
                result.append({
                    "Title": "Ping Result",
                    "SubTitle": item,
                    "IcoPath": "Images/app.png",
                    "JsonRPCAction": {
                        "method": "detail",
                        "parameters":["http://ping.chinaz.com"],
                        "dontHideAfterAction": False
                    }
                })
        return result

这里为了方便把上文的正则处理抽象为一个独立的方法:

1
2
3
4
5
6
7
    def regex(self, query, condition):
        pattern = re.compile(condition)
        result = re.search(pattern, query)
        if result:
            return result.group()
        else:
            return ""

又一个小插件完成了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Ping(Wox):
    def query(self, query):
        ip = self.regex(query, '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
        if not ip:
            return ""
        ping = self.ping(ip)
        result = []
        if len(ping) != 0:
            for item in ping:
                result.append({
                    "Title": "Ping Result",
                    "SubTitle": item,
                    "IcoPath": "Images/app.png",
                    "JsonRPCAction": {
                        "method": "detail",
                        "parameters":["http://ping.chinaz.com"],
                        "dontHideAfterAction": False
                    }
                })
        return result

    def regex(self, query, condition):
        pattern = re.compile(condition, re.S)
        result = re.findall(pattern, query)
        if len(result) == 1:
            return result[0]
        else:
            return []

    def detail(self, url):
        webbrowser.open(url)

    def ping(self, ip):
        ping = subprocess.Popen(["ping", ip], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        result = []
        line_to_print = [1, 2, 3, 4, 5, 8, 10]
        for line_number, line in enumerate(ping.stdout.readlines()):
            if line_number in line_to_print:
                result.append(line.strip().decode('utf-8'))
        return result

if __name__ == "__main__":
    Ping()

IPIP&PING

最后为了便捷把这两个插件整合在一起:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# -*- coding: utf-8 -*-
# Author: Eason Yang
# Date: 6/18/2016
import webbrowser

import requests
import re
import subprocess
from wox import Wox, WoxAPI


class IpInfo(Wox):
    def query(self, query):
        if not query:
            return ""
        ip = self.regex(query, '(\d{1,3}\.){3}\d')
        if not ip:
            return ""
        data = "Result:"
        url = "http://freeapi.ipip.net/" + ip
        result = requests.get(url)
        for item in result.json():
            data += " " + item
        result = [{
            "Title": "IPIP",
            "SubTitle": data,
            "IcoPath": "Image/app.png",
            "JsonRPCAction": {"method": "detail", "parameters": ["http://www.ipip.net/ip.html"],
                              "dontHideAfterAction": False}
        }]
        if self.regex(query, "ping [\d\.]+"):
            ping = self.ping(ip)
            if len(ping) != 0:
                for item in ping:
                    result.append({
                        "Title": "Ping Result",
                        "SubTitle": item,
                        "IcoPath": "Images/app.png",
                        "JsonRPCAction": {"method": "detail", "parameters": ["http://ping.chinaz.com"],
                                          "dontHideAfterAction": False}
                    })
        return result

    def regex(self, query, condition):
        pattern = re.compile(condition)
        result = re.search(pattern, query)
        if result:
            return result.group()
        else:
            return ""

    def detail(self, url):
        webbrowser.open(url)

    def ping(self, ip):
        ping = subprocess.Popen(["ping", ip], shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        result = []
        line_to_print = [1, 2, 3, 4, 5, 8, 10]
        for line_number, line in enumerate(ping.stdout.readlines()):
            if line_number in line_to_print:
                result.append(line.strip().decode('utf-8'))
        return result


if __name__ == "__main__":
    IpInfo()

这样就能输入 ipip+ip 来查询 IP 信息,也可以通过 ipip+ping+ip 来同时显示 IP 信息和 ping 的结果。

踩坑记录

  • 缺少 requests 模块

    1
    2
    3
    4
    
    2016-06-18 17:49:24.7948|ERROR|Wox.Core.Plugin.JsonRPCPlugin.Execute|Traceback (most recent call last):
    File "C:\Users\Eason Yang\AppData\Local\Wox\app-1.3.67\Plugins\Wox.Plugin.Ipip\ipip.py", line 8, in <module>
    import requests
    ImportError: No module named 'requests'

明显是缺少 requests 模块,但官方文档中写的是

Wox支持使用Python进行插件的开发。Wox自带了一个打包的Python及其标准库,所以使用Python 插件的用户不必自己再安装Python环境。同时,Wox还打包了requests和beautifulsoup4两个库, 方便用户进行网络访问与解析。

然后试了下官方的其他Python插件,都提示缺少request模块。后来几经周折,才发现原来还要在 WoX 的 UI 里设置 Python 路径才能正常使用。

  • 缺少 wox 模块

    1
    2
    3
    4
    
    2016-06-18 19:01:47.7101|ERROR|Wox.Core.Plugin.JsonRPCPlugin.Execute|Traceback (most recent call last):
    File "C:\Users\Eason Yang\AppData\Roaming\Wox\Plugins\Hacker News-36f26938-4a86-4a14-b105-68124c769c99\main.py", line 6, in <module>
    from wox import Wox,WoxAPI
    ImportError: No module named 'wox'

解决上一个问题的时候发现有些用 Python 写的插件运行时会报上面这个错,只要在能正常运行的插件(比如默认安装的 HelloWorldPython )目录中找到 wox.py 复制到报错插件的目录即可。