├── 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 | ![images](https://github.com/lululau/proxyswitcher/raw/master/screenshot.png) 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 | --------------------------------------------------------------------------------