|
| 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: `> 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> |
0 commit comments