Control.java
/*
* Copyright (c) 2016, Stein Eldar Johnsen
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.morimekta.strings.chr;
import net.morimekta.strings.EscapeUtil;
import java.util.Map;
import static java.lang.Character.toLowerCase;
import static java.lang.String.format;
import static net.morimekta.strings.EscapeUtil.javaEscape;
/**
* This class represents terminal control characters. These are not printable on
* their own, but will do stuff with the current terminal if printed out to it,
* and some represents non-printable keystrokes or key combinations.
* <p>
* See <a href="https://en.wikipedia.org/wiki/C0_and_C1_control_codes">
* Wikipedia on C0 and C1 control codes</a>.
*/
public class Control implements Char {
// This must be first to be available when instances are instantiated
// below.
private static final Map<String, String> REMAPPING =
Map.of(
"\033OH", "\033[1~", // HOME mac
"\033[H", "\033[1~", // HOME alt-linux
"\033OF", "\033[4~", // END mac
"\033[F", "\033[4~" // END alt-linux
);
// ----- Terminal Control -----
/**
* Enable full terminal control. This will also clear the screen and disable
* scrolling, but will not move the cursor position.
*/
public static final Control TERMINAL_CONTROL = new Control("\033[?1049h");
/**
* Restore state of terminal back to before control taken, including cursor
* position.
*/
public static final Control TERMINAL_RESTORE = new Control("\033[?1049l");
/**
* Clear screen, not moving cursor or clearing scroll history.
*/
public static final Control TERMINAL_CLEAR = new Control("\033[2j");
/**
* Reset terminal (will clear scroll history) and place cursor at top left.
* Note that resetting in the alternate screen buffer will also unset it back
* to default, not restore the previous state (restore will do nothing).
*/
public static final Control TERMINAL_RESET = new Control("\033c");
/** Erase character at cursor position. */
public static final Control CURSOR_ERASE = new Control("\033[K");
/** Save position of cursor. */
public static final Control CURSOR_SAVE = new Control("\033[s");
/** Restore position of cursor to last saved point. */
public static final Control CURSOR_RESTORE = new Control("\033[u");
/**
* Make control sequence setting the cursor on a fixed position on the
* screen using line numbers from the top to the first char of that line.
*
* @param line Line number to move to.
* @return The control sequence.
*/
public static Control cursorSetPos(int line) {
return cursorSetPos(line, 0);
}
/**
* Make control sequence setting the cursor on a fixed position on the
* screen using line numbers from the top, and column number from the
* left.
*
* @param line Line number to move to, 1-indexed.
* @param col Column number ot move to, 1-indexed.
* @return The control sequence.
*/
public static Control cursorSetPos(int line, int col) {
if (line <= 1 && col <= 1) {
// Set it to top left position.
return new Control("\033[H");
}
return new Control(format("\033[%d;%dH", line, col));
}
/**
* Make control sequence moving the cursor N positions to up.
* @param num Number of positions to move cursor.
* @return The control sequence.
*/
public static Control cursorUp(int num) {
return new Control(format("\033[%dA", num));
}
/**
* Make control sequence moving the cursor N positions to down.
* @param num Number of positions to move cursor.
* @return The control sequence.
*/
public static Control cursorDown(int num) {
return new Control(format("\033[%dB", num));
}
/**
* Make control sequence moving the cursor N positions to the right.
* @param num Number of positions to move cursor.
* @return The control sequence.
*/
public static Control cursorRight(int num) {
return new Control(format("\033[%dC", num));
}
/**
* Make control sequence moving the cursor N positions to the left.
* @param num Number of positions to move cursor.
* @return The control sequence.
*/
public static Control cursorLeft(int num) {
return new Control(format("\033[%dD", num));
}
// ----- Terminal Input -----
/** The up key. */
public static final Control UP = new Control("\033[A");
/** The down key. */
public static final Control DOWN = new Control("\033[B");
/** The right key. */
public static final Control RIGHT = new Control("\033[C");
/** The left key. */
public static final Control LEFT = new Control("\033[D");
/** The control up key combination. */
public static final Control CTRL_UP = new Control("\033[1;5A");
/** The control down key combination. */
public static final Control CTRL_DOWN = new Control("\033[1;5B");
/** The control right key combination. */
public static final Control CTRL_RIGHT = new Control("\033[1;5C");
/** The control left key combination. */
public static final Control CTRL_LEFT = new Control("\033[1;5D");
/** The DPAD middle key. */
public static final Control DPAD_MID = new Control("\033[E");
/** The insert key. */
public static final Control INSERT = new Control("\033[2~");
/** The delete key. */
public static final Control DELETE = new Control("\033[3~");
/** The home key. */
public static final Control HOME = new Control("\033[1~");
/** The end key. */
public static final Control END = new Control("\033[4~");
/** The page up key. */
public static final Control PAGE_UP = new Control("\033[5~");
/** The page down key. */
public static final Control PAGE_DOWN = new Control("\033[6~");
/** F1 key. */
public static final Control F1 = new Control("\033OP");
/** F2 key. */
public static final Control F2 = new Control("\033OQ");
/** F3 key. */
public static final Control F3 = new Control("\033OR");
/** F4 key. */
public static final Control F4 = new Control("\033OS");
/** F5 key. */
public static final Control F5 = new Control("\033[15~");
/** F6 key. */
public static final Control F6 = new Control("\033[17~");
/** F7 key. */
public static final Control F7 = new Control("\033[18~");
/** F8 key. */
public static final Control F8 = new Control("\033[19~");
/** F9 key. */
public static final Control F9 = new Control("\033[20~");
/** F10 key. */
public static final Control F10 = new Control("\033[21~");
/**
* NOTE: It is common to use F11 to mean 'fullscreen', so there is a good
* chance this is intercepted before reaching the terminal application.
*/
public static final Control F11 = new Control("\033[23~");
/** F12 key. */
public static final Control F12 = new Control("\033[24~");
/** Delete rest of word after (right of) cursor. */
public static final Char ALT_W = alt('w');
/** Delete rest of word before (left of) cursor. */
public static final Char ALT_K = alt('k');
/** Delete rest of current line after (right of) cursor. */
public static final Char ALT_U = alt('u');
/** Delete rest of current line before (left of) cursor. */
public static final Char ALT_D = alt('d');
/**
* Make a keystroke char for <code>alt + key</code>.
*
* @param c The char to get 'alt' keystroke char of.
* @return The keystroke char.
*/
public static Char alt(char c) {
if (('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z' && c != 'O') ||
('0' <= c && c <= '9')) {
return new Control(format("\033%c", c));
}
throw new IllegalArgumentException("Not suitable for <alt> modifier: '" + javaEscape(c) + "'");
}
/**
* Make a keystroke char for <code>control + key</code>, using the control
* char meaning in common linux terminals.
*
* @param c The char to get 'ctrl' keystroke char of.
* @return The keystroke char.
*/
public static Char ctrl(char c) {
switch (c) {
case 'c': case 'C': return Unicode.unicode(Char.ABR); // abort
case 'd': case 'D': return Unicode.unicode(Char.EOF); // end of transmission
case 'q': case 'Q': return Unicode.unicode(Char.XON); // continue output
case 's': case 'S': return Unicode.unicode(Char.XOFF); // buffer output
case 'x': case 'X': return Unicode.unicode(Char.CAN); // cancel
default: {
throw new IllegalArgumentException("Unknown control: <ctrl-" + c + ">");
}
}
}
// ----- Control Instance -----
@Override
public int codepoint() {
return -1;
}
@Override
public String asString() {
/*--*/
if (str.equals(UP.str)) {
return "<up>";
} else if (str.equals(DOWN.str)) {
return "<down>";
} else if (str.equals(RIGHT.str)) {
return "<right>";
} else if (str.equals(LEFT.str)) {
return "<left>";
} else if (str.equals(CTRL_UP.str)) {
return "<C-up>";
} else if (str.equals(CTRL_DOWN.str)) {
return "<C-down>";
} else if (str.equals(CTRL_RIGHT.str)) {
return "<C-right>";
} else if (str.equals(CTRL_LEFT.str)) {
return "<C-left>";
} else if (str.equals(CURSOR_ERASE.str)) {
return "<cursor-erase>";
} else if (str.equals(CURSOR_SAVE.str)) {
return "<cursor-save>";
} else if (str.equals(CURSOR_RESTORE.str)) {
return "<cursor-restore>";
} else if (str.equals(TERMINAL_CLEAR.str)) {
return "<clear>";
} else if (str.equals(TERMINAL_RESET.str)) {
return "<reset>";
} else if (str.equals(TERMINAL_CONTROL.str)) {
return "<terminal-control>";
} else if (str.equals(TERMINAL_RESTORE.str)) {
return "<terminal-restore>";
} else if (str.equals(DPAD_MID.str)) {
return "<dpa-mid>";
} else if (str.equals(INSERT.str)) {
return "<insert>";
} else if (str.equals(DELETE.str)) {
return "<delete>";
} else if (str.equals(HOME.str)) {
return "<home>";
} else if (str.equals(END.str)) {
return "<end>";
} else if (str.equals(PAGE_UP.str)) {
return "<pg-up>";
} else if (str.equals(PAGE_DOWN.str)) {
return "<pg-down>";
} else if (str.equals(F1.str)) {
return "<F1>";
} else if (str.equals(F2.str)) {
return "<F2>";
} else if (str.equals(F3.str)) {
return "<F3>";
} else if (str.equals(F4.str)) {
return "<F4>";
} else if (str.equals(F5.str)) {
return "<F5>";
} else if (str.equals(F6.str)) {
return "<F6>";
} else if (str.equals(F7.str)) {
return "<F7>";
} else if (str.equals(F8.str)) {
return "<F8>";
} else if (str.equals(F9.str)) {
return "<F9>";
} else if (str.equals(F10.str)) {
return "<F10>";
} else if (str.equals(F11.str)) {
return "<F11>";
} else if (str.equals(F12.str)) {
return "<F12>";
} else if (str.length() == 2 &&
(('a' <= str.charAt(1) && str.charAt(1) <= 'z') ||
('0' <= str.charAt(1) && str.charAt(1) <= '9'))) {
return "<alt-" + str.charAt(1) + '>';
} else if (str.length() == 2 &&
(('A' <= str.charAt(1) && str.charAt(1) <= 'Z'))) {
return "<alt-shift-" + toLowerCase(str.charAt(1)) + '>';
} else if (str.length() > 3 &&
(('[' <= str.charAt(1) && str.charAt(str.length() - 1) <= 'A'))) {
return "<cursor-" + str.substring(2, str.length() - 1) + "-up>";
} else if (str.length() > 3 &&
(('[' <= str.charAt(1) && str.charAt(str.length() - 1) <= 'B'))) {
return "<cursor-" + str.substring(2, str.length() - 1) + "-down>";
} else if (str.length() > 3 &&
(('[' <= str.charAt(1) && str.charAt(str.length() - 1) <= 'C'))) {
return "<cursor-" + str.substring(2, str.length() - 1) + "-right>";
} else if (str.length() > 3 &&
(('[' <= str.charAt(1) && str.charAt(str.length() - 1) <= 'D'))) {
return "<cursor-" + str.substring(2, str.length() - 1) + "-left>";
}
return EscapeUtil.javaEscape(str);
}
@Override
public int printableWidth() {
return 0;
}
@Override
public int length() {
return str.length();
}
@Override
public String toString() {
return str;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || !o.getClass().equals(getClass())) {
return false;
}
Control other = (Control) o;
return str.equals(other.str);
}
@Override
public int hashCode() {
return str.hashCode();
}
@Override
public int compareTo(Char o) {
if (o instanceof Control) {
return str.compareTo(((Control) o).str);
}
// compares to the
return Integer.compare(ESC, o.codepoint());
}
// ----- Protected -----
Control(CharSequence str) {
if (REMAPPING.containsKey(str.toString())) {
this.str = REMAPPING.get(str.toString());
} else {
this.str = str.toString();
}
}
private final String str;
}