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 位字符)的部分是 BindingReaderNonBlockingReader 遵循 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 }