Skip to content

Commit

Permalink
Cursor reporting support, fixes #40
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Nov 16, 2016
1 parent 80b798e commit 9ea4722
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/main/java/org/jline/terminal/Cursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2002-2016, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.terminal;

/**
* Class holding the cursor position.
*
* @see Terminal#getCursorPosition(java.util.function.IntConsumer)
*/
public class Cursor {

private final int x;
private final int y;

public Cursor(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public int getY() {
return y;
}

@Override
public boolean equals(Object o) {
if (o instanceof Cursor) {
Cursor c = (Cursor) o;
return x == c.x && y == c.y;
} else {
return false;
}
}

@Override
public int hashCode() {
return x * 31 + y;
}

@Override
public String toString() {
return "Cursor[" + "x=" + x + ", y=" + y + ']';
}
}
19 changes: 19 additions & 0 deletions src/main/java/org/jline/terminal/Terminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.function.IntConsumer;

import org.jline.terminal.impl.NativeSignalHandler;
import org.jline.utils.InfoCmp.Capability;
Expand Down Expand Up @@ -112,4 +113,22 @@ default int getHeight() {

String getStringCapability(Capability capability);

//
// Cursor support
//

/**
* Query the terminal to report the cursor position.
*
* As the response is read from the input stream, some
* characters may be read before the cursor position is actually
* read. Those characters can be given back using
* {@link org.jline.keymap.BindingReader#runMacro(String)}.
*
* @param discarded a consumer receiving discarded characters
* @return <code>null</code> if cursor position reporting
* is not supported or a valid cursor position
*/
Cursor getCursorPosition(IntConsumer discarded);

}
16 changes: 16 additions & 0 deletions src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
/*
* Copyright (c) 2002-2016, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.terminal.impl;

import java.io.IOError;
import java.io.IOException;
import java.util.Objects;
import java.util.function.IntConsumer;

import org.jline.terminal.Attributes;
import org.jline.terminal.Cursor;
import org.jline.terminal.Size;

public abstract class AbstractPosixTerminal extends AbstractTerminal {
Expand Down Expand Up @@ -63,4 +73,10 @@ public void close() throws IOException {
pty.setAttr(originalAttributes);
pty.close();
}

@Override
public Cursor getCursorPosition(IntConsumer discarded) {
return CursorSupport.getCursorPosition(this, discarded);
}

}
7 changes: 7 additions & 0 deletions src/main/java/org/jline/terminal/impl/AbstractTerminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.IntConsumer;

import org.jline.terminal.Attributes;
import org.jline.terminal.Attributes.ControlChar;
import org.jline.terminal.Attributes.InputFlag;
import org.jline.terminal.Attributes.LocalFlag;
import org.jline.terminal.Cursor;
import org.jline.terminal.Terminal;
import org.jline.utils.Curses;
import org.jline.utils.InfoCmp;
Expand Down Expand Up @@ -165,4 +167,9 @@ protected void parseInfoCmp() {
InfoCmp.parseInfoCmp(capabilities, bools, ints, strings);
}

@Override
public Cursor getCursorPosition(IntConsumer discarded) {
return null;
}

}
109 changes: 109 additions & 0 deletions src/main/java/org/jline/terminal/impl/CursorSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (c) 2002-2016, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package org.jline.terminal.impl;

import org.jline.terminal.Cursor;
import org.jline.terminal.Terminal;
import org.jline.utils.Curses;
import org.jline.utils.InfoCmp;

import java.io.IOError;
import java.io.IOException;
import java.util.function.IntConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CursorSupport {

public static Cursor getCursorPosition(Terminal terminal, IntConsumer discarded) {
try {
String u6 = terminal.getStringCapability(InfoCmp.Capability.user6);
String u7 = terminal.getStringCapability(InfoCmp.Capability.user7);
if (u6 == null || u7 == null) {
return null;
}
// Prepare parser
boolean inc1 = false;
StringBuilder patb = new StringBuilder();
int index = 0;
while (index < u6.length()) {
char ch;
switch (ch = u6.charAt(index++)) {
case '\\':
switch (u6.charAt(index++)) {
case 'e':
case 'E':
patb.append("\\x1b");
break;
default:
throw new IllegalArgumentException();
}
break;
case '%':
ch = u6.charAt(index++);
switch (ch) {
case '%':
patb.append('%');
break;
case 'i':
inc1 = true;
break;
case 'd':
patb.append("([0-9]+)");
break;
default:
throw new IllegalArgumentException();
}
break;
default:
switch (ch) {
case '[':
patb.append('\\');
break;
}
patb.append(ch);
break;
}
}
Pattern pattern = Pattern.compile(patb.toString());
// Output cursor position request
Curses.tputs(terminal.writer(), u7);
terminal.flush();
StringBuilder sb = new StringBuilder();
int start = 0;
while (true) {
int c = terminal.reader().read();
if (c < 0) {
return null;
}
sb.append((char) c);
Matcher matcher = pattern.matcher(sb.substring(start));
if (matcher.matches()) {
int y = Integer.parseInt(matcher.group(1));
int x = Integer.parseInt(matcher.group(2));
if (inc1) {
x--;
y--;
}
if (discarded != null) {
for (int i = 0; i < start; i++) {
discarded.accept(sb.charAt(i));
}
}
return new Cursor(x, y);
} else if (!matcher.hitEnd()) {
start++;
}
}
} catch (IOException e) {
throw new IOError(e);
}
}

}
8 changes: 8 additions & 0 deletions src/main/java/org/jline/terminal/impl/ExternalTerminal.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
*/
package org.jline.terminal.impl;

import org.jline.terminal.Cursor;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.IntConsumer;

/**
* Console implementation with embedded line disciplined.
Expand Down Expand Up @@ -74,4 +77,9 @@ public void pump() {
}
}

@Override
public Cursor getCursorPosition(IntConsumer discarded) {
return CursorSupport.getCursorPosition(this, discarded);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,26 @@

import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.util.function.IntConsumer;

import org.fusesource.jansi.WindowsAnsiOutputStream;
import org.fusesource.jansi.internal.Kernel32;
import org.fusesource.jansi.internal.Kernel32.CONSOLE_SCREEN_BUFFER_INFO;
import org.fusesource.jansi.internal.Kernel32.INPUT_RECORD;
import org.fusesource.jansi.internal.Kernel32.KEY_EVENT_RECORD;
import org.fusesource.jansi.internal.WindowsSupport;
import org.jline.terminal.Cursor;
import org.jline.terminal.Size;
import org.jline.terminal.impl.AbstractWindowsTerminal;
import org.jline.utils.InfoCmp;
import org.jline.utils.Log;

import static org.fusesource.jansi.internal.Kernel32.GetConsoleScreenBufferInfo;
import static org.fusesource.jansi.internal.Kernel32.GetStdHandle;
import static org.fusesource.jansi.internal.Kernel32.STD_OUTPUT_HANDLE;

public class JansiWinSysTerminal extends AbstractWindowsTerminal {

public JansiWinSysTerminal(String name, boolean nativeSignals) throws IOException {
Expand Down Expand Up @@ -114,4 +122,14 @@ protected byte[] readConsoleInput() {
return sb.toString().getBytes();
}

@Override
public Cursor getCursorPosition(IntConsumer discarded) {
CONSOLE_SCREEN_BUFFER_INFO info = new CONSOLE_SCREEN_BUFFER_INFO();
long console = GetStdHandle(STD_OUTPUT_HANDLE);
if (GetConsoleScreenBufferInfo(console, info) == 0) {
throw new IOError(new IOException("Could not get the cursor position: " + WindowsSupport.getLastErrorMessage()));
}
return new Cursor(info.cursorPosition.x, info.cursorPosition.y);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.function.IntConsumer;

import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import org.jline.terminal.Cursor;
import org.jline.terminal.Size;
import org.jline.terminal.impl.AbstractWindowsTerminal;
import org.jline.utils.InfoCmp;
Expand Down Expand Up @@ -132,4 +134,11 @@ private Kernel32.INPUT_RECORD[] doReadConsoleInput() throws IOException {
return null;
}

@Override
public Cursor getCursorPosition(IntConsumer discarded) {
Kernel32.CONSOLE_SCREEN_BUFFER_INFO info = new Kernel32.CONSOLE_SCREEN_BUFFER_INFO();
Kernel32.INSTANCE.GetConsoleScreenBufferInfo(consoleOut, info);
return new Cursor(info.dwCursorPosition.X, info.dwCursorPosition.Y);
}

}
19 changes: 19 additions & 0 deletions src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.jline.terminal.Attributes.InputFlag;
import org.jline.terminal.Attributes.LocalFlag;
import org.jline.terminal.Attributes.OutputFlag;
import org.jline.terminal.Cursor;
import org.jline.terminal.Terminal;
import org.junit.Test;

Expand Down Expand Up @@ -109,5 +110,23 @@ public void run() {
th.join();
}

@Test
public void testCursorPosition() throws IOException {
PipedInputStream in = new PipedInputStream();
final PipedOutputStream outIn = new PipedOutputStream(in);
ByteArrayOutputStream out = new ByteArrayOutputStream();
ExternalTerminal console = new ExternalTerminal("foo", "ansi", in, out, "UTF-8");

outIn.write(new byte[] { 'a', '\033', 'b', '\033', '[', '2', ';', '3', 'R', 'f'});
outIn.flush();

StringBuilder sb = new StringBuilder();
Cursor cursor = console.getCursorPosition(c -> sb.append((char) c));
assertNotNull(cursor);
assertEquals(1, cursor.getX());
assertEquals(2, cursor.getY());
assertEquals("a\033b", sb.toString());
assertEquals('f', console.reader().read());
}

}

0 comments on commit 9ea4722

Please sign in to comment.