2
\$\begingroup\$

I was cleaning up some old code and came across this (it's to read input from a terminal):

static public int read_input_key() {

    /*
    F1 ESC O P [27 79 80] 
    F2 ESC O Q [27 79 81] 
    F3 ESC O R [27 79 82] 
    F4 ESC O S [27 79 83]
    Up ESC [ A [27 91 65] 
    Down ESC [ B [27 91 66] 
    Left ESC [ D [27 91 68] 
    Right ESC [ C [27 91 67]
    Home ESC [ 1 ~ [27 91 49 126] 
    F5 ESC [ 1 5 ~ [27 91 49 53 126] 
    F6 ESC [ 1 7 ~ [27 91 49 55 126] 
    F7 ESC [ 1 8 ~ [27 91 49 56 126] 
    F8 ESC [ 1 9 ~ [27 91 49 57 126]
    End ESC [ 4 ~ [27 91 52 126] 
    Page-up ESC [ 5 ~ [27 91 53 126] 
    Page-down ESC [6 ~ [27 91 54 126]
    Insert ESC [ 2 ~ [27 91 50 126]
    F9 ESC [ 2 0 ~ [27 91 50 48 126] 
    F10 ESC [ 2 1 ~ [27 91 50 49 126] 
    F11 ESC [2 2 ~ [27 91 50 50 126] 
    F12 ESC [ 2 3 ~ [27 91 50 51 126]
    */

    // 

    try {

        if (System.in.available() != 0) {

            int c = System.in.read();
            if (c == 27) {

                switch (System.in.read()) {
                case 79:
                    switch (System.in.read()) {
                    case 80:
                        return VK_F1;
                    case 81:
                        return VK_F2;
                    case 82:
                        return VK_F3;
                    case 83:
                        return VK_F4;
                    }
                case 91:
                    switch (System.in.read()) {
                    case 65:
                        return VK_UP;
                    case 66:
                        return VK_DOWN;
                    case 68:
                        return VK_LEFT;
                    case 67:
                        return VK_RIGHT;
                    case 49: {
                        switch (System.in.read()) {
                        case 126:
                            return VK_HOME;
                        case 53:
                            return System.in.read() == 126 ? VK_F5 : 0;
                        case 55:
                            return System.in.read() == 126 ? VK_F6 : 0;
                        case 56:
                            return System.in.read() == 126 ? VK_F7 : 0;
                        case 57:
                            return System.in.read() == 126 ? VK_F8 : 0;
                        }
                    }
                    case 52:
                        return System.in.read() == 126 ? VK_END : 0;
                    case 53:
                        return System.in.read() == 126 ? VK_PAGE_UP : 0;
                    case 54:
                        return System.in.read() == 126 ? VK_PAGE_DOWN : 0;
                    case 50: {
                        switch (System.in.read()) {
                        case 126:
                            return VK_INSERT;
                        case 48:
                            return System.in.read() == 126 ? VK_F9 : 0;
                        case 49:
                            return System.in.read() == 126 ? VK_F10 : 0;
                        case 50:
                            return System.in.read() == 126 ? VK_F11 : 0;
                        case 51:
                            return System.in.read() == 126 ? VK_F12 : 0;
                        }
                    }
                    }
                }
            } else {
                return c;
            }
        }
    } catch (IOException e) {
        // restore terminal?
        e.printStackTrace();
    }

    return 0;
}

It work's fine, but I feel like it can be done really different. To explain the long comment (I should atleast refactor that), this for example:

    F1 ESC O P [27 79 80] 

Means when you press the F1 key you will get the sequence ESC O P, which is represented by the following ascii keys, 27 79 80.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

I think you will be able to implement this in a cleaner way by representing the key codes as data instead of control flow. Not only will the resulting code be shorter, but it will be more abstract. If you need to add new sequences or support multiple sets of sequences, you will be able to use the same basic logic.

More specifically, a trie data structure is a good match for your problem. The mapping process is simple: start at the root, input a character, and follow the corresponding branch. If there is no branch, fail. This must be modified slightly to output the original character if it is the sequence does not start with 27.

There is no trie in the Java collections framework, so you will have to build your own. The signature should be something like

class AsciiTrie {
  AsciiTrie[] children;
  int data;
}

Given this data structure, your new algorithm is the following

private static int readInputKey(AsciiTrie t) {
  if (System.in.available() == 0) {
    return 0;
  }

  int c;
  int depth = 0;
  while (t != null) {
    try {
      c = System.in.read();
    } catch {
      // handle error properly
    }

    t = t.children[c];
    depth += 1;

    if (t.data != 0) {
      return t.data;
    }
  }

  if (depth == 1) {
    return c;
  } else {
    return 0;
  }
}

I have not included code to build or populate the trie. But it is quite similar to traversal.

\$\endgroup\$
-1
\$\begingroup\$

What would already improve this would be to use constants instead of numbers, like this:

            int c = System.in.read();
            if (c == VK_ESC) {

                switch (System.in.read()) {
                case VK_O:
                    switch (System.in.read()) {
                    case VK_P:
                        return VK_F1;

That is, if the VK_* constants contain those, otherwise roll your own.


Another solution would be to use a class to represent each key sequence and test whether the pressed keys match any of that. Something like this:

public class KeySequence {
    protected int key = 0;
    protected int[] sequence = null;
    
    public KeySequence(int key, int... sequence) {
        super();
        
        // TODO: Validate key and sequence
        
        this.key = key;
        this.sequence = sequence;
    }
    
    public int getKey() {
        return key;
    }
    
    public int[] getSequence() {
        return sequence;
    }
    
    public boolean matches(int[] keys, int keyCount) {
        if (keyCount < sequence.length) {
            // Can't match, too few keys.
            return false;
        }
        
        for (int index = 0; index < sequence.length; index++) {
            if (sequence[index] != keys[index]) {
                return false;
            }
        }
        
        // keys started with sequence, so it matches.
        return true;
    }
}

With this your comment at the start of the function becomes logic:

List<KeySequence> keySequences = new ArrayList();
keySequences.add(new KeySequence(VK_F1, VK_ESC, VK_O, VK_P));
keySequences.add(new KeySequence(VK_F2, VK_ESC, VK_O, VK_Q));
// ...

And then, you just gather keystrokes from the terminal and test whether or not a KeySequence matches:

int[] keyBuffer = new int[16];
int keyCount = 0;

while ((keyBuffer[keyCount++] = System.in.read()) >= 0) {
    for (KeySequence keySequence : keySequences) {
        if (keySequence.matches(keyBuffer, keyCount)) {
            return keySequence.getKey();
        }
    }
    
    if (keyCount >= keyBuffer.length) {
        // TODO Handle that we've read too many keys.
    }
    
    if (keyCount >= MAX_KEYS_IN_SEQUENCE) {
        // TODO Handle that we can't find anything.
    }
}

Error handling needs to be extended.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.