I have a program that works on a console, and i want to make a custom console for it. Current command line interface can be started with a method that takes an InputStream and PrintStream as arguments.
I have two text areas (JTextArea), one of which i want to use for input and the other one for output. I’ve extended InputStream and OutputStreams to provide streams to my starting method:
public class ConsoleInputStream extends InputStream implements KeyListener {
private BlockingDeque<Integer> mBuffer = new LinkedBlockingDeque<>();
private JTextArea mJTextArea;
public ConsoleInputStream(JTextArea JTextArea) {
mJTextArea = JTextArea;
mJTextArea.addKeyListener(this);
}
@Override
public void keyTyped(KeyEvent e) {}
@Override
public void keyPressed(KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {
int key = e.getKeyChar();
char c = (char) key;
mBuffer.add(key);
}
@Override
public int read() {
try {
char c = (char) (int) mBuffer.take();
if(c == 'n')
mJTextArea.setText("");
return c;
} catch (InterruptedException e) {
e.printStackTrace();
}
return 0;
}
@Override
public int read(byte[] b, int off, int len) {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len && available() > 0 ; i++) {
c = read();
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException e) {
}
return i;
}
}
And for the output:
public class ConsoleOutputStream extends OutputStream {
private JTextArea mJTextArea;
public ConsoleOutputStream(JTextArea JTextArea) {
mJTextArea = JTextArea;
}
@Override
public void write(int b) throws IOException {
mJTextArea.append(String.valueOf((char) b));
}
}
Start the program:
CommandInterface.get().start(ui.getConsoleIn(), new PrintStream(ui.getConsoleOut()));
(ui is an instance of a class that extends JFrame, the getConsoleIn() and getConsoleOut() return an instance of ConsoleInputStream and ConsoleOutputStream)
Inside of which i use scanner to read the input stream:
public void start(InputStream inputStream, PrintStream outputStream){
Scanner scanner = new Scanner(inputStream, "UTF-8");
while (true){
String[] input = scanner.nextLine().split(" ");
if(input[0].equals("exit"))
break;
Command command = mCommands.get(input[0]);
if(command == null){
displayErrorMessage("No such command", outputStream);
continue;
}
List<String> flags = new LinkedList<>();
List<String> params = new LinkedList<>();
for(String s : Arrays.copyOfRange(input, 1, input.length)){
if(s.charAt(0) == '/')
flags.add(s.substring(1));
else
params.add(s);
}
command.execute(outputStream, flags, params);
}
}
And this works fine, untill I try to use the local characters: ś ć ó ż ź etc.
I have tried many diffrent solutions, none worked for me. Then I tried to figure it out myself. Every time I read a char I also printed it to standard output (my IDE), which I know can display those characters correctly. I found out that they are being read correctly, but thre are characters (UTF-8 65535) inbetween them (not in a regular pattern but in pairs), for reasons that are unclear to me. I also tried:
Scanner scanner = new Scanner(System.in);
while (true){
ui.getConsoleOut().write(scanner.nextLine().getBytes(StandardCharsets.UTF_8));
}
with diffrent charsets, but couldn’t get them do display correctly.
What is the proper way to display those (and other UTF-8) characters?
Advertisement
Answer
I’m not sure whether you have done anything else wrong, but I know you at least need to fix this:
read
and write
methods don’t work with characters, they work with bytes. One character != one byte.
I’m talking about these:
public int read() {
try {
char c = (char) (int) mBuffer.take();
if(c == 'n')
mJTextArea.setText("");
return c;
} catch (InterruptedException e) {
e.printStackTrace();
}
return 0;
}
public void write(int b) throws IOException {
mJTextArea.append(String.valueOf((char) b));
}
You need to turn the char
into a byte
array using an encoding that the Scanner can understand. Then turn each of those bytes to unsigned ints, as opposed to treating each character as a single byte.
public void keyReleased(KeyEvent e) {
int key = e.getKeyChar();
char c = (char) key;
if(c == 'n')
mJTextArea.setText("");
byte[] byteArray = Character.toString(c).getBytes(StandardCharset.UTF_8);
for (byte b : byteArray) {
mBuffer.add(Byte.toUnsignedInt(b));
}
}
public int read() {
try {
byte b = (int) mBuffer.take();
return b;
} catch (InterruptedException e) {
e.printStackTrace();
}
return -1;
}
For write
, you can’t treat each byte as a single character either. One way to handle this is to subclass PrintStream
directly. See solution 2 in this answer for an example.