JLine NonBlockingReader 的合同似乎已损坏
JLine the contract for NonBlockingReader seems broken
从 about JLine 开始。 OS:W10,使用 Cygwin。
def terminal = org.jline.terminal.TerminalBuilder.builder().jna( true ).system( true ).build()
terminal.enterRawMode()
// NB the Terminal I get is class org.jline.terminal.impl.PosixSysTerminal
def reader = terminal.reader()
// class org.jline.utils.NonBlocking$NonBlockingInputStreamReader
def bytes = [] // NB class ArrayList
int readInt = -1
while( readInt != 13 && readInt != 10 ) {
readInt = reader.read()
byte convertedByte = (byte)readInt
// see what the binary looks like:
String binaryString = String.format("%8s", Integer.toBinaryString( convertedByte & 0xFF)).replace(' ', '0')
println "binary |$binaryString|"
bytes << (byte)readInt // NB means "append to list"
// these seem to block forever, whatever the param...
// int peek = reader.peek( 50 )
int peek = reader.peek( 0 )
}
// strip final byte (13 or 10)
bytes = bytes[0..-2]
def response = new String( (byte[])bytes.toArray(), 'UTF-8' )
根据 Javadoc(从源代码本地制作)peek
看起来像这样:
public int peek(long timeout)
Peeks to see if there is a byte waiting in the input stream without
actually consuming the byte.
Parameters:
timeout - The amount of time to wait, 0 == forever Returns:
-1 on eof, -2 if the timeout expired with no available input or the character that was read (without consuming it).
它没有说明这里涉及什么时间单位...我假设是毫秒,但我也尝试使用“1”,以防它是秒。
这个 peek
命令功能足够,因为它代表您能够检测多字节 Unicode 输入,带有一点超时独创性:假定多字节 Unicode 的字节角色到达的速度比一个人打字的速度还快...
但是,如果它永远不会解锁,这意味着您必须将 peek
命令置于您必须自己滚动的超时机制中。下一个字符输入当然会解除阻塞。如果这是 Enter
,则 while
循环将结束。但是,比如说,如果您想在输入下一个字符之前打印一个字符(或做任何事情),那么 peek
的超时似乎不起作用会阻止您这样做。
JLine 使用通常的 java 语义:流获取字节,reader/writer 使用字符。唯一处理代码点(即单个值中可能的 32 位字符)的部分是 BindingReader
。
NonBlockingReader
遵循 Reader
语义,简单地添加一些带有超时的方法,可以 return -2 表示超时。
如果你想做解码,你需要使用 Character.isHighSurrogate
方法,正如 BindingReader
https://github.com/jline/jline3/blob/master/reader/src/main/java/org/jline/keymap/BindingReader.java#L124-L144
int s = 0;
int c = c = reader.read(100L);
if (c >= 0 && Character.isHighSurrogate((char) c)) {
s = c;
c = reader.read(100L);
}
return s != 0 ? Character.toCodePoint((char) s, (char) c) : c;
我找到了针对此问题的 Cywin 特定解决方案...而且可能是 (?) 拦截、隔离和识别 "keyboard control" 字符输入的唯一方法。
使用 JLine 和 Cygwin 获取正确的 Unicode 输入
正如我自己对一年前提出的问题的回答中所引用的 ,如果要正确处理 Unicode,Cygwin(无论如何在我的设置中)需要某种额外的缓冲和编码,用于控制台输入和输出.
为了同时应用这个和应用 JLine,我在 terminal.enterRawMode()
:
之后这样做
BufferedReader br = new BufferedReader( new InputStreamReader( terminal.input(), 'UTF-8' ))
注意 terminal.input()
returns 一个 org.jline.utils.NonBlockingInputStream
实例。
输入“ẃ”(英国扩展键盘中的 AltGr + W)然后在一个 br.read()
命令中消耗,并且产生的 int
值是 7811,正确的代码点值。 Hurrah:一个 Unicode 字符 不在 BMP(基本多语言平面) 中的字符已被正确使用。
处理键盘控制字符字节:
但我也想拦截、隔离并正确识别各种控制字符对应的字节。 TAB是一个字节(9),BACKSPACE是一个字节(127),好处理,但是UP-ARROW是以3个separately-read bytes的形式传递的,即三个单独的 br.read()
命令被解锁,即使使用上面的 BufferedReader
。一些控制序列包含 7 个这样的字节,例如Ctrl-Shift-F5 是 27(转义),后跟 6 个其他单独读取的字节,int
值:91、49、53、59、54、126。我还没有找到可能记录此类序列的位置:如果有人知道请添加评论。
然后有必要隔离这些 "grouped bytes":即你有一个字节流:你怎么知道这 3 个(或 7 个...)必须 共同解释?
这可以通过利用以下事实来实现:当为单个此类控制字符传送多个字节时,每个字节之间的传送时间不到一毫秒。也许并不那么令人惊讶。这个 Groovy 脚本似乎适合我的目的:
import org.apache.commons.lang3.StringUtils
@Grab(group='org.jline', module='jline', version='3.7.0')
@Grab(group='org.apache.commons', module='commons-lang3', version='3.7')
def terminal = org.jline.terminal.TerminalBuilder.builder().jna( true ).system( true ).build()
terminal.enterRawMode()
// BufferedReader needed for correct Unicode input using Cygwin
BufferedReader br = new BufferedReader( new InputStreamReader(terminal.input(), 'UTF-8' ))
// PrintStream needed for correct Unicode output using Cygwin
outPS = new PrintStream(System.out, true, 'UTF-8' )
userResponse = ''
int readInt
boolean continueLoop = true
while( continueLoop ) {
readInt = br.read()
while( readInt == 27 ) {
println "escape"
long startNano = System.nanoTime()
long nanoDiff = 0
// figure of 500000 nanoseconds arrived at by experimentation: see below
while( nanoDiff < 500000 ) {
readInt = br.read()
long timeNow = System.nanoTime()
nanoDiff = timeNow - startNano
println "z readInt $readInt char ${(char)readInt} nanoDiff $nanoDiff"
startNano = timeNow
}
}
switch( readInt ) {
case [10, 13]:
println ''
continueLoop = false
break
case 9:
println '...TAB'
continueLoop = false
break
case 127:
// backspace
if( ! userResponse.empty ) {
print '\b \b'
// chop off last character
userResponse = StringUtils.chop( userResponse )
}
break
default:
char unicodeChar = (char)readInt
outPS.print( unicodeChar )
userResponse += unicodeChar
}
}
outPS.print( "userResponse |$userResponse|")
br.close()
terminal.close()
以上代码使我能够成功"isolate"单个多字节键盘控制字符:
println "...TAB"
行中的 3 个点在用户按下 TAB 后立即打印在同一行上(上面的代码不会打印在输入行上)。这为在某些 BASH 命令中执行诸如 "autocompletion" 之类的操作打开了大门...
这个 500000 纳秒(0.5 毫秒)的设置是否足够快?也许吧!
最快的打字员可以每分钟打 220 个字。假设每个单词的平均字符数为 8(这看起来很高),则计算结果为每秒 29 个字符,或每个字符大约 34 毫秒。从理论上讲,事情应该没问题。但是 "rogue" 同时按下两个键可能意味着它们在彼此之间的按下时间小于 0.5 毫秒......但是,对于上面的代码,这仅在 这两个都是转义序列时才重要。它似乎工作正常。根据我的实验,它真的不会少于 500000 ns,因为它在多字节序列中的每个字节之间最多可能需要 70000 - 80000 ns(尽管通常需要更少)......以及各种中断或有趣发生的事情当然可能会干扰这些字节的传递。事实上,将它设置为 1000000(1 毫秒)似乎工作正常。
注意,如果我们想拦截和处理转义序列,我们现在上面的代码似乎有问题:上面的代码块在 nanoDiff
while
中的 br.read()
在转义序列的末尾循环。这没关系,因为我们可以跟踪我们正在接收的字节序列,因为 while
循环发生(在它阻塞之前)。
试试
jshell> " ẃ".getBytes()
==> byte[8] { -16, -112, -112, -73, 32, -31, -70, -125 }
jshell> " ẃ".chars().toArray()
==> int[4] { 55297, 56375, 32, 7811 }
jshell> " ẃ".codePoints() .toArray()
==> int[3] { 66615, 32, 7811 }
从
def terminal = org.jline.terminal.TerminalBuilder.builder().jna( true ).system( true ).build()
terminal.enterRawMode()
// NB the Terminal I get is class org.jline.terminal.impl.PosixSysTerminal
def reader = terminal.reader()
// class org.jline.utils.NonBlocking$NonBlockingInputStreamReader
def bytes = [] // NB class ArrayList
int readInt = -1
while( readInt != 13 && readInt != 10 ) {
readInt = reader.read()
byte convertedByte = (byte)readInt
// see what the binary looks like:
String binaryString = String.format("%8s", Integer.toBinaryString( convertedByte & 0xFF)).replace(' ', '0')
println "binary |$binaryString|"
bytes << (byte)readInt // NB means "append to list"
// these seem to block forever, whatever the param...
// int peek = reader.peek( 50 )
int peek = reader.peek( 0 )
}
// strip final byte (13 or 10)
bytes = bytes[0..-2]
def response = new String( (byte[])bytes.toArray(), 'UTF-8' )
根据 Javadoc(从源代码本地制作)peek
看起来像这样:
public int peek(long timeout)
Peeks to see if there is a byte waiting in the input stream without actually consuming the byte.
Parameters: timeout - The amount of time to wait, 0 == forever Returns: -1 on eof, -2 if the timeout expired with no available input or the character that was read (without consuming it).
它没有说明这里涉及什么时间单位...我假设是毫秒,但我也尝试使用“1”,以防它是秒。
这个 peek
命令功能足够,因为它代表您能够检测多字节 Unicode 输入,带有一点超时独创性:假定多字节 Unicode 的字节角色到达的速度比一个人打字的速度还快...
但是,如果它永远不会解锁,这意味着您必须将 peek
命令置于您必须自己滚动的超时机制中。下一个字符输入当然会解除阻塞。如果这是 Enter
,则 while
循环将结束。但是,比如说,如果您想在输入下一个字符之前打印一个字符(或做任何事情),那么 peek
的超时似乎不起作用会阻止您这样做。
JLine 使用通常的 java 语义:流获取字节,reader/writer 使用字符。唯一处理代码点(即单个值中可能的 32 位字符)的部分是 BindingReader
。
NonBlockingReader
遵循 Reader
语义,简单地添加一些带有超时的方法,可以 return -2 表示超时。
如果你想做解码,你需要使用 Character.isHighSurrogate
方法,正如 BindingReader
https://github.com/jline/jline3/blob/master/reader/src/main/java/org/jline/keymap/BindingReader.java#L124-L144
int s = 0;
int c = c = reader.read(100L);
if (c >= 0 && Character.isHighSurrogate((char) c)) {
s = c;
c = reader.read(100L);
}
return s != 0 ? Character.toCodePoint((char) s, (char) c) : c;
我找到了针对此问题的 Cywin 特定解决方案...而且可能是 (?) 拦截、隔离和识别 "keyboard control" 字符输入的唯一方法。
使用 JLine 和 Cygwin 获取正确的 Unicode 输入
正如我自己对一年前提出的问题的回答中所引用的
为了同时应用这个和应用 JLine,我在 terminal.enterRawMode()
:
BufferedReader br = new BufferedReader( new InputStreamReader( terminal.input(), 'UTF-8' ))
注意 terminal.input()
returns 一个 org.jline.utils.NonBlockingInputStream
实例。
输入“ẃ”(英国扩展键盘中的 AltGr + W)然后在一个 br.read()
命令中消耗,并且产生的 int
值是 7811,正确的代码点值。 Hurrah:一个 Unicode 字符 不在 BMP(基本多语言平面) 中的字符已被正确使用。
处理键盘控制字符字节:
但我也想拦截、隔离并正确识别各种控制字符对应的字节。 TAB是一个字节(9),BACKSPACE是一个字节(127),好处理,但是UP-ARROW是以3个separately-read bytes的形式传递的,即三个单独的 br.read()
命令被解锁,即使使用上面的 BufferedReader
。一些控制序列包含 7 个这样的字节,例如Ctrl-Shift-F5 是 27(转义),后跟 6 个其他单独读取的字节,int
值:91、49、53、59、54、126。我还没有找到可能记录此类序列的位置:如果有人知道请添加评论。
然后有必要隔离这些 "grouped bytes":即你有一个字节流:你怎么知道这 3 个(或 7 个...)必须 共同解释?
这可以通过利用以下事实来实现:当为单个此类控制字符传送多个字节时,每个字节之间的传送时间不到一毫秒。也许并不那么令人惊讶。这个 Groovy 脚本似乎适合我的目的:
import org.apache.commons.lang3.StringUtils
@Grab(group='org.jline', module='jline', version='3.7.0')
@Grab(group='org.apache.commons', module='commons-lang3', version='3.7')
def terminal = org.jline.terminal.TerminalBuilder.builder().jna( true ).system( true ).build()
terminal.enterRawMode()
// BufferedReader needed for correct Unicode input using Cygwin
BufferedReader br = new BufferedReader( new InputStreamReader(terminal.input(), 'UTF-8' ))
// PrintStream needed for correct Unicode output using Cygwin
outPS = new PrintStream(System.out, true, 'UTF-8' )
userResponse = ''
int readInt
boolean continueLoop = true
while( continueLoop ) {
readInt = br.read()
while( readInt == 27 ) {
println "escape"
long startNano = System.nanoTime()
long nanoDiff = 0
// figure of 500000 nanoseconds arrived at by experimentation: see below
while( nanoDiff < 500000 ) {
readInt = br.read()
long timeNow = System.nanoTime()
nanoDiff = timeNow - startNano
println "z readInt $readInt char ${(char)readInt} nanoDiff $nanoDiff"
startNano = timeNow
}
}
switch( readInt ) {
case [10, 13]:
println ''
continueLoop = false
break
case 9:
println '...TAB'
continueLoop = false
break
case 127:
// backspace
if( ! userResponse.empty ) {
print '\b \b'
// chop off last character
userResponse = StringUtils.chop( userResponse )
}
break
default:
char unicodeChar = (char)readInt
outPS.print( unicodeChar )
userResponse += unicodeChar
}
}
outPS.print( "userResponse |$userResponse|")
br.close()
terminal.close()
以上代码使我能够成功"isolate"单个多字节键盘控制字符:
println "...TAB"
行中的 3 个点在用户按下 TAB 后立即打印在同一行上(上面的代码不会打印在输入行上)。这为在某些 BASH 命令中执行诸如 "autocompletion" 之类的操作打开了大门...
这个 500000 纳秒(0.5 毫秒)的设置是否足够快?也许吧!
最快的打字员可以每分钟打 220 个字。假设每个单词的平均字符数为 8(这看起来很高),则计算结果为每秒 29 个字符,或每个字符大约 34 毫秒。从理论上讲,事情应该没问题。但是 "rogue" 同时按下两个键可能意味着它们在彼此之间的按下时间小于 0.5 毫秒......但是,对于上面的代码,这仅在 这两个都是转义序列时才重要。它似乎工作正常。根据我的实验,它真的不会少于 500000 ns,因为它在多字节序列中的每个字节之间最多可能需要 70000 - 80000 ns(尽管通常需要更少)......以及各种中断或有趣发生的事情当然可能会干扰这些字节的传递。事实上,将它设置为 1000000(1 毫秒)似乎工作正常。
注意,如果我们想拦截和处理转义序列,我们现在上面的代码似乎有问题:上面的代码块在 nanoDiff
while
中的 br.read()
在转义序列的末尾循环。这没关系,因为我们可以跟踪我们正在接收的字节序列,因为 while
循环发生(在它阻塞之前)。
试试
jshell> " ẃ".getBytes()
==> byte[8] { -16, -112, -112, -73, 32, -31, -70, -125 }
jshell> " ẃ".chars().toArray()
==> int[4] { 55297, 56375, 32, 7811 }
jshell> " ẃ".codePoints() .toArray()
==> int[3] { 66615, 32, 7811 }