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 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         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 }