├── Off.png
├── On.png
├── icon.png
├── screenshot.png
├── ProxySwitcher.alfredworkflow
├── .gitignore
├── proxyswitcher.rc.sample
├── LICENSE
├── README.md
├── info.plist
├── proxy_switcher.rb
└── alfred.rb
/Off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lululau/proxy-switcher-alfred-workflow/HEAD/Off.png
--------------------------------------------------------------------------------
/On.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lululau/proxy-switcher-alfred-workflow/HEAD/On.png
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lululau/proxy-switcher-alfred-workflow/HEAD/icon.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lululau/proxy-switcher-alfred-workflow/HEAD/screenshot.png
--------------------------------------------------------------------------------
/ProxySwitcher.alfredworkflow:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lululau/proxy-switcher-alfred-workflow/HEAD/ProxySwitcher.alfredworkflow
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *#
2 | *.swl
3 | *.swm
4 | *.swn
5 | *.swo
6 | *.swo
7 | *.swp
8 | *~
9 | .#*
10 | .DS_Store
11 | ._.DS_Store
12 | ._.TemporaryItems
13 |
--------------------------------------------------------------------------------
/proxyswitcher.rc.sample:
--------------------------------------------------------------------------------
1 | AutoDiscoveryProxy:
2 | AutoProxy:
3 | URL: "file://localhost/Applications/Safari.app/Contents/Resources/autoproxy.pac"
4 | SocksProxy:
5 | Host: 127.0.0.1
6 | Port: 8080
7 | Auth: true
8 | Username: hello
9 | Password: 123123
10 | HTTPProxy:
11 | Host: 127.0.0.1
12 | Port: 8080
13 | HTTPSProxy:
14 | Host: 127.0.0.1
15 | Port: 8080
16 | FTPProxy:
17 | Host: 127.0.0.1
18 | Port: 8080
19 | RTSPProxy:
20 | Host: 127.0.0.1
21 | Port: 8080
22 | GopherProxy:
23 | Host: 127.0.0.1
24 | Port: 8080
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 lululau
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | proxyswitcher
2 | =============
3 |
4 | ## New design for current version 2.0.0:
5 |
6 |
7 | An Alfred.app workflow for switching proxy states of Mac OS X.
8 |
9 | With this workflow, you will need not dive deepl into system preferences panel for toggling proxy states.
10 |
11 | This workflow will show proxy options(need to pre-configured in a dot file) of current primary network service (usually the "Wi-Fi" serivce on a MacBook X).
12 |
13 | ### Requirements:
14 |
15 | 1. Alfred.app with PowerPack activated.
16 |
17 | ### Install steps:
18 |
19 | 1. Download the ProxySwitcher.alfredworkflow file.
20 |
21 | 2. Double-click it.
22 |
23 | ### Usage:
24 |
25 | Put a file names `.proxyswitcher.rc` to your home directory, edit this file like this:
26 |
27 | ```yaml
28 | AutoDiscoveryProxy: # AutoDiscoveryProxy has no options
29 | AutoProxy:
30 | URL: "file://localhost/Applications/Safari.app/Contents/Resources/autoproxy.pac" # URL of pac file
31 | SocksProxy:
32 | Host: 127.0.0.1
33 | Port: 8080
34 | Auth: true
35 | Username: hello
36 | Password: 123123
37 | HTTPProxy:
38 | Host: 127.0.0.1
39 | Port: 8080
40 | Auth: false
41 | HTTPSProxy:
42 | Host: 127.0.0.1
43 | Port: 8080
44 | Auth: false
45 | FTPProxy:
46 | Host: 127.0.0.1
47 | Port: 8080
48 | Auth: false
49 | RTSPProxy:
50 | Host: 127.0.0.1
51 | Port: 8080
52 | Auth: false
53 | GopherProxy:
54 | Host: 127.0.0.1
55 | Port: 8080
56 | Auth: false
57 | ```
58 |
59 | The workflow will only show proxies already pre-configured in `.proxyswitcher.rc` file.
60 |
61 | In Alfre.app text input field, type "proxy", move cursor to one proxy option and press enter, the worlflow will toggle state of that proxy option.
62 |
63 | ## Specification for version 1.0.0:
64 |
65 |
66 | An Alfred.app workflow for switching proxy states of Mac OS X.
67 |
68 | With this workflow, you will need not dive deepl into system preferences panel for toggling proxy states.
69 |
70 | The searching keyword is names of the sevices, such as, `Wi-Fi`, `Ethernet`, etc.
71 |
72 | By Default, ProxySwitcher will show all proxy options for each services.
73 |
74 | If you have a file named `.proxyswitcher.rc` in your home dir, then ProxySwitcher will only show proxy options for those services with names in this file, each service name is in one single line.
75 |
76 | You could get all available service names via this command: `networksetup -listallnetworkservices`
77 |
78 |
79 | ### Requirements:
80 |
81 | 1. Alfred.app with PowerPack activated.
82 |
83 | ### Install steps:
84 |
85 | 1. Download the ProxySwitcher.alfredworkflow file.
86 |
87 | 2. Double-click it.
88 |
89 | 3. If you want ProxySwitcher only show proxy options for Wi-Fi, then you can put one line `"Wi-Fi\n"`(without quotes, and `\n` means an UNIX new-line character) into `~/.proxyswitcher.rc`
90 |
91 | ### Screenshots:
92 |
93 | 
94 |
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 | com.github.lululau.proxyswitcher
7 | category
8 | Tools
9 | connections
10 |
11 | 06C9C4A9-38CE-441A-8D06-E2F2D8B39B60
12 |
13 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE
14 |
15 |
16 | destinationuid
17 | 06C9C4A9-38CE-441A-8D06-E2F2D8B39B60
18 | modifiers
19 | 0
20 | modifiersubtext
21 |
22 |
23 |
24 |
25 | createdby
26 | Liu Xiang
27 | description
28 | Enable / Disable Proxies
29 | disabled
30 |
31 | name
32 | ProxySwitcher
33 | objects
34 |
35 |
36 | config
37 |
38 | argumenttype
39 | 1
40 | escaping
41 | 62
42 | keyword
43 | proxy
44 | script
45 | ruby proxy_switcher.rb "{query}"
46 | subtext
47 | Enable / Disable Proxies
48 | title
49 | Proxy Switcher
50 | type
51 | 0
52 | withspace
53 |
54 |
55 | type
56 | alfred.workflow.input.scriptfilter
57 | uid
58 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE
59 | version
60 | 0
61 |
62 |
63 | config
64 |
65 | escaping
66 | 62
67 | script
68 | if [[ ! -z "{query}" ]]
69 | then
70 | {query}
71 | fi
72 | type
73 | 0
74 |
75 | type
76 | alfred.workflow.action.script
77 | uid
78 | 06C9C4A9-38CE-441A-8D06-E2F2D8B39B60
79 | version
80 | 0
81 |
82 |
83 | readme
84 | The searching keyword is names of the sevices, such as, ‘Wi-Fi’, ‘Ethernet’, etc.
85 |
86 | By Default, ProxySwitcher will show all proxy options for each services.
87 |
88 | If you have a file named ‘.proxyswitcher.rc’ in your home dir, then ProxySwitcher will only show proxy options for those services with names in that file, each service name in a single line.
89 |
90 | You could get all available service names via this command: ‘networksetup -listallnetworkservices’
91 | uidata
92 |
93 | 06C9C4A9-38CE-441A-8D06-E2F2D8B39B60
94 |
95 | ypos
96 | 200
97 |
98 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE
99 |
100 | ypos
101 | 90
102 |
103 |
104 | webaddress
105 | https://github.com/lululau/proxyswitcher
106 |
107 |
108 |
--------------------------------------------------------------------------------
/proxy_switcher.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "yaml"
4 | require_relative "alfred"
5 |
6 | class ProxySwitcher
7 |
8 | class Config
9 |
10 | attr :proxies
11 |
12 | CONFIG_FILE = File.expand_path '~/.proxyswitcher.rc'
13 |
14 | def initialize(service='Wi-Fi')
15 | if test ?e, CONFIG_FILE
16 | @proxies = YAML.load(IO.read(CONFIG_FILE)).map { |k, v| ProxyOption.buildProxy(service, k, v) }
17 | end
18 | end
19 | end
20 |
21 | PRIMARY_SERVICE_CMD = <<-'EOF'
22 | SERVICE_GUID=`printf "open\nget State:/Network/Global/IPv4\nd.show" | \
23 | scutil | grep "PrimaryService" | awk '{print $3}'`
24 | SERVICE_NAME=`printf "open\nget Setup:/Network/Service/$SERVICE_GUID\nd.show" |\
25 | scutil | grep "UserDefinedName" | awk -F': ' '{print $2}'`
26 | echo $SERVICE_NAME
27 | EOF
28 |
29 | def initialize
30 | @service = self.primary_service
31 | @proxies = Config.new(@service).proxies
32 | end
33 |
34 | def primary_service
35 | `#{PRIMARY_SERVICE_CMD}`.chomp
36 | end
37 |
38 | def to_xml
39 | ItemList.new(@proxies).to_xml
40 | end
41 |
42 | class ProxyOption < Item
43 |
44 | def self.buildProxy(service, name, options)
45 | case name
46 | when 'AutoDiscoveryProxy'
47 | ProxyAutoDiscovery.new(service)
48 | when 'AutoProxy'
49 | AutoProxy.new(service, options)
50 | else
51 | ProxyOption.new(service, name, options)
52 | end
53 | end
54 |
55 | Names = {
56 | 'AutoDiscoveryProxy' => 'proxyautodiscovery',
57 | 'AutoProxy' => 'autoproxy',
58 | 'SocksProxy' => 'socksfirewallproxy',
59 | 'HTTPProxy' => 'webproxy',
60 | 'HTTPSProxy' => 'securewebproxy',
61 | 'FTPProxy' => 'ftpproxy',
62 | 'RTSPProxy' => 'streamingproxy',
63 | 'GopherProxy' => 'gopherproxy'
64 | }
65 |
66 | attr :human_name
67 |
68 | GET_INFO_CMD = "networksetup -get%s '%s'"
69 | TURN_ON_CMD = "networksetup -set%s '%s' '%s' '%s' %s %s %s"
70 | TURN_OFF_CMD = "networksetup -set%sstate '%s' off"
71 |
72 | def initialize(service, name, options={})
73 | super()
74 | @service = service
75 | @human_name = name
76 | @name = Names[name]
77 | self.parse_options(options)
78 | self.fetch_info
79 | @attributes[:uid] = @name
80 | @subtitle = @service
81 | @icon[:text] = "%s.png" % @status
82 | end
83 |
84 | def parse_options(options)
85 | @url = options['URL']
86 | @server = options['Host']
87 | @port = options['Port']
88 | @auth = options['Auth']
89 | @username = options['Username']
90 | @password = options['Password']
91 | end
92 |
93 | def fetch_info
94 | IO.popen GET_INFO_CMD % [@name, @service] do |io|
95 | io.each do |line|
96 | line.chomp!
97 | if line =~ /^Enabled: (Yes|No)/
98 | if $1 == 'Yes'
99 | @status = 'On'
100 | else
101 | @status = 'Off'
102 | end
103 | elsif line.start_with? 'Server:' and @status == 'On'
104 | @server = line
105 | elsif line.start_with? 'Port:' and @status == 'On'
106 | @port = line
107 | end
108 | end
109 | end
110 | if @status == 'On'
111 | @attributes[:arg] = TURN_OFF_CMD % [@name, @service, 'off']
112 | else
113 | if @auth
114 | @attributes[:arg] = TURN_ON_CMD % [@name, @service, @server, @port, 'on', "'#@username'", "'#@password'"]
115 | else
116 | @attributes[:arg] = TURN_ON_CMD % [@name, @service, @server, @port, '', "", ""]
117 | end
118 | end
119 | @title = "%s: %s, %s, %s" % [@human_name, @status, @server, @port]
120 | end
121 |
122 | end
123 |
124 | class ProxyAutoDiscovery < ProxyOption
125 |
126 | SET_STATE_CMD = "networksetup -setproxyautodiscovery '%s' %s"
127 |
128 | def initialize(service)
129 | super(service, 'AutoDiscoveryProxy')
130 | end
131 |
132 | def fetch_info
133 | IO.popen ProxyOption::GET_INFO_CMD % [@name, @service] do |io|
134 | io.each do |line|
135 | line.chomp!
136 | if line =~ /: (On|Off)/
137 | if $1 == 'On'
138 | @status = 'On'
139 | @reversed_status = 'off'
140 | else
141 | @status = 'Off'
142 | @reversed_status = 'on'
143 | end
144 | end
145 | end
146 | end
147 | @attributes[:arg] = SET_STATE_CMD % [@service, @reversed_status]
148 | @title = "%s: %s" % [@human_name, @status]
149 | end
150 | end
151 |
152 | class AutoProxy < ProxyOption
153 |
154 | GET_INFO_CMD = "networksetup -getautoproxyurl '%s'"
155 | TURN_ON_CMD = "networksetup -set%surl '%s' '%s'"
156 | TURN_OFF_CMD = "networksetup -set%sstate '%s' off"
157 |
158 | def initialize(service, options)
159 | super(service, 'AutoProxy', options)
160 | end
161 |
162 | def fetch_info
163 | IO.popen GET_INFO_CMD % @service do |io|
164 | io.each do |line|
165 | line.chomp!
166 | if line =~ /^Enabled: (Yes|No)/
167 | if $1 == 'Yes'
168 | @status = 'On'
169 | @reversed_status = 'off'
170 | else
171 | @status = 'Off'
172 | @reversed_status = 'on'
173 | end
174 | elsif line.start_with? 'URL:' and @status == 'On'
175 | @url = line
176 | end
177 | end
178 | end
179 | if @status == 'On'
180 | @attributes[:arg] = TURN_OFF_CMD % [@name, @service, 'off']
181 | else
182 | @attributes[:arg] = TURN_ON_CMD % [@name, @service, @url]
183 | end
184 | @title = "%s: %s, %s" % [@human_name, @status, @url]
185 | end
186 | end
187 | end
188 |
189 | if $0 == __FILE__
190 | puts ProxySwitcher.new.to_xml
191 | end
192 |
--------------------------------------------------------------------------------
/alfred.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "delegate"
4 |
5 | class String
6 | def to_argv
7 | argv = []
8 | meta_char_stack = []
9 | arg = nil
10 | idx = 0
11 | while idx < self.size
12 | char = self[idx]
13 | top = meta_char_stack.last
14 | case char
15 | when %{"}
16 | case top
17 | when %{"}
18 | meta_char_stack.pop
19 | arg ||= ""
20 | when %{'}
21 | (arg ||= "") << char
22 | when %{\\}
23 | meta_char_stack.pop
24 | (arg ||= "") << char
25 | else
26 | meta_char_stack << char
27 | end
28 |
29 | when %{'}
30 | case top
31 | when %{"}
32 | (arg ||= "") << char
33 | when %{'}
34 | meta_char_stack.pop
35 | arg ||= ""
36 | else
37 | meta_char_stack << char
38 | end
39 |
40 | when %{\\}
41 | case top
42 | when %{"}
43 | meta_char_stack << char
44 | when %{'}
45 | (arg ||= "") << char
46 | when %{\\}
47 | meta_char_stack.pop
48 | (arg ||= "") << char
49 | else
50 | meta_char_stack << char
51 | end
52 |
53 | when %{ }
54 | case top
55 | when %{"}
56 | (arg ||= "") << char
57 | when %{'}
58 | (arg ||= "") << char
59 | when %{\\}
60 | meta_char_stack.pop
61 | (arg ||= "") << char
62 | else
63 | argv << arg if arg
64 | arg = nil
65 | end
66 | else
67 | if top == %{\\}
68 | meta_char_stack.pop
69 | end
70 | (arg ||= "") << char
71 | end
72 | idx += 1
73 | end
74 | argv << arg if arg
75 | argv
76 | end
77 |
78 | end
79 |
80 | class MDLS
81 |
82 | module ContentType
83 | Mail = "com.apple.mail.emlx"
84 | WebHistory = "com.apple.safari.history"
85 | end
86 |
87 | def initialize(filename)
88 | @filename = filename
89 | @metadata = `mdls '#{filename}'`
90 | end
91 |
92 | def [](key)
93 | m = @metadata.scan(/^#{key}\s*=\s\(\n([^\)]*)\)\n/m)
94 | if not m.empty?
95 | multi_values = m.first.first
96 | return multi_values.lines.map do |line|
97 | line[/"([^"]*)"/, 1]
98 | end
99 | else
100 | return @metadata[/^#{key}\s*=\s"([^"]*)"/, 1]
101 | end
102 | end
103 |
104 | def content_type
105 | @content_type ||= self['kMDItemContentType']
106 | end
107 |
108 | def web_history?
109 | content_type == ContentType::WebHistory
110 | end
111 |
112 | def web_title
113 | @web_title ||= self['kMDItemDisplayName']
114 | end
115 |
116 | def web_url
117 | @web_url ||= self['kMDItemURL']
118 | end
119 |
120 | def mail?
121 | content_type == ContentType::Mail
122 | end
123 |
124 | def mail_title
125 | @mail_title ||= self['kMDItemSubject']
126 | end
127 |
128 | def mail_authors
129 | @mail_authors ||= self['kMDItemAuthorEmailAddresses']
130 | end
131 |
132 | def mail_recipients
133 | @mail_recipients ||= self['kMDItemRecipientEmailAddresses']
134 | end
135 | end
136 |
137 | class Item
138 | attr_accessor :attributes, :title, :subtitle, :icon
139 |
140 | def initialize
141 | @attributes = {}
142 | @title = ""
143 | @subtitle = ""
144 | @icon = {}
145 | end
146 |
147 | def to_xml
148 | xml = "- "
153 |
154 | xml << ""
155 | xml << title
156 | xml << ""
157 |
158 | xml << ""
159 | xml << subtitle
160 | xml << ""
161 |
162 | if icon and not icon.empty?
163 | xml << ""
166 | xml << icon[:text]
167 | xml << ""
168 | end
169 |
170 | xml << "
"
171 | end
172 | end
173 |
174 | class FileItem < Item
175 |
176 | class << self
177 | def create(path)
178 | case path
179 | when /\.emlx/
180 | MailFileItem.new path
181 | when /\.webhistory/
182 | WebHistoryFileItem.new path
183 | else
184 | new path
185 | end
186 | end
187 | end
188 |
189 | def initialize(path)
190 | super()
191 | path.chomp!
192 | basename = File.basename path
193 | @attributes[:uid] = path
194 | @attributes[:arg] = path
195 | @attributes[:valid] = "yes"
196 | @attributes[:autocomplete] = basename
197 | @attributes[:type] = "file"
198 | @title = basename
199 | @subtitle = path
200 | @icon[:type] = "fileicon"
201 | @icon[:text] = path
202 | end
203 | end
204 |
205 | class MailFileItem < FileItem
206 | def initialize(path)
207 | super
208 | path.chomp!
209 | mdls = MDLS.new path
210 | return unless mdls.mail?
211 | @title = mdls.mail_title
212 | @subtitle = "Author: %s, Recipient: %s" % [mdls.mail_authors.first,
213 | mdls.mail_recipients && mdls.mail_recipients.first]
214 | end
215 | end
216 |
217 | class WebHistoryFileItem < FileItem
218 | def initialize(path)
219 | super
220 | path.chomp!
221 | mdls = MDLS.new path
222 | return unless mdls.web_history?
223 | @title = mdls.web_title
224 | @subject = mdls.web_url
225 | end
226 |
227 | end
228 |
229 | class ItemList < DelegateClass(Array)
230 | def initialize(items=[])
231 | @items = items
232 | super items
233 | end
234 |
235 | def to_xml
236 | xml = <<-EOF
237 |
238 |
239 | #{@items.map { |item| " " + item.to_xml }.join("\n") rescue ''}
240 |
241 | EOF
242 | end
243 |
244 | def add_file_item(path)
245 | @items << FileItem.create(path)
246 | self
247 | end
248 |
249 | def add_file_list(paths)
250 | @items.concat(paths.map { |p| FileItem.create(p) })
251 | self
252 | end
253 |
254 | def add_items(lines)
255 | lines.each do |line|
256 | if test ?e, line.chomp
257 | @items << FileItem.create(line)
258 | else
259 | item = Item.new
260 | item.title = line
261 | @items << item
262 | end
263 | end
264 | self
265 | end
266 | end
267 |
--------------------------------------------------------------------------------