├── .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 |
--------------------------------------------------------------------------------