├── .gitignore ├── Settings.plist ├── MIT-LICENSE ├── README.md ├── overlay.js ├── Info.plist ├── globalpage.html └── vim-keybindings.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | -------------------------------------------------------------------------------- /Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Key 7 | disabledsites 8 | Title 9 | Disabled sites 10 | Type 11 | TextField 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Mutwin Kraus 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vim Keybindings for Safari 2 | -------------------------- 3 | 4 | Currently supported keys: 5 | 6 | * gg, G 7 | * h, j, k, l 8 | * ^D, ^U, ^F, ^B 9 | * esc, i, dd 10 | * gt, gT, gt 11 | 12 | Currently supported commands: 13 | 14 | * :q, :q!, :tabnew 15 | * :tabn, :tabp, :tabfir, :tabfirst, :tablast 16 | * :e @url, :edit @url, :tabe @url, :tabedit @url 17 | * :%s/@search/@replace 18 | 19 | @url should be a valid url. http:// will be added if it is not provided. 20 | @search and @replace needs to be regular expression. Modifiers are supported. Remember though that this is the javascript engine NOT the vim engine. 21 | 22 | In the preferences for the extension, it is possible to give a list of sites, where the extension should not be loaded. Separate sites by , (comma). Spaces are allowed. 23 | 24 | Known issues 25 | ------------ 26 | * Some pages takes over the keyboard just as this extension does. That means that on some pages the overlay wont show up and wont receive key strokes. 27 | * Some pages makes the gt and gT combos jump past it. 28 | 29 | Contributors 30 | ============ 31 | 32 | Mutwin Kraus 33 | Jason Green 34 | Jannik Nielsen 35 | -------------------------------------------------------------------------------- /overlay.js: -------------------------------------------------------------------------------- 1 | if (window.top === window) { 2 | var overlay = document.createElement("div"); 3 | overlay.textContent = ":"; 4 | overlay.style.color = "black"; 5 | overlay.style.backgroundColor = "#CADDF2"; 6 | overlay.style.position = "fixed"; 7 | overlay.style.width = "400px"; 8 | overlay.style.bottom = "0"; 9 | overlay.style.left = "0"; 10 | overlay.style.padding = "1px 0"; 11 | overlay.style.margin = "0"; 12 | overlay.style.border = "1px solid #97BAEB"; 13 | overlay.style.borderTopRightRadius = "5px"; 14 | overlay.style.borderBottomRightRadius = "5px"; 15 | overlay.style.display = "none"; 16 | overlay.setAttribute('id', 'vimOverlay'); 17 | overlay.style.opacity = ".9"; 18 | overlay.style.zIndex = "2147483648"; 19 | 20 | var overlayTextinput = document.createElement("input"); 21 | overlayTextinput.style.border = "0"; 22 | overlayTextinput.style.backgroundColor = "transparent"; 23 | overlayTextinput.style.width = "380px"; 24 | overlayTextinput.style.outline = "none"; 25 | overlayTextinput.style.color = "black"; 26 | overlayTextinput.style.margin = "0"; 27 | overlayTextinput.style.opacity = ".9"; 28 | overlayTextinput.style.clear = "none"; 29 | overlayTextinput.setAttribute('id', 'vimOverlayTextinput'); 30 | 31 | document.body.insertBefore(overlay, document.body.firstChild); 32 | overlay.appendChild(overlayTextinput); 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Mutwin Kraus, Jannik Nielsen 7 | CFBundleDisplayName 8 | vim 9 | CFBundleIdentifier 10 | com.mutwinkraus.vim 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleShortVersionString 14 | 0.6 15 | CFBundleVersion 16 | 2 17 | Chrome 18 | 19 | Database Quota 20 | 1048576 21 | Global Page 22 | globalpage.html 23 | 24 | Content 25 | 26 | Scripts 27 | 28 | End 29 | 30 | overlay.js 31 | 32 | Start 33 | 34 | vim-keybindings.js 35 | 36 | 37 | 38 | ExtensionInfoDictionaryVersion 39 | 1.0 40 | Permissions 41 | 42 | Website Access 43 | 44 | Include Secure Pages 45 | 46 | Level 47 | All 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /globalpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | global page 5 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /vim-keybindings.js: -------------------------------------------------------------------------------- 1 | var combokey = ''; 2 | var multiplier = 0; 3 | var t = {}; 4 | var timer; 5 | var loaded = false; 6 | 7 | var handler = function(e) { 8 | var c = String.fromCharCode(e.keyCode).toLowerCase(); 9 | if(e.shiftKey) c = c.toUpperCase(); 10 | if (e.keyCode > 32 && e.keyCode < 91) { 11 | if (parseInt(c, 10) == c) { 12 | multiplier = (multiplier * 10) + c; 13 | } else { 14 | combokey += c; 15 | } 16 | clearTimeout(timer); 17 | timer = window.setTimeout(function() { combokey = ''; multiplier = 0; }, 5000); 18 | } 19 | 20 | if(window.document.activeElement !== window.document.body) { 21 | switch (e.keyCode) { 22 | case 27: 23 | if (document.getElementById('vimOverlay').style.display == "block") { 24 | document.getElementById('vimOverlay').style.display = 'none'; 25 | document.getElementById('vimOverlayTextinput').value = ''; 26 | } else { 27 | t.lastActiveElement = window.document.activeElement; 28 | } 29 | combokey = ''; 30 | multiplier = 0; 31 | window.document.activeElement.blur(); 32 | break; 33 | case 8: 34 | if (window.document.activeElement.id == "vimOverlayTextinput" && window.document.activeElement.value == '') { 35 | window.document.activeElement.blur(); 36 | document.getElementById('vimOverlay').style.display = "none"; 37 | e.preventDefault(); 38 | } 39 | break; 40 | case 13: 41 | if (window.document.activeElement.id == "vimOverlayTextinput") { 42 | t.inputCommand(window.document.activeElement.value); 43 | window.document.activeElement.value = ''; 44 | window.document.activeElement.blur(); 45 | document.getElementById('vimOverlay').style.display = "none"; 46 | } 47 | break; 48 | } 49 | return; 50 | } else { 51 | 52 | switch (e.keyCode) { 53 | case 27: 54 | t.resetCombo(); 55 | break; 56 | } 57 | } 58 | 59 | t.scroll = function(x, y) { 60 | window.scrollBy(x, y); 61 | }; 62 | t.scrollTo = function(x, y) { 63 | window.scrollTo(x, y); 64 | }; 65 | 66 | t.halfWindowHeight = function() { 67 | return window.innerHeight / 2; 68 | }; 69 | 70 | t.fullWindowHeight = function() { 71 | return window.innerHeight; 72 | }; 73 | 74 | t.screenHeight = function() { 75 | return document.body.offsetHeight; 76 | }; 77 | 78 | t.functionkeys = function(keys) { 79 | if (keys.none == '1' && (e.altKey || e.metaKey || e.ctrlKey || e.altGraphKey || e.shiftKey)) { 80 | return false; 81 | } 82 | if ((keys.alt != '1' && e.altKey) || (keys.alt == '1' && !e.altKey)) { 83 | return false; 84 | } 85 | if ((keys.meta != '1' && e.metaKey) || (keys.meta == '1' && !e.metaKey)) { 86 | return false; 87 | } 88 | if ((keys.ctrl != '1' && e.ctrlKey) || (keys.ctrl == '1' && !e.ctrlKey)) { 89 | return false; 90 | } 91 | if ((keys.altgr != '1' && e.altGraphKey) || (keys.altgr == '1' && !e.altGraphKey)) { 92 | return false; 93 | } 94 | if ((keys.shift != '1' && e.shiftKey) || (keys.shift == '1' && !e.shiftKey)) { 95 | return false; 96 | } 97 | return true; 98 | } 99 | 100 | switch (e.keyIdentifier) { 101 | case "U+003A": 102 | document.getElementById('vimOverlay').style.display = "block"; 103 | document.getElementById('vimOverlayTextinput').focus(); 104 | e.preventDefault(); 105 | break; 106 | case "U+0008": 107 | if (document.getElementById('vimOverlay').style.display == "block" && document.getElementById('vimOverlayTextinput').value == '') { 108 | document.getElementById('vimOverlayTextinput').blur(); 109 | document.getElementById('vimOverlay').style.display = "none"; 110 | } 111 | break; 112 | case "U+0027": 113 | combokey += "'"; 114 | break; 115 | } 116 | 117 | t.keyCommand(combokey, e); 118 | } 119 | 120 | t.keyCommand = function(c, e) { 121 | 122 | var reset_combo = true; 123 | var SCROLL_STEP = 35; 124 | 125 | switch(c) { 126 | case 'gg': 127 | if (t.functionkeys({'none': '1'})) { 128 | t.scrollTo(0,0); 129 | } 130 | break; 131 | case 'h': 132 | if (t.functionkeys({'none': '1'})) { 133 | t.scroll(-SCROLL_STEP, 0); 134 | } 135 | break; 136 | case 'j': 137 | if (t.functionkeys({'none': '1'})) { 138 | t.scroll(0, SCROLL_STEP); 139 | } 140 | break; 141 | case 'k': 142 | if (t.functionkeys({'none': '1'})) { 143 | t.scroll(0, -SCROLL_STEP); 144 | } 145 | break; 146 | case 'l': 147 | if (t.functionkeys({'none': '1'})) { 148 | t.scroll(SCROLL_STEP, 0); 149 | } 150 | break; 151 | case 'd': 152 | if (t.functionkeys({'ctrl': '1'})) { 153 | t.scroll(0, t.halfWindowHeight()); 154 | } else { 155 | reset_combo = false; 156 | } 157 | break; 158 | case 'dd': 159 | if (t.functionkeys({'none': '1'}) && t.lastActiveElement != undefined) { 160 | t.lastActiveElement.value = ''; 161 | } 162 | break; 163 | case 'f': 164 | if(t.functionkeys({'ctrl': '1'})) { 165 | t.scroll(0, t.fullWindowHeight()); 166 | } 167 | break; 168 | case 'u': 169 | if(t.functionkeys({'ctrl': '1'})) { 170 | t.scroll(0, -t.halfWindowHeight()); 171 | } 172 | break; 173 | case 'b': 174 | if(t.functionkeys({'ctrl': '1'})) { 175 | t.scroll(0, -t.fullWindowHeight()); 176 | } 177 | break; 178 | case 'G': 179 | if (t.functionkeys({'shift': '1'})) { 180 | t.scrollTo(0, t.screenHeight()); 181 | } 182 | break; 183 | case 'i': 184 | if (t.lastActiveElement != undefined) { 185 | t.lastActiveElement.focus(); 186 | e.preventDefault(); 187 | } 188 | break; 189 | case 'gT': 190 | if (t.functionkeys({'shift': '1'})) { 191 | safari.self.tab.dispatchMessage("prevTab",""); 192 | } 193 | break; 194 | case 'gt': 195 | if (t.functionkeys({'none': '1'})) { 196 | safari.self.tab.dispatchMessage("nextTab",multiplier); 197 | } 198 | break; 199 | /*case '\'\'': 200 | if (t.functionkeys({'none': '1'})) { 201 | safari.self.tab.dispatchMessage("backTab", ""); 202 | } 203 | break;*/ 204 | 205 | default: 206 | reset_combo = false; 207 | break; 208 | } 209 | 210 | if (reset_combo || c.length > 4) { 211 | t.resetCombo(); 212 | } 213 | 214 | } 215 | 216 | t.inputCommand = function(command) { 217 | if (command == '') return; 218 | 219 | if (command.charAt(0) == "%") { 220 | t.percentCommand(command); 221 | return; 222 | } 223 | 224 | param = command.split(" "); 225 | 226 | switch (param[0]) { 227 | case 'tabe': 228 | case 'tabedit': 229 | case 'e': 230 | case 'edit': 231 | if (param[1] == "" || param[1] == undefined) { 232 | if (param[0] == 'e' || param[0] == 'edit') { 233 | alert('Usage: command "edit" or "e" for short, opens the url specified as first parameter in the current tab'); 234 | } else { 235 | alert('Usage: command "tabedit" or "tabe" for short, opens a url specified as first parameter in a new tab'); 236 | } 237 | 238 | } else { 239 | var url = param[1] 240 | if (url.substr(0,5) != "http:" && url.substr(0,6) != "https:") { 241 | url = "http://" + url; 242 | } 243 | if (param[0] == 'tabe' || param[0] == 'tabedit') { 244 | safari.self.tab.dispatchMessage("openTab",url); 245 | } else { 246 | location.href = url; 247 | } 248 | } 249 | break; 250 | 251 | case 'q': 252 | safari.self.tab.dispatchMessage("closeTab"); 253 | break; 254 | 255 | case 'qa': 256 | safari.self.tab.dispatchMessage("closeWindow"); 257 | break; 258 | 259 | case 'tabn': 260 | safari.self.tab.dispatchMessage("nextTab",0); 261 | break; 262 | 263 | case 'tabp': 264 | safari.self.tab.dispatchMessage("prevTab",""); 265 | break; 266 | 267 | case 'tabfirst': 268 | case 'tabfir': 269 | safari.self.tab.dispatchMessage("nextTab",1); 270 | break; 271 | 272 | case 'tablast': 273 | safari.self.tab.dispatchMessage("nextTab","last"); 274 | break; 275 | 276 | case 'tabnew': 277 | safari.self.tab.dispatchMessage("newTab",""); 278 | break; 279 | 280 | } 281 | } 282 | 283 | t.percentCommand = function(command) { 284 | param = command.split("/"); 285 | 286 | switch (param[0]) { 287 | case "%s": 288 | if (t.lastActiveElement == undefined) break; 289 | if (param.length == 3 || param.length == 4) { 290 | var mod = ""; 291 | if (param.length == 4) mod = param[3]; 292 | var regex = new RegExp(param[1], mod); 293 | t.lastActiveElement.value = t.lastActiveElement.value.replace(regex, param[2]); 294 | } 295 | break; 296 | } 297 | } 298 | 299 | t.resetCombo = function() { 300 | combokey = ''; 301 | multiplier = 0; 302 | } 303 | 304 | t.disable = function() { 305 | window.document.removeEventListener("keydown", handler, false); 306 | } 307 | 308 | function getAnswer(theMessageEvent) { 309 | switch (theMessageEvent.name) { 310 | case "resetcombo": 311 | t.resetCombo(); 312 | break; 313 | 314 | case "disable": 315 | if (loaded) { 316 | t.disable(); 317 | } 318 | break; 319 | 320 | case "load": 321 | if (!loaded) { 322 | loaded = true; 323 | window.document.addEventListener("keydown", handler); 324 | } 325 | break; 326 | } 327 | } 328 | safari.self.addEventListener("message", getAnswer, false); 329 | 330 | safari.self.tab.dispatchMessage("disabledSites",""); 331 | --------------------------------------------------------------------------------