CharReader.java
/*
* Copyright (c) 2016, Stein Eldar Johnsen
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.strings.chr;
import net.morimekta.strings.io.Utf8StreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import static net.morimekta.strings.EscapeUtil.javaEscape;
import static net.morimekta.strings.chr.Unicode.unicode;
/**
* A keystroke char reader. returns a Char object at a time related to the
* single keystroke the user typed. Note that since this does not implement
* the {@link Reader} base class, it does not belong in the 'io' package.
*/
public class CharReader {
private final Reader in;
/**
* Create a char reader.
*
* @param in Input stream to read from.
*/
public CharReader(InputStream in) {
this(new Utf8StreamReader(in));
}
/**
* Create a char reader.
*
* @param in Reader to read from.
*/
public CharReader(Reader in) {
this.in = in;
}
/**
* Read a single char if ready (something is available in the input
* stream), otherwise return null.
*
* @return The read char or null of not ready.
* @throws IOException If failed to read from input.
*/
public Char readIfAvailable() throws IOException {
if (in.ready()) {
return read();
}
return null;
}
/**
* Read the next char.
*
* @return The next char, or null of input stream is closed.
* @throws IOException If unable to read a char.
*/
public Char read() throws IOException {
int cp = in.read();
if (cp < 0) {
return null;
}
if (cp == '\033') {
// We have received an 'esc' and nothing else...
if (!in.ready()) {
return unicode(cp);
}
// 'esc' was last char in stream.
int c2 = in.read();
if (c2 < 0) {
return unicode(cp);
}
StringBuilder charBuilder = new StringBuilder();
charBuilder.append((char) cp);
charBuilder.append((char) c2);
if (c2 == '\033') {
// Treat double 'esc' as a single 'esc'. Otherwise pressing 'esc'
// will consistently crash the application.
return unicode(cp);
} else if (c2 == '[') {
char c3 = expect();
charBuilder.append(c3);
if ('A' <= c3 && c3 <= 'Z') {
// \033 [ A-Z
return new Control(charBuilder.toString());
}
if (c3 == '?') {
c3 = expect();
charBuilder.append(c3);
}
while (('0' <= c3 && c3 <= '9') || c3 == ';') {
c3 = expect();
charBuilder.append(c3);
}
if (c3 == '~' ||
('a' <= c3 && c3 <= 'z') ||
('A' <= c3 && c3 <= 'Z')) {
// \033 [ (number) ~ (F1, F2 ... Fx)
// \033 [ (number...;) [A-D] (numbered cursor movement)
// \033 [ (number...;) [su] (cursor save / restore, ...)
// \033 [ (number...;) m (color)
if (c3 == 'm') {
try {
return new Color(charBuilder.toString());
} catch (IllegalArgumentException e) {
throw new IOException(e.getMessage(), e);
}
}
return new Control(charBuilder.toString());
}
} else if (c2 == 'O') {
char c3 = expect();
charBuilder.append(c3);
if ('A' <= c3 && c3 <= 'Z') {
// \033 O [A-Z]
return new Control(charBuilder.toString());
}
} else if (('a' <= c2 && c2 <= 'z') ||
('0' <= c2 && c2 <= '9') ||
('A' <= c2 && c2 <= 'Z')) {
// \033 [a-z]: <alt-{c}> aka <M-{c}>.
// \033 [0-9]: <alt-{c}> aka <M-{c}>.
// \033 [A-N]: <alt-shift-{c}> aka <M-S-{c}>.
// \033 [P-Z]: <alt-shift-{c}> aka <M-S-{c}>.
return new Control(charBuilder.toString());
}
throw new IOException("Invalid escape sequence: \"" + javaEscape(charBuilder.toString()) + "\"");
} else {
// Make sure to consume both surrogates on 32-bit code-points.
if (Character.isHighSurrogate((char) cp)) {
cp = Character.toCodePoint((char) cp, expect());
}
return unicode(cp);
}
}
private char expect() throws IOException {
int cp = in.read();
if (cp < 0) {
throw new IOException("Unexpected end of stream.");
}
return (char) cp;
}
}