View Javadoc

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