Skip to content

Commit e20c08d

Browse files
committedMar 16, 2013
Further refactoring, particularly utilising nikipore's alfred module.
Fix breaking when pybonjour is missing.
1 parent 6fbf2bf commit e20c08d

File tree

4 files changed

+290
-34
lines changed

4 files changed

+290
-34
lines changed
 

‎alfred.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -*- coding: utf-8 -*-
2+
import itertools
3+
import os
4+
import plistlib
5+
import unicodedata
6+
import sys
7+
8+
from xml.etree.ElementTree import Element, SubElement, tostring
9+
10+
_MAX_RESULTS = 9
11+
UNESCAPE_CHARACTERS = u"""\\ ()[]{};`'"$"""
12+
13+
preferences = plistlib.readPlist('info.plist')
14+
bundleid = preferences['bundleid']
15+
16+
class Item(object):
17+
@classmethod
18+
def unicode(cls, value):
19+
try:
20+
items = value.iteritems()
21+
except AttributeError:
22+
return unicode(value)
23+
else:
24+
return dict(map(unicode, item) for item in items)
25+
26+
def __init__(self, attributes, title, subtitle, icon=None):
27+
self.attributes = attributes
28+
self.title = title
29+
self.subtitle = subtitle
30+
self.icon = icon
31+
32+
def __str__(self):
33+
return tostring(self.xml(), encoding='utf-8')
34+
35+
def xml(self):
36+
item = Element(u'item', self.unicode(self.attributes))
37+
for attribute in (u'title', u'subtitle', u'icon'):
38+
value = getattr(self, attribute)
39+
if value is None:
40+
continue
41+
try:
42+
(value, attributes) = value
43+
except:
44+
attributes = {}
45+
SubElement(item, attribute, self.unicode(attributes)).text = unicode(value)
46+
return item
47+
48+
def args(characters=None):
49+
return tuple(unescape(decode(arg), characters) for arg in sys.argv[1:])
50+
51+
def config():
52+
return _create('config')
53+
54+
def decode(s):
55+
return unicodedata.normalize('NFC', s.decode('utf-8'))
56+
57+
def uid(uid):
58+
return u'-'.join(map(unicode, (bundleid, uid)))
59+
60+
def unescape(query, characters=None):
61+
for character in (UNESCAPE_CHARACTERS if (characters is None) else characters):
62+
query = query.replace('\\%s' % character, character)
63+
return query
64+
65+
def work(volatile):
66+
path = {
67+
True: '~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data',
68+
False: '~/Library/Application Support/Alfred 2/Workflow Data'
69+
}[bool(volatile)]
70+
return _create(os.path.join(os.path.expanduser(path), bundleid))
71+
72+
def write(text):
73+
sys.stdout.write(text)
74+
75+
def xml(items):
76+
root = Element('items')
77+
for item in itertools.islice(items, _MAX_RESULTS):
78+
root.append(item.xml())
79+
return tostring(root, encoding='utf-8')
80+
81+
def _create(path):
82+
if not os.path.isdir(path):
83+
os.mkdir(path)
84+
if not os.access(path, os.W_OK):
85+
raise IOError('No write access: %s' % path)
86+
return path

‎icon.png

14 KB
Loading

‎info.plist

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>bundleid</key>
6+
<string>net.isometry.alfred.ssh</string>
7+
<key>connections</key>
8+
<dict>
9+
<key>73503A72-F4BD-4C29-B531-ACE7CF405F6B</key>
10+
<array>
11+
<dict>
12+
<key>destinationuid</key>
13+
<string>027D62F5-14E9-4EA0-BE27-57C38B1ECC1F</string>
14+
<key>modifiers</key>
15+
<integer>0</integer>
16+
<key>modifiersubtext</key>
17+
<string></string>
18+
</dict>
19+
</array>
20+
</dict>
21+
<key>createdby</key>
22+
<string>Robin Breathe</string>
23+
<key>description</key>
24+
<string>Open Secure SHell with smart hostname autocompletion</string>
25+
<key>disabled</key>
26+
<false/>
27+
<key>name</key>
28+
<string>Open SSH</string>
29+
<key>objects</key>
30+
<array>
31+
<dict>
32+
<key>config</key>
33+
<dict>
34+
<key>argumenttype</key>
35+
<integer>0</integer>
36+
<key>escaping</key>
37+
<integer>4</integer>
38+
<key>keyword</key>
39+
<string>ssh</string>
40+
<key>runningsubtext</key>
41+
<string>Please Wait: matching host…</string>
42+
<key>script</key>
43+
<string># Open SSH.alfredworkflow, v0.9
44+
# Robin Breathe, 2013
45+
46+
import json
47+
from os import path
48+
import xml.etree.ElementTree as ET
49+
import re
50+
import .alfred
51+
52+
53+
query = "{query}"
54+
55+
bonjour_timeout = 0.1
56+
57+
if '@' in query:
58+
(user, host) = query.split('@', 1)
59+
else:
60+
(user, host) = (None, query)
61+
62+
host_chars = map(lambda x: '\.' if x is '.' else x, list(host))
63+
pattern = re.compile('.*?%s' % '.*?\b?'.join(host_chars), flags=re.IGNORECASE)
64+
65+
arg = lambda u, h: u and '@'.join([u,h]) or h
66+
67+
class SSHItem(alfred.Item):
68+
def __init__(user, host):
69+
_arg = arg(user, host)
70+
_uri = 'ssh://%s' % _arg
71+
return super(SSHItem, self).__init__(attributes={'uid':_uri, 'arg':_arg},
72+
title=_uri, subtitle='SSH to %s' % host, icon='icon.png')
73+
74+
def fetch_ssh_config(_path):
75+
results = set([])
76+
try:
77+
with open(path.expanduser(_path), 'r') as ssh_config:
78+
for line in (x for x in ssh_config if x.startswith('Host ')):
79+
results.update((x for x in line.split()[1:] if not ('*' in x or '?' in x or '!' in x)))
80+
except IOError:
81+
pass
82+
return results
83+
84+
def fetch_known_hosts(_path):
85+
results = set([])
86+
try:
87+
with open(path.expanduser(_path), 'r') as known_hosts:
88+
for line in known_hosts:
89+
results.update(line.split()[0].split(','))
90+
except IOError:
91+
pass
92+
return results
93+
94+
def fetch_hosts(_path):
95+
results = set([])
96+
try:
97+
with open(_path, 'r') as etc_hosts:
98+
for line in (x for x in etc_hosts if not x.startswith('#')):
99+
results.update(line.split()[1:])
100+
results.discard('broadcasthost')
101+
except IOError:
102+
pass
103+
return results
104+
105+
def fetch_bonjour(_service):
106+
results = set([])
107+
try:
108+
from pybonjour import DNSServiceBrowse, DNSServiceProcessResult
109+
from select import select
110+
bj_callback = lambda s, f, i, e, n, t, d: results.add('%s.%s' % (n, d))
111+
bj_browser = DNSServiceBrowse(regtype = _service, callBack = bj_callback)
112+
select([bj_browser], [], [], bonjour_timeout)
113+
DNSServiceProcessResult(bj_browser)
114+
bj_browser.close()
115+
except ImportError:
116+
pass
117+
return results
118+
119+
hosts = set([])
120+
hosts.update(fetch_ssh_config('~/.ssh/config'))
121+
hosts.update(fetch_known_hosts('~/.ssh/known_hosts'))
122+
hosts.update(fetch_hosts('/etc/hosts'))
123+
hosts.update(fetch_bonjour('_ssh._tcp'))
124+
hosts.discard(host)
125+
126+
results = [SSHItem(user, host)]
127+
for host in (x for x in hosts if pattern.match(x)):
128+
results.append(SSHItem(user, host))
129+
130+
print alfred.xml(results)
131+
</string>
132+
<key>subtext</key>
133+
<string>Open Secure SHell with smart hostname autocompletion</string>
134+
<key>title</key>
135+
<string>Open SSH</string>
136+
<key>type</key>
137+
<integer>3</integer>
138+
<key>withspace</key>
139+
<true/>
140+
</dict>
141+
<key>type</key>
142+
<string>alfred.workflow.input.scriptfilter</string>
143+
<key>uid</key>
144+
<string>73503A72-F4BD-4C29-B531-ACE7CF405F6B</string>
145+
</dict>
146+
<dict>
147+
<key>config</key>
148+
<dict>
149+
<key>plusspaces</key>
150+
<false/>
151+
<key>url</key>
152+
<string>ssh://{query}</string>
153+
<key>utf8</key>
154+
<true/>
155+
</dict>
156+
<key>type</key>
157+
<string>alfred.workflow.action.openurl</string>
158+
<key>uid</key>
159+
<string>027D62F5-14E9-4EA0-BE27-57C38B1ECC1F</string>
160+
</dict>
161+
</array>
162+
<key>readme</key>
163+
<string>Easily open remote SSH sessions using your default ssh: protocol handler (the default being Terminal.app) with full anchored hostname autocompletion against the contents of ~/.ssh/known_hosts (every host you've connected to before), /etc/hosts and, optionally, Bonjour (Back to My Mac and local hosts advertising their ability to accept SSH connections).
164+
165+
In order to enable Bonjour discovery, you must install the pybonjour module: `&gt; sudo /usr/bin/easy_install pybonjour`</string>
166+
<key>uidata</key>
167+
<dict>
168+
<key>027D62F5-14E9-4EA0-BE27-57C38B1ECC1F</key>
169+
<dict>
170+
<key>ypos</key>
171+
<real>10</real>
172+
</dict>
173+
<key>73503A72-F4BD-4C29-B531-ACE7CF405F6B</key>
174+
<dict>
175+
<key>ypos</key>
176+
<real>10</real>
177+
</dict>
178+
</dict>
179+
<key>webaddress</key>
180+
<string>http://isometry.net/</string>
181+
</dict>
182+
</plist>

‎script_filter.py

+22-34
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
# Open SSH.alfredworkflow, v0.8
1+
# Open SSH.alfredworkflow, v0.9
22
# Robin Breathe, 2013
33

4+
import json
45
from os import path
56
import xml.etree.ElementTree as ET
67
import re
8+
import alfred
9+
710

811
query = "{query}"
912

@@ -17,17 +20,14 @@
1720
host_chars = map(lambda x: '\.' if x is '.' else x, list(host))
1821
pattern = re.compile('.*?%s' % '.*?\b?'.join(host_chars), flags=re.IGNORECASE)
1922

20-
arg = lambda u, h: u and '@'.join([u,h]) or h
21-
22-
def add_item(root, user, host):
23-
_arg = arg(user, host)
24-
_uri = 'ssh://%s' % _arg
25-
item = ET.SubElement(root, 'item', uid=_uri, arg=_arg, autocomplete=_arg)
26-
ET.SubElement(item, 'title').text = _uri
27-
ET.SubElement(item, 'subtitle').text = 'SSH to %s' % host
28-
ET.SubElement(item, 'icon', type='fileicon').text = '/Applications/Utilities/Terminal.app'
23+
class SSHItem(alfred.Item):
24+
def __init__(self, user, host):
25+
_arg = user and '@'.join([user,host]) or host
26+
_uri = 'ssh://%s' % _arg
27+
return super(SSHItem, self).__init__(attributes={'uid':_uri, 'arg':_arg},
28+
title=_uri, subtitle='SSH to %s' % host, icon='icon.png')
2929

30-
def parse_ssh_config(_path):
30+
def fetch_ssh_config(_path):
3131
results = set([])
3232
try:
3333
with open(path.expanduser(_path), 'r') as ssh_config:
@@ -37,7 +37,7 @@ def parse_ssh_config(_path):
3737
pass
3838
return results
3939

40-
def parse_known_hosts(_path):
40+
def fetch_known_hosts(_path):
4141
results = set([])
4242
try:
4343
with open(path.expanduser(_path), 'r') as known_hosts:
@@ -47,7 +47,7 @@ def parse_known_hosts(_path):
4747
pass
4848
return results
4949

50-
def parse_hosts(_path):
50+
def fetch_hosts(_path):
5151
results = set([])
5252
try:
5353
with open(_path, 'r') as etc_hosts:
@@ -58,7 +58,7 @@ def parse_hosts(_path):
5858
pass
5959
return results
6060

61-
def discover_bonjour(_service):
61+
def fetch_bonjour(_service):
6262
results = set([])
6363
try:
6464
from pybonjour import DNSServiceBrowse, DNSServiceProcessResult
@@ -72,27 +72,15 @@ def discover_bonjour(_service):
7272
pass
7373
return results
7474

75-
def discover_bonjour(_service):
76-
results = set([])
77-
from pybonjour import DNSServiceBrowse, DNSServiceProcessResult
78-
from select import select
79-
bj_callback = lambda s, f, i, e, n, t, d: results.add('%s.%s' % (n, d))
80-
bj_browser = DNSServiceBrowse(regtype = _service, callBack = bj_callback)
81-
select([bj_browser], [], [], bonjour_timeout)
82-
DNSServiceProcessResult(bj_browser)
83-
bj_browser.close()
84-
return results
85-
8675
hosts = set([])
87-
hosts.update(parse_ssh_config('~/.ssh/config'))
88-
hosts.update(parse_known_hosts('~/.ssh/known_hosts'))
89-
hosts.update(parse_hosts('/etc/hosts'))
90-
hosts.update(discover_bonjour('_ssh._tcp'))
76+
hosts.update(fetch_ssh_config('~/.ssh/config'))
77+
hosts.update(fetch_known_hosts('~/.ssh/known_hosts'))
78+
hosts.update(fetch_hosts('/etc/hosts'))
79+
hosts.update(fetch_bonjour('_ssh._tcp'))
9180
hosts.discard(host)
9281

93-
root = ET.Element('items')
94-
add_item(root, user, host)
95-
for h in (h for h in hosts if pattern.match(h)):
96-
add_item(root, user, h)
82+
results = [SSHItem(user, host)]
83+
for host in (x for x in hosts if pattern.match(x)):
84+
results.append(SSHItem(user, host))
9785

98-
print ET.tostring(root, encoding='utf-8')
86+
print alfred.xml(results)

0 commit comments

Comments
 (0)
Please sign in to comment.