1 /* |
|
2 * Copyright (c) 2002-2016, the original author or authors. |
|
3 * |
|
4 * This software is distributable under the BSD license. See the terms of the |
|
5 * BSD license in the documentation provided with this software. |
|
6 * |
|
7 * http://www.opensource.org/licenses/bsd-license.php |
|
8 */ |
|
9 package jdk.internal.jline.console; |
|
10 |
|
11 import java.io.BufferedReader; |
|
12 import java.io.File; |
|
13 import java.io.IOException; |
|
14 import java.io.InputStream; |
|
15 import java.net.URL; |
|
16 import java.util.ArrayList; |
|
17 import java.util.HashMap; |
|
18 import java.util.List; |
|
19 import java.util.Map; |
|
20 |
|
21 import jdk.internal.jline.internal.Log; |
|
22 |
|
23 /** |
|
24 * @author St\u00E5le W. Pedersen <stale.pedersen@jboss.org> |
|
25 */ |
|
26 public class ConsoleKeys { |
|
27 |
|
28 private KeyMap keys; |
|
29 |
|
30 private Map<String, KeyMap> keyMaps; |
|
31 private Map<String, String> variables = new HashMap<String,String>(); |
|
32 |
|
33 public ConsoleKeys(String appName, URL inputrcUrl) { |
|
34 keyMaps = KeyMap.keyMaps(); |
|
35 setVar("editing-mode", "emacs"); |
|
36 loadKeys(appName, inputrcUrl); |
|
37 String editingMode = variables.get("editing-mode"); |
|
38 if ("vi".equalsIgnoreCase(editingMode)) { |
|
39 keys = keyMaps.get(KeyMap.VI_INSERT); |
|
40 } else if ("emacs".equalsIgnoreCase(editingMode)) { |
|
41 keys = keyMaps.get(KeyMap.EMACS); |
|
42 } |
|
43 } |
|
44 |
|
45 protected boolean setKeyMap (String name) { |
|
46 KeyMap map = keyMaps.get(name); |
|
47 if (map == null) { |
|
48 return false; |
|
49 } |
|
50 this.keys = map; |
|
51 return true; |
|
52 } |
|
53 |
|
54 protected Map<String, KeyMap> getKeyMaps() { |
|
55 return keyMaps; |
|
56 } |
|
57 |
|
58 protected KeyMap getKeys() { |
|
59 return keys; |
|
60 } |
|
61 |
|
62 protected void setKeys(KeyMap keys) { |
|
63 this.keys = keys; |
|
64 } |
|
65 |
|
66 protected void loadKeys(String appName, URL inputrcUrl) { |
|
67 keys = keyMaps.get(KeyMap.EMACS); |
|
68 |
|
69 try { |
|
70 InputStream input = inputrcUrl.openStream(); |
|
71 try { |
|
72 loadKeys(input, appName); |
|
73 Log.debug("Loaded user configuration: ", inputrcUrl); |
|
74 } |
|
75 finally { |
|
76 try { |
|
77 input.close(); |
|
78 } catch (IOException e) { |
|
79 // Ignore |
|
80 } |
|
81 } |
|
82 } |
|
83 catch (IOException e) { |
|
84 if (inputrcUrl.getProtocol().equals("file")) { |
|
85 File file = new File(inputrcUrl.getPath()); |
|
86 if (file.exists()) { |
|
87 Log.warn("Unable to read user configuration: ", inputrcUrl, e); |
|
88 } |
|
89 } else { |
|
90 Log.warn("Unable to read user configuration: ", inputrcUrl, e); |
|
91 } |
|
92 } |
|
93 } |
|
94 |
|
95 private void loadKeys(InputStream input, String appName) throws IOException { |
|
96 BufferedReader reader = new BufferedReader( new java.io.InputStreamReader( input ) ); |
|
97 String line; |
|
98 boolean parsing = true; |
|
99 List<Boolean> ifsStack = new ArrayList<Boolean>(); |
|
100 while ( (line = reader.readLine()) != null ) { |
|
101 try { |
|
102 line = line.trim(); |
|
103 if (line.length() == 0) { |
|
104 continue; |
|
105 } |
|
106 if (line.charAt(0) == '#') { |
|
107 continue; |
|
108 } |
|
109 int i = 0; |
|
110 if (line.charAt(i) == '$') { |
|
111 String cmd; |
|
112 String args; |
|
113 for (++i; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++); |
|
114 int s = i; |
|
115 for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++); |
|
116 cmd = line.substring(s, i); |
|
117 for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++); |
|
118 s = i; |
|
119 for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++); |
|
120 args = line.substring(s, i); |
|
121 if ("if".equalsIgnoreCase(cmd)) { |
|
122 ifsStack.add( parsing ); |
|
123 if (!parsing) { |
|
124 continue; |
|
125 } |
|
126 if (args.startsWith("term=")) { |
|
127 // TODO |
|
128 } else if (args.startsWith("mode=")) { |
|
129 String mode = variables.get("editing-mode"); |
|
130 parsing = args.substring("mode=".length()).equalsIgnoreCase(mode); |
|
131 } else { |
|
132 parsing = args.equalsIgnoreCase(appName); |
|
133 } |
|
134 } else if ("else".equalsIgnoreCase(cmd)) { |
|
135 if (ifsStack.isEmpty()) { |
|
136 throw new IllegalArgumentException("$else found without matching $if"); |
|
137 } |
|
138 boolean invert = true; |
|
139 for (boolean b : ifsStack) { |
|
140 if (!b) { |
|
141 invert = false; |
|
142 break; |
|
143 } |
|
144 } |
|
145 if (invert) { |
|
146 parsing = !parsing; |
|
147 } |
|
148 } else if ("endif".equalsIgnoreCase(cmd)) { |
|
149 if (ifsStack.isEmpty()) { |
|
150 throw new IllegalArgumentException("endif found without matching $if"); |
|
151 } |
|
152 parsing = ifsStack.remove( ifsStack.size() - 1 ); |
|
153 } else if ("include".equalsIgnoreCase(cmd)) { |
|
154 // TODO |
|
155 } |
|
156 continue; |
|
157 } |
|
158 if (!parsing) { |
|
159 continue; |
|
160 } |
|
161 boolean equivalency; |
|
162 String keySeq = ""; |
|
163 if (line.charAt(i++) == '"') { |
|
164 boolean esc = false; |
|
165 for (;; i++) { |
|
166 if (i >= line.length()) { |
|
167 throw new IllegalArgumentException("Missing closing quote on line '" + line + "'"); |
|
168 } |
|
169 if (esc) { |
|
170 esc = false; |
|
171 } else if (line.charAt(i) == '\\') { |
|
172 esc = true; |
|
173 } else if (line.charAt(i) == '"') { |
|
174 break; |
|
175 } |
|
176 } |
|
177 } |
|
178 for (; i < line.length() && line.charAt(i) != ':' |
|
179 && line.charAt(i) != ' ' && line.charAt(i) != '\t' |
|
180 ; i++); |
|
181 keySeq = line.substring(0, i); |
|
182 equivalency = i + 1 < line.length() && line.charAt(i) == ':' && line.charAt(i + 1) == '='; |
|
183 i++; |
|
184 if (equivalency) { |
|
185 i++; |
|
186 } |
|
187 if (keySeq.equalsIgnoreCase("set")) { |
|
188 String key; |
|
189 String val; |
|
190 for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++); |
|
191 int s = i; |
|
192 for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++); |
|
193 key = line.substring( s, i ); |
|
194 for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++); |
|
195 s = i; |
|
196 for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++); |
|
197 val = line.substring( s, i ); |
|
198 setVar( key, val ); |
|
199 } else { |
|
200 for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++); |
|
201 int start = i; |
|
202 if (i < line.length() && (line.charAt(i) == '\'' || line.charAt(i) == '\"')) { |
|
203 char delim = line.charAt(i++); |
|
204 boolean esc = false; |
|
205 for (;; i++) { |
|
206 if (i >= line.length()) { |
|
207 break; |
|
208 } |
|
209 if (esc) { |
|
210 esc = false; |
|
211 } else if (line.charAt(i) == '\\') { |
|
212 esc = true; |
|
213 } else if (line.charAt(i) == delim) { |
|
214 break; |
|
215 } |
|
216 } |
|
217 } |
|
218 for (; i < line.length() && line.charAt(i) != ' ' && line.charAt(i) != '\t'; i++); |
|
219 String val = line.substring(Math.min(start, line.length()), Math.min(i, line.length())); |
|
220 if (keySeq.charAt(0) == '"') { |
|
221 keySeq = translateQuoted(keySeq); |
|
222 } else { |
|
223 // Bind key name |
|
224 String keyName = keySeq.lastIndexOf('-') > 0 ? keySeq.substring( keySeq.lastIndexOf('-') + 1 ) : keySeq; |
|
225 char key = getKeyFromName(keyName); |
|
226 keyName = keySeq.toLowerCase(); |
|
227 keySeq = ""; |
|
228 if (keyName.contains("meta-") || keyName.contains("m-")) { |
|
229 keySeq += "\u001b"; |
|
230 } |
|
231 if (keyName.contains("control-") || keyName.contains("c-") || keyName.contains("ctrl-")) { |
|
232 key = (char)(Character.toUpperCase( key ) & 0x1f); |
|
233 } |
|
234 keySeq += key; |
|
235 } |
|
236 if (val.length() > 0 && (val.charAt(0) == '\'' || val.charAt(0) == '\"')) { |
|
237 keys.bind( keySeq, translateQuoted(val) ); |
|
238 } else { |
|
239 String operationName = val.replace('-', '_').toUpperCase(); |
|
240 try { |
|
241 keys.bind(keySeq, Operation.valueOf(operationName)); |
|
242 } catch(IllegalArgumentException e) { |
|
243 Log.info("Unable to bind key for unsupported operation: ", val); |
|
244 } |
|
245 } |
|
246 } |
|
247 } catch (IllegalArgumentException e) { |
|
248 Log.warn("Unable to parse user configuration: ", e); |
|
249 } |
|
250 } |
|
251 } |
|
252 |
|
253 private static String translateQuoted(String keySeq) { |
|
254 int i; |
|
255 String str = keySeq.substring( 1, keySeq.length() - 1 ); |
|
256 keySeq = ""; |
|
257 for (i = 0; i < str.length(); i++) { |
|
258 char c = str.charAt(i); |
|
259 if (c == '\\') { |
|
260 boolean ctrl = str.regionMatches(i, "\\C-", 0, 3)|| str.regionMatches(i, "\\M-\\C-", 0, 6); |
|
261 boolean meta = str.regionMatches(i, "\\M-", 0, 3)|| str.regionMatches(i, "\\C-\\M-", 0, 6); |
|
262 i += (meta ? 3 : 0) + (ctrl ? 3 : 0) + (!meta && !ctrl ? 1 : 0); |
|
263 if (i >= str.length()) { |
|
264 break; |
|
265 } |
|
266 c = str.charAt(i); |
|
267 if (meta) { |
|
268 keySeq += "\u001b"; |
|
269 } |
|
270 if (ctrl) { |
|
271 c = c == '?' ? 0x7f : (char)(Character.toUpperCase( c ) & 0x1f); |
|
272 } |
|
273 if (!meta && !ctrl) { |
|
274 switch (c) { |
|
275 case 'a': c = 0x07; break; |
|
276 case 'b': c = '\b'; break; |
|
277 case 'd': c = 0x7f; break; |
|
278 case 'e': c = 0x1b; break; |
|
279 case 'f': c = '\f'; break; |
|
280 case 'n': c = '\n'; break; |
|
281 case 'r': c = '\r'; break; |
|
282 case 't': c = '\t'; break; |
|
283 case 'v': c = 0x0b; break; |
|
284 case '\\': c = '\\'; break; |
|
285 case '0': case '1': case '2': case '3': |
|
286 case '4': case '5': case '6': case '7': |
|
287 c = 0; |
|
288 for (int j = 0; j < 3; j++, i++) { |
|
289 if (i >= str.length()) { |
|
290 break; |
|
291 } |
|
292 int k = Character.digit(str.charAt(i), 8); |
|
293 if (k < 0) { |
|
294 break; |
|
295 } |
|
296 c = (char)(c * 8 + k); |
|
297 } |
|
298 c &= 0xFF; |
|
299 break; |
|
300 case 'x': |
|
301 i++; |
|
302 c = 0; |
|
303 for (int j = 0; j < 2; j++, i++) { |
|
304 if (i >= str.length()) { |
|
305 break; |
|
306 } |
|
307 int k = Character.digit(str.charAt(i), 16); |
|
308 if (k < 0) { |
|
309 break; |
|
310 } |
|
311 c = (char)(c * 16 + k); |
|
312 } |
|
313 c &= 0xFF; |
|
314 break; |
|
315 case 'u': |
|
316 i++; |
|
317 c = 0; |
|
318 for (int j = 0; j < 4; j++, i++) { |
|
319 if (i >= str.length()) { |
|
320 break; |
|
321 } |
|
322 int k = Character.digit(str.charAt(i), 16); |
|
323 if (k < 0) { |
|
324 break; |
|
325 } |
|
326 c = (char)(c * 16 + k); |
|
327 } |
|
328 break; |
|
329 } |
|
330 } |
|
331 keySeq += c; |
|
332 } else { |
|
333 keySeq += c; |
|
334 } |
|
335 } |
|
336 return keySeq; |
|
337 } |
|
338 |
|
339 private static char getKeyFromName(String name) { |
|
340 if ("DEL".equalsIgnoreCase(name) || "Rubout".equalsIgnoreCase(name)) { |
|
341 return 0x7f; |
|
342 } else if ("ESC".equalsIgnoreCase(name) || "Escape".equalsIgnoreCase(name)) { |
|
343 return '\033'; |
|
344 } else if ("LFD".equalsIgnoreCase(name) || "NewLine".equalsIgnoreCase(name)) { |
|
345 return '\n'; |
|
346 } else if ("RET".equalsIgnoreCase(name) || "Return".equalsIgnoreCase(name)) { |
|
347 return '\r'; |
|
348 } else if ("SPC".equalsIgnoreCase(name) || "Space".equalsIgnoreCase(name)) { |
|
349 return ' '; |
|
350 } else if ("Tab".equalsIgnoreCase(name)) { |
|
351 return '\t'; |
|
352 } else { |
|
353 return name.charAt(0); |
|
354 } |
|
355 } |
|
356 |
|
357 private void setVar(String key, String val) { |
|
358 if ("keymap".equalsIgnoreCase(key)) { |
|
359 if (keyMaps.containsKey(val)) { |
|
360 keys = keyMaps.get(val); |
|
361 } |
|
362 } else if ("blink-matching-paren".equals(key)) { |
|
363 if ("on".equalsIgnoreCase(val)) { |
|
364 keys.setBlinkMatchingParen(true); |
|
365 } else if ("off".equalsIgnoreCase(val)) { |
|
366 keys.setBlinkMatchingParen(false); |
|
367 } |
|
368 } |
|
369 |
|
370 /* |
|
371 * Technically variables should be defined as a functor class |
|
372 * so that validation on the variable value can be done at parse |
|
373 * time. This is a stop-gap. |
|
374 */ |
|
375 variables.put(key, val); |
|
376 } |
|
377 |
|
378 /** |
|
379 * Retrieves the value of a variable that was set in the .inputrc file |
|
380 * during processing |
|
381 * @param var The variable name |
|
382 * @return The variable value. |
|
383 */ |
|
384 public String getVariable(String var) { |
|
385 return variables.get (var); |
|
386 } |
|
387 } |
|