CharReader.java

  1. /*
  2.  * Copyright (c) 2016, Stein Eldar Johnsen
  3.  *
  4.  * Licensed to the Apache Software Foundation (ASF) under one
  5.  * or more contributor license agreements. See the NOTICE file
  6.  * distributed with this work for additional information
  7.  * regarding copyright ownership. The ASF licenses this file
  8.  * to you under the Apache License, Version 2.0 (the
  9.  * "License"); you may not use this file except in compliance
  10.  * with the License. You may obtain a copy of the License at
  11.  *
  12.  *   http://www.apache.org/licenses/LICENSE-2.0
  13.  *
  14.  * Unless required by applicable law or agreed to in writing,
  15.  * software distributed under the License is distributed on an
  16.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  17.  * KIND, either express or implied. See the License for the
  18.  * specific language governing permissions and limitations
  19.  * under the License.
  20.  */
  21. package net.morimekta.strings.chr;


  22. import net.morimekta.strings.io.Utf8StreamReader;

  23. import java.io.IOException;
  24. import java.io.InputStream;
  25. import java.io.Reader;

  26. import static net.morimekta.strings.EscapeUtil.javaEscape;
  27. import static net.morimekta.strings.chr.Unicode.unicode;

  28. /**
  29.  * A keystroke char reader. returns a Char object at a time related to the
  30.  * single keystroke the user typed. Note that since this does not implement
  31.  * the {@link Reader} base class, it does not belong in the 'io' package.
  32.  */
  33. public class CharReader {
  34.     private final Reader in;

  35.     /**
  36.      * Create a char reader.
  37.      *
  38.      * @param in Input stream to read from.
  39.      */
  40.     public CharReader(InputStream in) {
  41.         this(new Utf8StreamReader(in));
  42.     }

  43.     /**
  44.      * Create a char reader.
  45.      *
  46.      * @param in Reader to read from.
  47.      */
  48.     public CharReader(Reader in) {
  49.         this.in = in;
  50.     }

  51.     /**
  52.      * Read a single char if ready (something is available in the input
  53.      * stream), otherwise return null.
  54.      *
  55.      * @return The read char or null of not ready.
  56.      * @throws IOException If failed to read from input.
  57.      */
  58.     public Char readIfAvailable() throws IOException {
  59.         if (in.ready()) {
  60.             return read();
  61.         }
  62.         return null;
  63.     }

  64.     /**
  65.      * Read the next char.
  66.      *
  67.      * @return The next char, or null of input stream is closed.
  68.      * @throws IOException If unable to read a char.
  69.      */
  70.     public Char read() throws IOException {
  71.         int cp = in.read();
  72.         if (cp < 0) {
  73.             return null;
  74.         }

  75.         if (cp == '\033') {
  76.             // We have received an 'esc' and nothing else...
  77.             if (!in.ready()) {
  78.                 return unicode(cp);
  79.             }

  80.             // 'esc' was last char in stream.
  81.             int c2 = in.read();
  82.             if (c2 < 0) {
  83.                 return unicode(cp);
  84.             }

  85.             StringBuilder charBuilder = new StringBuilder();
  86.             charBuilder.append((char) cp);
  87.             charBuilder.append((char) c2);

  88.             if (c2 == '\033') {
  89.                 // Treat double 'esc' as a single 'esc'. Otherwise pressing 'esc'
  90.                 // will consistently crash the application.
  91.                 return unicode(cp);
  92.             } else if (c2 == '[') {
  93.                 char c3 = expect();
  94.                 charBuilder.append(c3);
  95.                 if ('A' <= c3 && c3 <= 'Z') {
  96.                     // \033 [ A-Z
  97.                     return new Control(charBuilder.toString());
  98.                 }
  99.                 if (c3 == '?') {
  100.                     c3 = expect();
  101.                     charBuilder.append(c3);
  102.                 }
  103.                 while (('0' <= c3 && c3 <= '9') || c3 == ';') {
  104.                     c3 = expect();
  105.                     charBuilder.append(c3);
  106.                 }
  107.                 if (c3 == '~' ||
  108.                     ('a' <= c3 && c3 <= 'z') ||
  109.                     ('A' <= c3 && c3 <= 'Z')) {
  110.                     // \033 [ (number) ~ (F1, F2 ... Fx)
  111.                     // \033 [ (number...;) [A-D] (numbered cursor movement)
  112.                     // \033 [ (number...;) [su] (cursor save / restore, ...)
  113.                     // \033 [ (number...;) m (color)
  114.                     if (c3 == 'm') {
  115.                         try {
  116.                             return new Color(charBuilder.toString());
  117.                         } catch (IllegalArgumentException e) {
  118.                             throw new IOException(e.getMessage(), e);
  119.                         }
  120.                     }
  121.                     return new Control(charBuilder.toString());
  122.                 }
  123.             } else if (c2 == 'O') {
  124.                 char c3 = expect();
  125.                 charBuilder.append(c3);
  126.                 if ('A' <= c3 && c3 <= 'Z') {
  127.                     // \033 O [A-Z]
  128.                     return new Control(charBuilder.toString());
  129.                 }
  130.             } else if (('a' <= c2 && c2 <= 'z') ||
  131.                        ('0' <= c2 && c2 <= '9') ||
  132.                        ('A' <= c2 && c2 <= 'Z')) {
  133.                 // \033 [a-z]: <alt-{c}> aka <M-{c}>.
  134.                 // \033 [0-9]: <alt-{c}> aka <M-{c}>.
  135.                 // \033 [A-N]: <alt-shift-{c}> aka <M-S-{c}>.
  136.                 // \033 [P-Z]: <alt-shift-{c}> aka <M-S-{c}>.
  137.                 return new Control(charBuilder.toString());
  138.             }

  139.             throw new IOException("Invalid escape sequence: \"" + javaEscape(charBuilder.toString()) + "\"");
  140.         } else {
  141.             // Make sure to consume both surrogates on 32-bit code-points.
  142.             if (Character.isHighSurrogate((char) cp)) {
  143.                 cp = Character.toCodePoint((char) cp, expect());
  144.             }
  145.             return unicode(cp);
  146.         }
  147.     }

  148.     private char expect() throws IOException {
  149.         int cp = in.read();
  150.         if (cp < 0) {
  151.             throw new IOException("Unexpected end of stream.");
  152.         }
  153.         return (char) cp;
  154.     }
  155. }