Skip to content

Commit

Permalink
Issue #422: Adding a native Win32 Terminal implementation, replying o…
Browse files Browse the repository at this point in the history
…n jna being available
  • Loading branch information
rednoah authored and mabe02 committed Jun 14, 2020
1 parent 0e14e59 commit 60d8d41
Show file tree
Hide file tree
Showing 6 changed files with 524 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ public TerminalScreen createScreen() throws IOException {

private Terminal createWindowsTerminal() throws IOException {
try {
Class<?> nativeImplementation = Class.forName("com.googlecode.lanterna.terminal.WindowsTerminal");
Class<?> nativeImplementation = Class.forName("com.googlecode.lanterna.terminal.win32.WindowsTerminal");
Constructor<?> constructor = nativeImplementation.getConstructor(InputStream.class, OutputStream.class, Charset.class, UnixLikeTTYTerminal.CtrlCBehaviour.class);
return (Terminal)constructor.newInstance(inputStream, outputStream, charset, UnixLikeTTYTerminal.CtrlCBehaviour.CTRL_C_KILLS_APPLICATION);
}
Expand Down
139 changes: 139 additions & 0 deletions src/main/java/com/googlecode/lanterna/terminal/win32/WinDef.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.googlecode.lanterna.terminal.win32;

import com.sun.jna.Structure;
import com.sun.jna.Structure.FieldOrder;
import com.sun.jna.Union;

public interface WinDef extends com.sun.jna.platform.win32.WinDef {

/**
* COORD structure
*/
@FieldOrder({ "X", "Y" })
class COORD extends Structure {

public short X;
public short Y;

@Override
public String toString() {
return String.format("COORD(%s,%s)", X, Y);
}
}

/**
* SMALL_RECT structure
*/
@FieldOrder({ "Left", "Top", "Right", "Bottom" })
class SMALL_RECT extends Structure {

public short Left;
public short Top;
public short Right;
public short Bottom;

@Override
public String toString() {
return String.format("SMALL_RECT(%s,%s)(%s,%s)", Left, Top, Right, Bottom);
}
}

/**
* CONSOLE_SCREEN_BUFFER_INFO structure
*/
@FieldOrder({ "dwSize", "dwCursorPosition", "wAttributes", "srWindow", "dwMaximumWindowSize" })
class CONSOLE_SCREEN_BUFFER_INFO extends Structure {

public COORD dwSize;
public COORD dwCursorPosition;
public short wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;

@Override
public String toString() {
return String.format("CONSOLE_SCREEN_BUFFER_INFO(%s,%s,%s,%s,%s)", dwSize, dwCursorPosition, wAttributes, srWindow, dwMaximumWindowSize);
}
}

@FieldOrder({ "EventType", "Event" })
class INPUT_RECORD extends Structure {

public static final short KEY_EVENT = 0x01;
public static final short MOUSE_EVENT = 0x02;
public static final short WINDOW_BUFFER_SIZE_EVENT = 0x04;

public short EventType;
public Event Event;

public static class Event extends Union {
public KEY_EVENT_RECORD KeyEvent;
public MOUSE_EVENT_RECORD MouseEvent;
public WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
}

@Override
public void read() {
super.read();
switch (EventType) {
case KEY_EVENT:
Event.setType("KeyEvent");
break;
case MOUSE_EVENT:
Event.setType("MouseEvent");
break;
case WINDOW_BUFFER_SIZE_EVENT:
Event.setType("WindowBufferSizeEvent");
break;
}
Event.read();
}

@Override
public String toString() {
return String.format("INPUT_RECORD(%s)", EventType);
}
}

@FieldOrder({ "bKeyDown", "wRepeatCount", "wVirtualKeyCode", "wVirtualScanCode", "uChar", "dwControlKeyState" })
class KEY_EVENT_RECORD extends Structure {

public boolean bKeyDown;
public short wRepeatCount;
public short wVirtualKeyCode;
public short wVirtualScanCode;
public char uChar;
public int dwControlKeyState;

@Override
public String toString() {
return String.format("KEY_EVENT_RECORD(%s,%s,%s,%s,%s,%s)", bKeyDown, wRepeatCount, wVirtualKeyCode, wVirtualKeyCode, wVirtualScanCode, uChar, dwControlKeyState);
}
}

@FieldOrder({ "dwMousePosition", "dwButtonState", "dwControlKeyState", "dwEventFlags" })
class MOUSE_EVENT_RECORD extends Structure {

public COORD dwMousePosition;
public int dwButtonState;
public int dwControlKeyState;
public int dwEventFlags;

@Override
public String toString() {
return String.format("MOUSE_EVENT_RECORD(%s,%s,%s,%s)", dwMousePosition, dwButtonState, dwControlKeyState, dwEventFlags);
}
}

@FieldOrder({ "dwSize" })
class WINDOW_BUFFER_SIZE_RECORD extends Structure {

public COORD dwSize;

@Override
public String toString() {
return String.format("WINDOW_BUFFER_SIZE_RECORD(%s)", dwSize);
}
}

}
24 changes: 24 additions & 0 deletions src/main/java/com/googlecode/lanterna/terminal/win32/Wincon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.googlecode.lanterna.terminal.win32;

import com.sun.jna.Native;
import com.sun.jna.platform.win32.WinDef.LPVOID;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;

public interface Wincon extends StdCallLibrary, com.sun.jna.platform.win32.Wincon {

Wincon INSTANCE = Native.load("kernel32", Wincon.class, W32APIOptions.UNICODE_OPTIONS);

int ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
int DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
int ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;

boolean GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, WinDef.CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);

boolean ReadConsoleInput(HANDLE hConsoleInput, WinDef.INPUT_RECORD[] lpBuffer, int nLength, IntByReference lpNumberOfEventsRead);

boolean WriteConsole(HANDLE hConsoleOutput, String lpBuffer, int nNumberOfCharsToWrite, IntByReference lpNumberOfCharsWritten, LPVOID lpReserved);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.googlecode.lanterna.terminal.win32;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.function.Consumer;

import com.googlecode.lanterna.terminal.win32.WinDef.INPUT_RECORD;
import com.googlecode.lanterna.terminal.win32.WinDef.KEY_EVENT_RECORD;
import com.googlecode.lanterna.terminal.win32.WinDef.MOUSE_EVENT_RECORD;
import com.googlecode.lanterna.terminal.win32.WinDef.WINDOW_BUFFER_SIZE_RECORD;
import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;

public class WindowsConsoleInputStream extends InputStream {

private final HANDLE hConsoleInput;
private final Charset encoderCharset;
private ByteBuffer buffer = ByteBuffer.allocate(0);

public WindowsConsoleInputStream(Charset encoderCharset) {
this(Wincon.INSTANCE.GetStdHandle(Wincon.STD_INPUT_HANDLE), encoderCharset);
}

public WindowsConsoleInputStream(HANDLE hConsoleInput, Charset encoderCharset) {
this.hConsoleInput = hConsoleInput;
this.encoderCharset = encoderCharset;
}

public HANDLE getHandle() {
return hConsoleInput;
}

public Charset getEncoderCharset() {
return encoderCharset;
}

private INPUT_RECORD[] readConsoleInput() throws IOException {
INPUT_RECORD[] lpBuffer = new INPUT_RECORD[64];
IntByReference lpNumberOfEventsRead = new IntByReference();
if (Wincon.INSTANCE.ReadConsoleInput(hConsoleInput, lpBuffer, lpBuffer.length, lpNumberOfEventsRead)) {
int n = lpNumberOfEventsRead.getValue();
return Arrays.copyOfRange(lpBuffer, 0, n);
}
throw new EOFException();
}

private int availableConsoleInput() {
IntByReference lpcNumberOfEvents = new IntByReference();
if (Wincon.INSTANCE.GetNumberOfConsoleInputEvents(hConsoleInput, lpcNumberOfEvents)) {
return lpcNumberOfEvents.getValue();
}
return 0;
}

@Override
public synchronized int read() throws IOException {
while (!buffer.hasRemaining()) {
buffer = readKeyEvents(true);
}

return buffer.get();
}

@Override
public synchronized int read(byte[] b, int offset, int length) throws IOException {
while (length > 0 && !buffer.hasRemaining()) {
buffer = readKeyEvents(true);
}

int n = Math.min(buffer.remaining(), length);
buffer.get(b, offset, n);
return n;
}

@Override
public synchronized int available() throws IOException {
if (buffer.hasRemaining()) {
return buffer.remaining();
}

buffer = readKeyEvents(false);
return buffer.remaining();
}

private ByteBuffer readKeyEvents(boolean blocking) throws IOException {
StringBuilder keyEvents = new StringBuilder();

if (blocking || availableConsoleInput() > 0) {
for (INPUT_RECORD i : readConsoleInput()) {
filter(i, keyEvents);
}
}

return encoderCharset.encode(CharBuffer.wrap(keyEvents));
}

private void filter(INPUT_RECORD input, Appendable keyEvents) throws IOException {
switch (input.EventType) {
case INPUT_RECORD.KEY_EVENT:
if (input.Event.KeyEvent.uChar != 0 && input.Event.KeyEvent.bKeyDown) {
keyEvents.append(input.Event.KeyEvent.uChar);
}
if (keyEventHandler != null) {
keyEventHandler.accept(input.Event.KeyEvent);
}
break;
case INPUT_RECORD.MOUSE_EVENT:
if (mouseEventHandler != null) {
mouseEventHandler.accept(input.Event.MouseEvent);
}
break;
case INPUT_RECORD.WINDOW_BUFFER_SIZE_EVENT:
if (windowBufferSizeEventHandler != null) {
windowBufferSizeEventHandler.accept(input.Event.WindowBufferSizeEvent);
}
break;
}
}

private Consumer<KEY_EVENT_RECORD> keyEventHandler = null;
private Consumer<MOUSE_EVENT_RECORD> mouseEventHandler = null;
private Consumer<WINDOW_BUFFER_SIZE_RECORD> windowBufferSizeEventHandler = null;

public void onKeyEvent(Consumer<KEY_EVENT_RECORD> handler) {
if (keyEventHandler == null) {
keyEventHandler = handler;
} else {
keyEventHandler = keyEventHandler.andThen(handler);
}
}

public void onMouseEvent(Consumer<MOUSE_EVENT_RECORD> handler) {
if (mouseEventHandler == null) {
mouseEventHandler = handler;
} else {
mouseEventHandler = mouseEventHandler.andThen(handler);
}
}

public void onWindowBufferSizeEvent(Consumer<WINDOW_BUFFER_SIZE_RECORD> handler) {
if (windowBufferSizeEventHandler == null) {
windowBufferSizeEventHandler = handler;
} else {
windowBufferSizeEventHandler = windowBufferSizeEventHandler.andThen(handler);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.googlecode.lanterna.terminal.win32;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;

import com.sun.jna.platform.win32.WinNT.HANDLE;
import com.sun.jna.ptr.IntByReference;

public class WindowsConsoleOutputStream extends OutputStream {

private final HANDLE hConsoleOutput;
private final Charset decoderCharset;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

public WindowsConsoleOutputStream(Charset decoder) {
this(Wincon.INSTANCE.GetStdHandle(Wincon.STD_OUTPUT_HANDLE), decoder);
}

public WindowsConsoleOutputStream(HANDLE hConsoleOutput, Charset decoderCharset) {
this.hConsoleOutput = hConsoleOutput;
this.decoderCharset = decoderCharset;
}

public HANDLE getHandle() {
return hConsoleOutput;
}

public Charset getDecoderCharset() {
return decoderCharset;
}


@Override
public synchronized void write(int b) {
buffer.write(b);
}

@Override
public synchronized void write(byte[] b, int off, int len) {
buffer.write(b, off, len);
}

@Override
public synchronized void flush() throws IOException {
String characters = buffer.toString(decoderCharset.name());
buffer.reset();

IntByReference lpNumberOfCharsWritten = new IntByReference();
while (!characters.isEmpty()) {
if (!Wincon.INSTANCE.WriteConsole(hConsoleOutput, characters, characters.length(), lpNumberOfCharsWritten, null)) {
throw new EOFException();
}
characters = characters.substring(lpNumberOfCharsWritten.getValue());
}
}

}
Loading

0 comments on commit 60d8d41

Please sign in to comment.