diff --git a/README.md b/README.md index a59772bba..181a82ee9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ JLine is distributed under the [BSD License](http://www.opensource.org/licenses/ Documentation ------------- -* [wiki](https://github.com/jline/jline2/wiki) +* [wiki](https://github.com/jline/jline3/wiki) Forums ------ @@ -39,7 +39,7 @@ Use the following definition to use JLine in your maven project: org.jline jline - 3.0.0.M2 + 3.0.1 Building diff --git a/demo/jline-gogo.bat b/demo/jline-gogo.bat new file mode 100755 index 000000000..b72066134 --- /dev/null +++ b/demo/jline-gogo.bat @@ -0,0 +1,80 @@ +@echo off + +set DIRNAME=%~dp0% +set ROOTDIR=%DIRNAME%\.. +set TARGETDIR=%ROOTDIR%\target + +set JLINE_VERSION=3.0.2-SNAPSHOT +set JANSI_VERSION=1.14 +set JNA_VERSION=4.2.2 +set GOGO_RUNTIME_VERSION=1.0.0 +set GOGO_JLINE_VERSION=1.0.0 + +rem initialization +if not exist %TARGETDIR%\jline-%JLINE_VERSION%.jar ( + echo Build jline with maven before running the demo + goto END +) +if not exist %TARGETDIR%\lib ( + mkdir %TARGETDIR%\lib +) + +rem JLINE +set cp=%TARGETDIR%\jline-%JLINE_VERSION%.jar + +rem JANSI +if not exist %TARGETDIR%\lib\jansi-%JANSI_VERSION%.jar ( + echo "Downloading Jansi..." + %DIRNAME%\wget.exe -O %TARGETDIR%\lib\jansi-%JANSI_VERSION%.jar http://repo1.maven.org/maven2/org/fusesource/jansi/jansi/%JANSI_VERSION%/jansi-%JANSI_VERSION%.jar +) + +rem JNA +if not exist %TARGETDIR%\lib\jna-%JNA_VERSION%.jar ( + echo "Downloading JNA..." + %DIRNAME%\wget.exe -O %TARGETDIR%\lib\jna-%JNA_VERSION%.jar http://repo1.maven.org/maven2/net/java/dev/jna/jna/%JNA_VERSION%/jna-%JNA_VERSION%.jar +) + +rem Gogo Runtime +if not exist %TARGETDIR%\lib\org.apache.felix.gogo.runtime-%GOGO_RUNTIME_VERSION%.jar ( + echo "Downloading Gogo Runtime..." + %DIRNAME%\wget.exe -O %TARGETDIR%\lib\org.apache.felix.gogo.runtime-%GOGO_RUNTIME_VERSION%.jar http://repo1.maven.org/maven2/org/apache/felix/org.apache.felix.gogo.runtime/%GOGO_RUNTIME_VERSION%/org.apache.felix.gogo.runtime-%GOGO_RUNTIME_VERSION%.jar +) +set cp=%cp%;%TARGETDIR%\lib\org.apache.felix.gogo.runtime-%GOGO_RUNTIME_VERSION%.jar + +rem Gogo JLine +if not exist %TARGETDIR%\lib\org.apache.felix.gogo.jline-%GOGO_JLINE_VERSION%.jar ( + echo "Downloading Gogo JLine..." + %DIRNAME%\wget.exe -O %TARGETDIR%\lib\org.apache.felix.gogo.jline-%GOGO_JLINE_VERSION%.jar http://repo1.maven.org/maven2/org/apache/felix/org.apache.felix.gogo.jline/%GOGO_JLINE_VERSION%/org.apache.felix.gogo.jline-%GOGO_JLINE_VERSION%.jar +) +set cp=%cp%;%TARGETDIR%\lib\org.apache.felix.gogo.jline-%GOGO_JLINE_VERSION%.jar + + +set opts= +:RUN_LOOP + if "%1" == "jansi" goto :EXECUTE_JANSI + if "%1" == "jna" goto :EXECUTE_JNA + if "%1" == "debug" goto :EXECUTE_DEBUG + goto :EXECUTE + +:EXECUTE_JANSI + set cp=%cp%;%TARGETDIR%/lib/jansi-%JANSI_VERSION%.jar + shift + goto :RUN_LOOP + +:EXECUTE_JNA + set cp=%cp%;%TARGETDIR%/lib/jna-%JNA_VERSION%.jar + shift + goto :RUN_LOOP + +:EXECUTE_DEBUG + set opts=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 + shift + goto :RUN_LOOP + +:EXECUTE +rem Launch gogo shell +echo "Classpath: %cp%" +echo "Launching Gogo JLine..." +java -cp %cp% %opts% org.apache.felix.gogo.jline.Main + +:END \ No newline at end of file diff --git a/demo/jline-gogo.sh b/demo/jline-gogo.sh new file mode 100755 index 000000000..b8d4fea6d --- /dev/null +++ b/demo/jline-gogo.sh @@ -0,0 +1,104 @@ +#!/bin/sh + +realpath() { + OURPWD=${PWD} + cd "$(dirname "${1}")" + LINK=$(readlink "$(basename "${1}")") + while [ "${LINK}" ]; do + cd "$(dirname "${LINK}")" + LINK=$(readlink "$(basename "${1}")") + done + REALPATH="${PWD}/$(basename "${1}")" + cd "${OURPWD}" + echo "${REALPATH}" +} + +REALNAME=$(realpath "$0") +DIRNAME=$(dirname "${REALNAME}") +PROGNAME=$(basename "${REALNAME}") +ROOTDIR=${DIRNAME}/.. +TARGETDIR=${ROOTDIR}/target + +if [ ! -e ${TARGETDIR} ] ; then + echo "Build jline with maven before running the demo" + exit +fi; +if [ ! -e ${TARGETDIR}/lib ] ; then + mkdir ${TARGETDIR}/lib +fi; + +JLINE_VERSION=$(ls ${TARGETDIR}/jline-*-SNAPSHOT.jar | sed -e 's#.*/jline-## ; s#SNAPSHOT.*#SNAPSHOT#') +JANSI_VERSION=$(cat ${ROOTDIR}/pom.xml| grep jansi.version\> | sed -e 's#^.*## ; s# | sed -e 's#^.*## ; s#1.8 1.8 + 4.2.2 1.14 1.0.3 @@ -109,7 +110,7 @@ net.java.dev.jna jna - 4.2.2 + ${jna.version} true diff --git a/src/main/java/org/jline/builtins/Nano.java b/src/main/java/org/jline/builtins/Nano.java index bf6ffbcb5..ff4a6dc55 100644 --- a/src/main/java/org/jline/builtins/Nano.java +++ b/src/main/java/org/jline/builtins/Nano.java @@ -44,6 +44,7 @@ import org.jline.terminal.Attributes.ControlChar; import org.jline.terminal.Attributes.InputFlag; import org.jline.terminal.Attributes.LocalFlag; +import org.jline.terminal.MouseEvent; import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.terminal.Terminal.Signal; @@ -78,6 +79,7 @@ public class Nano { public boolean printLineNumbers = true; public boolean wrapping; public boolean smoothScrolling = true; + public boolean mouseSupport = false; public boolean oneMoreLine = true; public boolean constantCursor; public int tabs = 4; @@ -366,7 +368,7 @@ boolean moveRight(int chars) { return ret; } - void moveDown(int lines) throws IOException { + void moveDown(int lines) { cursorDown(lines); ensureCursorVisible(); } @@ -696,6 +698,16 @@ List getDisplayedLines(int nbLines) { return newLines; } + public void moveTo(int x, int y) { + if (printLineNumbers) { + x = Math.max(x - 8, 0); + } + line = firstLineToDisplay; + offsetInLine = offsetInLineToDisplay; + wantedColumn = x; + cursorDown(y); + } + public int getDisplayedCursor() { int rwidth = size.getColumns(); int cursor = (printLineNumbers ? 8 : 0); @@ -965,6 +977,9 @@ public void run() throws IOException { display.clear(); display.reset(); display.resize(size.getRows(), size.getColumns()); + if (mouseSupport) { + terminal.trackMouse(Terminal.MouseTracking.Normal); + } this.shortcuts = standardShortcuts(); @@ -1020,6 +1035,9 @@ public void run() throws IOException { case SMOOTH_SCROLLING: smoothScrolling(); break; + case MOUSE_SUPPORT: + mouseSupport(); + break; case ONE_MORE_LINE: oneMoreLine(); break; @@ -1083,6 +1101,9 @@ public void run() throws IOException { case MATCHING: buffer.matching(); break; + case MOUSE_EVENT: + mouseEvent(); + break; default: setMessage("Unsupported " + op.name().toLowerCase().replace('_', '-')); break; @@ -1090,6 +1111,9 @@ public void run() throws IOException { display(); } } finally { + if (mouseSupport) { + terminal.trackMouse(Terminal.MouseTracking.Off); + } terminal.puts(Capability.exit_ca_mode); terminal.puts(Capability.keypad_local); terminal.flush(); @@ -1117,6 +1141,7 @@ boolean write() throws IOException { writeKeyMap.bind(Operation.ACCEPT, "\r"); writeKeyMap.bind(Operation.CANCEL, ctrl('C')); writeKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1)); + writeKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse)); editMessage = getWriteMessage(); editBuffer.setLength(0); @@ -1162,6 +1187,9 @@ boolean write() throws IOException { case BACKUP: writeBackup = !writeBackup; break; + case MOUSE_EVENT: + mouseEvent(); + break; } editMessage = getWriteMessage(); display(); @@ -1298,6 +1326,9 @@ void read() { for (char i = 32; i < 256; i++) { readKeyMap.bind(Operation.INSERT, Character.toString(i)); } + for (char i = 'A'; i <= 'Z'; i++) { + readKeyMap.bind(Operation.DO_LOWER_CASE, alt(i)); + } readKeyMap.bind(Operation.BACKSPACE, del()); readKeyMap.bind(Operation.NEW_BUFFER, alt('f')); readKeyMap.bind(Operation.TO_FILES, ctrl('T')); @@ -1305,6 +1336,7 @@ void read() { readKeyMap.bind(Operation.ACCEPT, "\r"); readKeyMap.bind(Operation.CANCEL, ctrl('C')); readKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1)); + readKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse)); editMessage = getReadMessage(); editBuffer.setLength(0); @@ -1358,6 +1390,9 @@ void read() { case NEW_BUFFER: readNewBuffer = !readNewBuffer; break; + case MOUSE_EVENT: + mouseEvent(); + break; } editMessage = getReadMessage(); display(); @@ -1492,6 +1527,9 @@ void help(String help) { case CLEAR_SCREEN: clearScreen(); break; + case MOUSE_EVENT: + mouseEvent(); + break; } display(); } @@ -1508,6 +1546,9 @@ void help(String help) { void search() throws IOException { KeyMap searchKeyMap = new KeyMap<>(); searchKeyMap.setUnicode(Operation.INSERT); + for (char i = 'A'; i <= 'Z'; i++) { + searchKeyMap.bind(Operation.DO_LOWER_CASE, alt(i)); + } searchKeyMap.bind(Operation.CASE_SENSITIVE, alt('c')); searchKeyMap.bind(Operation.BACKWARDS, alt('b')); searchKeyMap.bind(Operation.REGEXP, alt('r')); @@ -1515,6 +1556,7 @@ void search() throws IOException { searchKeyMap.bind(Operation.CANCEL, ctrl('C')); searchKeyMap.bind(Operation.FIRST_LINE, ctrl('Y')); searchKeyMap.bind(Operation.LAST_LINE, ctrl('V')); + searchKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse)); editMessage = getSearchMessage(); editBuffer.setLength(0); @@ -1561,6 +1603,9 @@ void search() throws IOException { case LAST_LINE: buffer.lastLine(); return; + case MOUSE_EVENT: + mouseEvent(); + break; } editMessage = getSearchMessage(); display(); @@ -1709,6 +1754,12 @@ void smoothScrolling() { setMessage("Smooth scrolling " + (smoothScrolling ? "enabled" : "disabled")); } + void mouseSupport() throws IOException { + mouseSupport = !mouseSupport; + setMessage("Mouse support " + (mouseSupport ? "enabled" : "disabled")); + terminal.trackMouse(mouseSupport ? Terminal.MouseTracking.Normal : Terminal.MouseTracking.Off); + } + void constantCursor() { constantCursor = !constantCursor; setMessage("Constant cursor position display " + (constantCursor ? "enabled" : "disabled")); @@ -1729,6 +1780,42 @@ void clearScreen() { resetDisplay(); } + void mouseEvent() { + MouseEvent event = terminal.readMouseEvent(); + if (event.getModifiers().isEmpty() && event.getType() == MouseEvent.Type.Released + && event.getButton() == MouseEvent.Button.Button1) { + int x = event.getX(); + int y = event.getY(); + int hdr = buffer.computeHeader().size(); + int ftr = computeFooter().size(); + if (y < hdr) { + // nothing + } else if (y < size.getRows() - ftr) { + buffer.moveTo(x, y - hdr); + } else { + int cols = (shortcuts.size() + 1) / 2; + int cw = size.getColumns() / cols; + int l = y - (size.getRows() - ftr) - 1; + int si = l * cols + x / cw; + String shortcut = null; + Iterator it = shortcuts.keySet().iterator(); + while (si-- >= 0 && it.hasNext()) { shortcut = it.next(); } + if (shortcut != null) { + shortcut = shortcut.replaceAll("M-", "\\\\E"); + String seq = KeyMap.translate(shortcut); + bindingReader.runMacro(seq); + } + } + } + else if (event.getType() == MouseEvent.Type.Wheel) { + if (event.getButton() == MouseEvent.Button.WheelDown) { + buffer.moveDown(1); + } else if (event.getButton() == MouseEvent.Button.WheelUp) { + buffer.moveUp(1); + } + } + } + public String getTitle() { return title; } @@ -1914,6 +2001,7 @@ protected void bindKeys() { keys.bind(Operation.CONSTANT_CURSOR, alt('c')); keys.bind(Operation.ONE_MORE_LINE, alt('o')); keys.bind(Operation.SMOOTH_SCROLLING, alt('s')); + keys.bind(Operation.MOUSE_SUPPORT, alt('m')); keys.bind(Operation.WHITESPACE, alt('p')); keys.bind(Operation.HIGHLIGHT, alt('y')); @@ -1932,6 +2020,8 @@ protected void bindKeys() { keys.bind(Operation.DOWN, key(terminal, Capability.key_down)); keys.bind(Operation.RIGHT, key(terminal, Capability.key_right)); keys.bind(Operation.LEFT, key(terminal, Capability.key_left)); + + keys.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse)); } enum Operation { @@ -1946,6 +2036,7 @@ enum Operation { WRAP, NUMBERS, SMOOTH_SCROLLING, + MOUSE_SUPPORT, ONE_MORE_LINE, CLEAR_SCREEN, @@ -2015,7 +2106,9 @@ enum Operation { AUTO_INDENT, CUT_TO_END_TOGGLE, TABS_TO_SPACE, - UNCUT + UNCUT, + + MOUSE_EVENT } } diff --git a/src/main/java/org/jline/builtins/Tmux.java b/src/main/java/org/jline/builtins/Tmux.java index b3e52d296..954f3d18f 100644 --- a/src/main/java/org/jline/builtins/Tmux.java +++ b/src/main/java/org/jline/builtins/Tmux.java @@ -28,6 +28,7 @@ import org.jline.reader.ParsedLine; import org.jline.reader.impl.DefaultParser; import org.jline.terminal.Attributes; +import org.jline.terminal.MouseEvent; import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.terminal.Terminal.Signal; @@ -67,10 +68,12 @@ public class Tmux { private final AtomicInteger paneId = new AtomicInteger(); private final Map serverOptions = new HashMap<>(); - private final KeyMap keyMap = new KeyMap<>(); - private static final Object UNMAPPED = new Object(); + private KeyMap keyMap; + enum Binding { + Discard, SelfInsert, Mouse + } public Tmux(Terminal terminal, PrintStream err, Consumer runner) throws IOException { InfoCmp.setDefaultInfoCmp("screen", SCREEN_CAPS); @@ -84,16 +87,25 @@ public Tmux(Terminal terminal, PrintStream err, Consumer runner) throw term = (colors != null && colors >= 256) ? "screen-256color" : "screen"; // Setup defaults bindings serverOptions.put(OPT_PREFIX, "`"); - keyMap.setUnicode(UNMAPPED); - keyMap.bind(UNMAPPED, KeyMap.range("^@-^?")); - keyMap.bind(CMD_SEND_PREFIX, serverOptions.get(OPT_PREFIX)); - keyMap.bind(CMD_SEND_PREFIX, serverOptions.get(OPT_PREFIX)); - keyMap.bind(CMD_SPLIT_WINDOW, "\""); - keyMap.bind(CMD_SPLIT_WINDOW + " -h", "%"); - keyMap.bind(CMD_SELECT_PANE + " -U", KeyMap.key(terminal, Capability.key_up)); - keyMap.bind(CMD_SELECT_PANE + " -L", KeyMap.key(terminal, Capability.key_left)); - keyMap.bind(CMD_SELECT_PANE + " -R", KeyMap.key(terminal, Capability.key_right)); - keyMap.bind(CMD_SELECT_PANE + " -D", KeyMap.key(terminal, Capability.key_down)); + keyMap = createKeyMap(serverOptions.get(OPT_PREFIX)); + } + + protected KeyMap createKeyMap(String prefix) { + KeyMap keyMap = new KeyMap<>(); + keyMap.setUnicode(Binding.SelfInsert); + keyMap.setNomatch(Binding.SelfInsert); + for (int i = 0; i < 255; i++) { + keyMap.bind(Binding.Discard, prefix + (char)(i)); + } + keyMap.bind(Binding.Mouse, KeyMap.key(terminal, Capability.key_mouse)); + keyMap.bind(CMD_SEND_PREFIX, prefix + prefix); + keyMap.bind(CMD_SPLIT_WINDOW, prefix + "\""); + keyMap.bind(CMD_SPLIT_WINDOW + " -h", prefix + "%"); + keyMap.bind(CMD_SELECT_PANE + " -U", prefix + KeyMap.key(terminal, Capability.key_up)); + keyMap.bind(CMD_SELECT_PANE + " -L", prefix + KeyMap.key(terminal, Capability.key_left)); + keyMap.bind(CMD_SELECT_PANE + " -R", prefix + KeyMap.key(terminal, Capability.key_right)); + keyMap.bind(CMD_SELECT_PANE + " -D", prefix + KeyMap.key(terminal, Capability.key_down)); + return keyMap; } public void run() throws IOException { @@ -103,6 +115,7 @@ public void run() throws IOException { Attributes attributes = terminal.enterRawMode(); terminal.puts(Capability.enter_ca_mode); terminal.puts(Capability.keypad_xmit); + terminal.trackMouse(Terminal.MouseTracking.Any); terminal.flush(); try { // Create first pane @@ -116,6 +129,7 @@ public void run() throws IOException { // Redraw loop redrawLoop(); } finally { + terminal.trackMouse(Terminal.MouseTracking.Off); terminal.puts(Capability.keypad_local); terminal.puts(Capability.exit_ca_mode); terminal.flush(); @@ -153,26 +167,24 @@ private void setDirty() { private void inputLoop() { try { - int c; - while ((c = terminal.reader().read()) >= 0) { - String pfx = serverOptions.get(OPT_PREFIX); - if (pfx != null && c == pfx.charAt(0)) { - // escape sequences - Object b = new BindingReader(terminal.reader()).readBinding(keyMap); - if (b instanceof String) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ByteArrayOutputStream err = new ByteArrayOutputStream(); - try (PrintStream pout = new PrintStream(out); - PrintStream perr = new PrintStream(err)) { - execute(pout, perr, (String) b); - } catch (Exception e) { - // TODO: log - } - } - } else { - String s = new String(Character.toChars(c)); - active.getMasterInputOutput().write(s.getBytes()); + BindingReader reader = new BindingReader(terminal.reader()); + while (running.get()) { + Object b = reader.readBinding(keyMap); + if (b == Binding.SelfInsert) { + active.getMasterInputOutput().write(reader.getLastBinding().getBytes()); active.getMasterInputOutput().flush(); + } else if (b == Binding.Mouse) { + MouseEvent event = terminal.readMouseEvent(); + //System.err.println(event.toString()); + } else if (b instanceof String) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + try (PrintStream pout = new PrintStream(out); + PrintStream perr = new PrintStream(err)) { + execute(pout, perr, (String) b); + } catch (Exception e) { + // TODO: log + } } } } catch (IOException e) { diff --git a/src/main/java/org/jline/reader/LineReader.java b/src/main/java/org/jline/reader/LineReader.java index 5ecbab29a..e6f7508e2 100644 --- a/src/main/java/org/jline/reader/LineReader.java +++ b/src/main/java/org/jline/reader/LineReader.java @@ -243,6 +243,7 @@ public interface LineReader { String WHAT_CURSOR_POSITION = "what-cursor-position"; String YANK = "yank"; String YANK_POP = "yank-pop"; + String MOUSE = "mouse"; // // KeyMap names @@ -346,7 +347,8 @@ enum Option { DELAY_LINE_WRAP, AUTO_PARAM_SLASH(true), AUTO_REMOVE_SLASH(true), - INSERT_TAB(true); + INSERT_TAB(true), + MOUSE; private final boolean def; diff --git a/src/main/java/org/jline/reader/impl/BufferImpl.java b/src/main/java/org/jline/reader/impl/BufferImpl.java index e2646f5ca..9c2b91cc2 100644 --- a/src/main/java/org/jline/reader/impl/BufferImpl.java +++ b/src/main/java/org/jline/reader/impl/BufferImpl.java @@ -262,6 +262,30 @@ public boolean down() { return true; } + public boolean moveXY(int dx, int dy) { + int col = 0; + while (prevChar() != '\n' && move(-1) == -1) { + col++; + } + cursorCol = 0; + while (dy < 0) { + up(); + dy++; + } + while (dy > 0) { + down(); + dy--; + } + col = Math.max(col + dx, 0); + for (int i = 0; i < col; i++) { + if (move(1) != 1 || currChar() == '\n') { + break; + } + } + cursorCol = col; + return true; + } + private int getCursorCol() { if (cursorCol < 0) { cursorCol = 0; diff --git a/src/main/java/org/jline/reader/impl/DefaultParser.java b/src/main/java/org/jline/reader/impl/DefaultParser.java index bf04c14c1..6ad8eecaa 100644 --- a/src/main/java/org/jline/reader/impl/DefaultParser.java +++ b/src/main/java/org/jline/reader/impl/DefaultParser.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Objects; +import org.jline.reader.EOFError; import org.jline.reader.ParsedLine; import org.jline.reader.Parser; import org.jline.reader.Parser.ParseContext; @@ -23,6 +24,8 @@ public class DefaultParser implements Parser { private char[] escapeChars = {'\\'}; + private boolean eofOnUnclosedQuote; + public void setQuoteChars(final char[] chars) { this.quoteChars = chars; } @@ -39,6 +42,14 @@ public char[] getEscapeChars() { return this.escapeChars; } + public void setEofOnUnclosedQuote(boolean eofOnUnclosedQuote) { + this.eofOnUnclosedQuote = eofOnUnclosedQuote; + } + + public boolean isEofOnUnclosedQuote() { + return eofOnUnclosedQuote; + } + public ParsedLine parse(final String line, final int cursor, ParseContext context) { List words = new LinkedList<>(); StringBuilder current = new StringBuilder(); @@ -92,10 +103,11 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex wordCursor = words.get(words.size() - 1).length(); } -// if (quoteStart >= 0) { -// throw new EOFError(-1, -1, "Missing closing quote", line.charAt(quoteStart) == '\'' -// ? "quote" : "dquote"); -// } + if (eofOnUnclosedQuote && quoteStart >= 0) { + throw new EOFError(-1, -1, "Missing closing quote", line.charAt(quoteStart) == '\'' + ? "quote" : "dquote"); + } + return new ArgumentList(line, words, wordIndex, wordCursor, cursor); } diff --git a/src/main/java/org/jline/reader/impl/LineReaderImpl.java b/src/main/java/org/jline/reader/impl/LineReaderImpl.java index 41caac3b4..5c91a11fb 100644 --- a/src/main/java/org/jline/reader/impl/LineReaderImpl.java +++ b/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -59,10 +59,8 @@ import org.jline.reader.UserInterruptException; import org.jline.reader.Widget; import org.jline.reader.impl.history.DefaultHistory; -import org.jline.terminal.Attributes; +import org.jline.terminal.*; import org.jline.terminal.Attributes.ControlChar; -import org.jline.terminal.Size; -import org.jline.terminal.Terminal; import org.jline.terminal.Terminal.Signal; import org.jline.terminal.Terminal.SignalHandler; import org.jline.utils.AttributedString; @@ -485,6 +483,8 @@ public String readLine(String prompt, String rightPrompt, Character mask, String terminal.puts(Capability.keypad_xmit); if (isSet(Option.AUTO_FRESH_LINE)) freshLine(); + if (isSet(Option.MOUSE)) + terminal.trackMouse(Terminal.MouseTracking.Normal); setPrompt(prompt); setRightPrompt(rightPrompt); @@ -615,7 +615,7 @@ public void callWidget(String name) { w.apply(); } } catch (Throwable t) { - Log.debug("Error executing widget '" + name + "'", t); + Log.debug("Error executing widget '", name, "'", t); } } @@ -2177,6 +2177,7 @@ protected void cleanup() { redisplay(false); println(); terminal.puts(Capability.keypad_local); + terminal.trackMouse(Terminal.MouseTracking.Off); flush(); } history.moveToEnd(); @@ -3176,6 +3177,7 @@ protected Map builtinWidgets() { widgets.put(WHAT_CURSOR_POSITION, this::whatCursorPosition); widgets.put(YANK, this::yank); widgets.put(YANK_POP, this::yankPop); + widgets.put(MOUSE, this::mouse); return widgets; } @@ -3190,42 +3192,15 @@ protected void redisplay(boolean flush) { return; } // TODO: support TERM_SHORT, terminal lines < 3 - String buffer = buf.toString(); - AttributedString attBuf; - if (mask != null) { - if (mask == NULL_MASK) { - buffer = ""; - } else { - StringBuilder sb = new StringBuilder(); - for (int i = buffer.length(); i-- > 0;) { - sb.append((char) mask); - } - buffer = sb.toString(); - } - attBuf = new AttributedString(buffer); - } else if (highlighter != null) { - attBuf = highlighter.highlight(this, buffer); - } else { - attBuf = new AttributedString(buffer); - } - List secondaryPrompts = new ArrayList<>(); - AttributedString tNewBuf = insertSecondaryPrompts(attBuf, secondaryPrompts); - AttributedStringBuilder full = new AttributedStringBuilder().tabs(TAB_WIDTH); - full.append(prompt); - full.append(tNewBuf); - if (post != null) { - full.append("\n"); - full.append(post.get()); - } - + AttributedString full = getDisplayedBufferWithPrompts(secondaryPrompts); List newLines; if (size.getColumns() <= 0) { newLines = new ArrayList<>(); - newLines.add(full.toAttributedString()); + newLines.add(full); } else { - newLines = full.toAttributedString().columnSplitLength(size.getColumns(), true); + newLines = full.columnSplitLength(size.getColumns(), true); } List rightPromptLines; @@ -3248,7 +3223,7 @@ protected void redisplay(boolean flush) { // TODO: in case of wide chars AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH); sb.append(prompt); - sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts)); + sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts, false)); List promptLines = sb.columnSplitLength(size.getColumns()); if (!promptLines.isEmpty()) { cursorPos = size.cursorPos(promptLines.size() - 1, @@ -3263,6 +3238,37 @@ protected void redisplay(boolean flush) { } } + private AttributedString getDisplayedBufferWithPrompts(List secondaryPrompts) { + AttributedString attBuf; + String buffer = buf.toString(); + if (mask != null) { + if (mask == NULL_MASK) { + buffer = ""; + } else { + StringBuilder sb = new StringBuilder(); + for (int i = buffer.length(); i-- > 0;) { + sb.append((char) mask); + } + buffer = sb.toString(); + } + attBuf = new AttributedString(buffer); + } else if (highlighter != null) { + attBuf = highlighter.highlight(this, buffer); + } else { + attBuf = new AttributedString(buffer); + } + + AttributedString tNewBuf = insertSecondaryPrompts(attBuf, secondaryPrompts); + AttributedStringBuilder full = new AttributedStringBuilder().tabs(TAB_WIDTH); + full.append(prompt); + full.append(tNewBuf); + if (post != null) { + full.append("\n"); + full.append(post.get()); + } + return full.toAttributedString(); + } + private AttributedString expandPromptPattern(String pattern, int padToWidth, String message, int line) { ArrayList parts = new ArrayList<>(); @@ -3379,43 +3385,40 @@ private AttributedString insertSecondaryPrompts(AttributedString strAtt, List lines = strAtt.columnSplitLength(Integer.MAX_VALUE); AttributedStringBuilder sb = new AttributedStringBuilder(); String secondaryPromptPattern = getString(SECONDARY_PROMPT_PATTERN, DEFAULT_SECONDARY_PROMPT_PATTERN); - int line = 0; - if (computePrompts || prompts.size() < 2) { - boolean needsMessage = secondaryPromptPattern.contains("%M"); - AttributedStringBuilder buf = new AttributedStringBuilder(); - int width = 0; - if (secondaryPromptPattern.contains("%P")) { - width = prompt.columnLength(); - for (int l = 0; l < lines.size() - 1; l++) { - AttributedString prompt; - buf.append(lines.get(l)).append("\n"); - if (computePrompts) { - String missing = ""; - if (needsMessage) { - try { - parser.parse(buf.toString(), buf.length(),ParseContext.SECONDARY_PROMPT); - } catch (EOFError e) { - missing = e.getMissing(); - } catch (SyntaxError e) { - // Ignore - } - } - prompt = expandPromptPattern(secondaryPromptPattern, - 0, missing, l+1); - } else { - prompt = prompts.get(l); + boolean needsMessage = secondaryPromptPattern.contains("%M"); + AttributedStringBuilder buf = new AttributedStringBuilder(); + int width = 0; + List missings = new ArrayList<>(); + if (computePrompts && secondaryPromptPattern.contains("%P")) { + width = prompt.columnLength(); + for (int line = 0; line < lines.size() - 1; line++) { + AttributedString prompt; + buf.append(lines.get(line)).append("\n"); + String missing = ""; + if (needsMessage) { + try { + parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); + } catch (EOFError e) { + missing = e.getMissing(); + } catch (SyntaxError e) { + // Ignore } - width = Math.max(width, prompt.columnLength()); } - buf.setLength(0); + missings.add(missing); + prompt = expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1); + width = Math.max(width, prompt.columnLength()); } - while (line < lines.size() - 1) { - sb.append(lines.get(line)).append("\n"); - buf.append(lines.get(line)).append("\n"); - AttributedString prompt; - if (computePrompts) { - String missing = ""; - if (needsMessage) { + buf.setLength(0); + } + int line = 0; + while (line < lines.size() - 1) { + sb.append(lines.get(line)).append("\n"); + buf.append(lines.get(line)).append("\n"); + AttributedString prompt; + if (computePrompts) { + String missing = ""; + if (needsMessage) { + if (missings.isEmpty()) { try { parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT); } catch (EOFError e) { @@ -3423,19 +3426,20 @@ private AttributedString insertSecondaryPrompts(AttributedString strAtt, List tsb.append((char) c)); + bindingReader.runMacro(tsb.toString()); + + List secondaryPrompts = new ArrayList<>(); + getDisplayedBufferWithPrompts(secondaryPrompts); + + AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH); + sb.append(prompt); + sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts, false)); + List promptLines = sb.columnSplitLength(size.getColumns()); + + int currentLine = promptLines.size() - 1; + int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size())); + int pl0 = currentLine == 0 ? prompt.columnLength() : secondaryPrompts.get(currentLine - 1).columnLength(); + int pl1 = wantedLine == 0 ? prompt.columnLength() : secondaryPrompts.get(wantedLine - 1).columnLength(); + int adjust = pl1 - pl0; + buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY()); + } + return true; + } + /** * Clear the screen by issuing the ANSI "clear screen" code. */ @@ -5081,6 +5111,7 @@ private void bindArrowKeys(KeyMap map) { bind(map, DELETE_CHAR, key(Capability.key_dc)); bind(map, KILL_WHOLE_LINE, key(Capability.key_dl)); bind(map, OVERWRITE_MODE, key(Capability.key_ic)); + bind(map, MOUSE, key(Capability.key_mouse)); } /** diff --git a/src/main/java/org/jline/terminal/Cursor.java b/src/main/java/org/jline/terminal/Cursor.java new file mode 100644 index 000000000..e6f52de56 --- /dev/null +++ b/src/main/java/org/jline/terminal/Cursor.java @@ -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 + ']'; + } +} diff --git a/src/main/java/org/jline/terminal/MouseEvent.java b/src/main/java/org/jline/terminal/MouseEvent.java new file mode 100644 index 000000000..2533bdc77 --- /dev/null +++ b/src/main/java/org/jline/terminal/MouseEvent.java @@ -0,0 +1,82 @@ +/* + * 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; + +import java.util.EnumSet; + +public class MouseEvent { + + public enum Type { + Released, + Pressed, + Wheel, + Moved, + Dragged + } + + public enum Button { + NoButton, + Button1, + Button2, + Button3, + WheelUp, + WheelDown + } + + public enum Modifier { + Shift, + Alt, + Control + } + + private final Type type; + private final Button button; + private final EnumSet modifiers; + private final int x; + private final int y; + + public MouseEvent(Type type, Button button, EnumSet modifiers, int x, int y) { + this.type = type; + this.button = button; + this.modifiers = modifiers; + this.x = x; + this.y = y; + } + + public Type getType() { + return type; + } + + public Button getButton() { + return button; + } + + public EnumSet getModifiers() { + return modifiers; + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + @Override + public String toString() { + return "MouseEvent[" + + "type=" + type + + ", button=" + button + + ", modifiers=" + modifiers + + ", x=" + x + + ", y=" + y + + ']'; + } +} diff --git a/src/main/java/org/jline/terminal/Terminal.java b/src/main/java/org/jline/terminal/Terminal.java index 152158ad1..753dfd7bb 100644 --- a/src/main/java/org/jline/terminal/Terminal.java +++ b/src/main/java/org/jline/terminal/Terminal.java @@ -13,13 +13,25 @@ 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; import org.jline.utils.NonBlockingReader; +/** + * A terminal representing a virtual terminal on the computer. + * + * Terminals should be closed by calling the {@link #close()} method + * in order to restore their original state. + */ public interface Terminal extends Closeable, Flushable { + /** + * Type used for dumb terminals. + */ + String TYPE_DUMB = "dumb"; + String getName(); // @@ -101,4 +113,36 @@ 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 null if cursor position reporting + * is not supported or a valid cursor position + */ + Cursor getCursorPosition(IntConsumer discarded); + + // + // Mouse support + // + + enum MouseTracking { + Off, Normal, Button, Any + } + + boolean hasMouseSupport(); + + boolean trackMouse(MouseTracking tracking); + + MouseEvent readMouseEvent(); + } diff --git a/src/main/java/org/jline/terminal/TerminalBuilder.java b/src/main/java/org/jline/terminal/TerminalBuilder.java index 8c88b8bd3..edbf820cf 100644 --- a/src/main/java/org/jline/terminal/TerminalBuilder.java +++ b/src/main/java/org/jline/terminal/TerminalBuilder.java @@ -29,12 +29,26 @@ import org.jline.utils.Log; import org.jline.utils.OSUtils; +/** + * Builder class to create terminals. + */ public final class TerminalBuilder { + /** + * Returns the default system terminal. + * Terminals should be closed properly using the {@link Terminal#close()} + * method in order to restore the original terminal state. + * + * This call is equivalent to: + * builder().build() + */ public static Terminal terminal() throws IOException { return builder().build(); } + /** + * Creates a new terminal builder instance. + */ public static TerminalBuilder builder() { return new TerminalBuilder(); } @@ -46,6 +60,7 @@ public static TerminalBuilder builder() { private String encoding; private Boolean system; private boolean jna = true; + private Boolean dumb; private Attributes attributes; private Size size; private boolean nativeSignals = false; @@ -75,6 +90,11 @@ public TerminalBuilder jna(boolean jna) { return this; } + public TerminalBuilder dumb(boolean dumb) { + this.dumb = dumb; + return this; + } + public TerminalBuilder type(String type) { this.type = type; return this; @@ -85,11 +105,31 @@ public TerminalBuilder encoding(String encoding) { return this; } + /** + * Attributes to use when creating a non system terminal, + * i.e. when the builder has been given the input and + * outut streams using the {@link #streams(InputStream, OutputStream)} method + * or when {@link #system(boolean)} has been explicitely called with + * false. + * + * @see #size(Size) + * @see #system(boolean) + */ public TerminalBuilder attributes(Attributes attributes) { this.attributes = attributes; return this; } + /** + * Initial size to use when creating a non system terminal, + * i.e. when the builder has been given the input and + * outut streams using the {@link #streams(InputStream, OutputStream)} method + * or when {@link #system(boolean)} has been explicitely called with + * false. + * + * @see #attributes(Attributes) + * @see #system(boolean) + */ public TerminalBuilder size(Size size) { this.size = size; return this; @@ -107,9 +147,9 @@ public TerminalBuilder signalHandler(Terminal.SignalHandler signalHandler) { public Terminal build() throws IOException { Terminal terminal = doBuild(); - Log.debug("Using terminal " + terminal.getClass().getSimpleName()); + Log.debug(() -> "Using terminal " + terminal.getClass().getSimpleName()); if (terminal instanceof AbstractPosixTerminal) { - Log.debug("Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); + Log.debug(() -> "Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName()); } return terminal; } @@ -128,22 +168,38 @@ private Terminal doBuild() throws IOException { type = System.getenv("TERM"); } if ((system != null && system) || (system == null && in == null && out == null)) { + if (attributes != null || size != null) { + Log.warn("Attributes and size fields are ignored when creating a system terminal"); + } + IllegalStateException exception = new IllegalStateException("Unable to create a system terminal"); // // Cygwin support // if (OSUtils.IS_CYGWIN) { - Pty pty = ExecPty.current(); - return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); + try { + Pty pty = ExecPty.current(); + return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); + } catch (IOException e) { + // Ignore if not a tty + Log.debug("Error creating exec based pty: ", e.getMessage(), e); + exception.addSuppressed(e); + } } else if (OSUtils.IS_WINDOWS) { if (useJna()) { try { return new JnaWinSysTerminal(name, nativeSignals, signalHandler); } catch (Throwable t) { - Log.debug("Error creating JNA based pty", t.getMessage()); + Log.debug("Error creating JNA based terminal: ", t.getMessage(), t); + exception.addSuppressed(t); } } - return new JansiWinSysTerminal(name, nativeSignals, signalHandler); + try { + return new JansiWinSysTerminal(name, nativeSignals, signalHandler); + } catch (Throwable t) { + Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t); + exception.addSuppressed(t); + } } else { Pty pty = null; if (useJna()) { @@ -151,36 +207,51 @@ else if (OSUtils.IS_WINDOWS) { pty = JnaNativePty.current(); } catch (Throwable t) { // ignore - Log.debug("Error creating JNA based pty", t.getMessage()); + Log.debug("Error creating JNA based pty: ", t.getMessage(), t); + exception.addSuppressed(t); } } if (pty == null) { try { pty = ExecPty.current(); - } catch (IOException e) { + } catch (Throwable t) { // Ignore if not a tty - Log.debug("Error creating exec based pty", e.getMessage()); + Log.debug("Error creating exec based pty: ", t.getMessage(), t); + exception.addSuppressed(t); } } if (pty != null) { return new PosixSysTerminal(name, type, pty, encoding, nativeSignals, signalHandler); - } else { - return new DumbTerminal(name, type, - new FileInputStream(FileDescriptor.in), - new FileOutputStream(FileDescriptor.out), - encoding, signalHandler); } } + if (dumb == null || dumb) { + if (dumb == null) { + Log.warn("Creating a dumb terminal", exception); + } + return new DumbTerminal(name, type, + new FileInputStream(FileDescriptor.in), + new FileOutputStream(FileDescriptor.out), + encoding, signalHandler); + } else { + throw exception; + } } else { if (useJna()) { try { Pty pty = JnaNativePty.open(attributes, size); return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler); } catch (Throwable t) { - Log.debug("Error creating JNA based pty", t.getMessage()); + Log.debug("Error creating JNA based pty: ", t.getMessage(), t); } } - return new ExternalTerminal(name, type, in, out, encoding, signalHandler); + Terminal terminal = new ExternalTerminal(name, type, in, out, encoding, signalHandler); + if (attributes != null) { + terminal.setAttributes(attributes); + } + if (size != null) { + terminal.setSize(size); + } + return terminal; } } diff --git a/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java b/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java index 48222706a..01367c157 100644 --- a/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java +++ b/src/main/java/org/jline/terminal/impl/AbstractPosixTerminal.java @@ -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 { @@ -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); + } + } diff --git a/src/main/java/org/jline/terminal/impl/AbstractTerminal.java b/src/main/java/org/jline/terminal/impl/AbstractTerminal.java index 02eba743b..ae35a6743 100644 --- a/src/main/java/org/jline/terminal/impl/AbstractTerminal.java +++ b/src/main/java/org/jline/terminal/impl/AbstractTerminal.java @@ -16,11 +16,14 @@ 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.MouseEvent; import org.jline.terminal.Terminal; import org.jline.utils.Curses; import org.jline.utils.InfoCmp; @@ -165,4 +168,27 @@ protected void parseInfoCmp() { InfoCmp.parseInfoCmp(capabilities, bools, ints, strings); } + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return null; + } + + private MouseEvent lastMouseEvent = new MouseEvent( + MouseEvent.Type.Moved, MouseEvent.Button.NoButton, + EnumSet.noneOf(MouseEvent.Modifier.class), 0, 0); + + @Override + public boolean hasMouseSupport() { + return MouseSupport.hasMouseSupport(this); + } + + @Override + public boolean trackMouse(MouseTracking tracking) { + return MouseSupport.trackMouse(this, tracking); + } + + @Override + public MouseEvent readMouseEvent() { + return lastMouseEvent = MouseSupport.readMouse(this, lastMouseEvent); + } } diff --git a/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java b/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java index 4658d093a..7728e7590 100644 --- a/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java +++ b/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java @@ -53,6 +53,7 @@ public abstract class AbstractWindowsTerminal extends AbstractTerminal { protected final Attributes attributes = new Attributes(); protected final Thread pump; + protected MouseTracking tracking = MouseTracking.Off; private volatile boolean closing; public AbstractWindowsTerminal(OutputStream output, String name, boolean nativeSignals, SignalHandler signalHandler) throws IOException { @@ -140,13 +141,20 @@ public Attributes getAttributes() { public void setAttributes(Attributes attr) { attributes.copy(attr); - int mode = 0; - if (attr.getLocalFlag(Attributes.LocalFlag.ECHO)) { + updateConsoleMode(); + } + + protected void updateConsoleMode() { + int mode = ENABLE_WINDOW_INPUT; + if (attributes.getLocalFlag(Attributes.LocalFlag.ECHO)) { mode |= ENABLE_ECHO_INPUT; } - if (attr.getLocalFlag(Attributes.LocalFlag.ICANON)) { + if (attributes.getLocalFlag(Attributes.LocalFlag.ICANON)) { mode |= ENABLE_LINE_INPUT; } + if (tracking != MouseTracking.Off) { + mode |= ENABLE_MOUSE_INPUT; + } setConsoleMode(mode); } @@ -176,6 +184,8 @@ public void close() throws IOException { protected abstract byte[] readConsoleInput(); protected String getEscapeSequence(short keyCode) { + // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx + // TODO: numpad keys, modifiers String escapeSequence = null; switch (keyCode) { case 0x08: // VK_BACK BackSpace @@ -314,5 +324,13 @@ public void processInputByte(int c) throws IOException { slaveInputPipe.write(c); slaveInputPipe.flush(); } + + @Override + public boolean trackMouse(MouseTracking tracking) { + this.tracking = tracking; + updateConsoleMode(); + return true; + } + } diff --git a/src/main/java/org/jline/terminal/impl/CursorSupport.java b/src/main/java/org/jline/terminal/impl/CursorSupport.java new file mode 100644 index 000000000..6397eea3d --- /dev/null +++ b/src/main/java/org/jline/terminal/impl/CursorSupport.java @@ -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); + } + } + +} diff --git a/src/main/java/org/jline/terminal/impl/DumbTerminal.java b/src/main/java/org/jline/terminal/impl/DumbTerminal.java index e2354e5ec..4ad110d7c 100644 --- a/src/main/java/org/jline/terminal/impl/DumbTerminal.java +++ b/src/main/java/org/jline/terminal/impl/DumbTerminal.java @@ -33,7 +33,7 @@ public class DumbTerminal extends AbstractTerminal { private final Size size; public DumbTerminal(InputStream in, OutputStream out) throws IOException { - this("dumb", "ansi", in, out, Charset.defaultCharset().name()); + this(TYPE_DUMB, TYPE_DUMB, in, out, Charset.defaultCharset().name()); } public DumbTerminal(String name, String type, InputStream in, OutputStream out, String encoding) throws IOException { diff --git a/src/main/java/org/jline/terminal/impl/ExternalTerminal.java b/src/main/java/org/jline/terminal/impl/ExternalTerminal.java index 6b4b7de1d..a052223ef 100644 --- a/src/main/java/org/jline/terminal/impl/ExternalTerminal.java +++ b/src/main/java/org/jline/terminal/impl/ExternalTerminal.java @@ -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. @@ -74,4 +77,9 @@ public void pump() { } } + @Override + public Cursor getCursorPosition(IntConsumer discarded) { + return CursorSupport.getCursorPosition(this, discarded); + } + } diff --git a/src/main/java/org/jline/terminal/impl/MouseSupport.java b/src/main/java/org/jline/terminal/impl/MouseSupport.java new file mode 100644 index 000000000..78f9bb781 --- /dev/null +++ b/src/main/java/org/jline/terminal/impl/MouseSupport.java @@ -0,0 +1,113 @@ +/* + * 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.MouseEvent; +import org.jline.terminal.Terminal; +import org.jline.utils.InfoCmp; + +import java.io.EOFException; +import java.io.IOError; +import java.io.IOException; +import java.util.EnumSet; + +public class MouseSupport { + + public enum Tracking { + Normal, Button, Any + } + + public static boolean hasMouseSupport(Terminal terminal) { + return terminal.getStringCapability(InfoCmp.Capability.key_mouse) != null; + } + + public static boolean trackMouse(Terminal terminal, Terminal.MouseTracking tracking) { + if (hasMouseSupport(terminal)) { + switch (tracking) { + case Off: + terminal.writer().write("\033[?1000l"); + break; + case Normal: + terminal.writer().write("\033[?1005h\033[?1000h"); + break; + case Button: + terminal.writer().write("\033[?1005h\033[?1002h"); + break; + case Any: + terminal.writer().write("\033[?1005h\033[?1003h"); + break; + } + terminal.flush(); + return true; + } else { + return false; + } + } + + public static MouseEvent readMouse(Terminal terminal, MouseEvent last) { + int cb = readExt(terminal) - ' '; + int cx = readExt(terminal) - ' ' - 1; + int cy = readExt(terminal) - ' ' - 1; + MouseEvent.Type type; + MouseEvent.Button button; + EnumSet modifiers = EnumSet.noneOf(MouseEvent.Modifier.class); + if ((cb & 4) == 4) { + modifiers.add(MouseEvent.Modifier.Shift); + } + if ((cb & 8) == 8) { + modifiers.add(MouseEvent.Modifier.Alt); + } + if ((cb & 16) == 16) { + modifiers.add(MouseEvent.Modifier.Control); + } + if ((cb & 64) == 64) { + type = MouseEvent.Type.Wheel; + button = (cb & 1) == 1 ? MouseEvent.Button.WheelDown : MouseEvent.Button.WheelUp; + } else { + int b = (cb & 3); + switch (b) { + case 0: + button = MouseEvent.Button.Button1; + type = last.getButton() == button ? MouseEvent.Type.Dragged : MouseEvent.Type.Pressed; + break; + case 1: + button = MouseEvent.Button.Button2; + type = last.getButton() == button ? MouseEvent.Type.Dragged : MouseEvent.Type.Pressed; + break; + case 2: + button = MouseEvent.Button.Button3; + type = last.getButton() == button ? MouseEvent.Type.Dragged : MouseEvent.Type.Pressed; + break; + default: + if (last.getType() == MouseEvent.Type.Pressed || last.getType() == MouseEvent.Type.Dragged) { + button = last.getButton(); + type = MouseEvent.Type.Released; + } else { + button = MouseEvent.Button.NoButton; + type = MouseEvent.Type.Moved; + } + break; + } + } + return new MouseEvent(type, button, modifiers, cx, cy); + } + + private static int readExt(Terminal terminal) { + try { + int c = terminal.reader().read(); + if (c < 0) { + throw new EOFException(); + } + return c; + } catch (IOException e) { + throw new IOError(e); + } + } + +} diff --git a/src/main/java/org/jline/terminal/impl/jansi/JansiWinSysTerminal.java b/src/main/java/org/jline/terminal/impl/jansi/JansiWinSysTerminal.java index 84550358c..8e0a8aeeb 100644 --- a/src/main/java/org/jline/terminal/impl/jansi/JansiWinSysTerminal.java +++ b/src/main/java/org/jline/terminal/impl/jansi/JansiWinSysTerminal.java @@ -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 { @@ -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); + } + } diff --git a/src/main/java/org/jline/terminal/impl/jna/win/JnaWinSysTerminal.java b/src/main/java/org/jline/terminal/impl/jna/win/JnaWinSysTerminal.java index fa03fbcf2..7b4200c04 100644 --- a/src/main/java/org/jline/terminal/impl/jna/win/JnaWinSysTerminal.java +++ b/src/main/java/org/jline/terminal/impl/jna/win/JnaWinSysTerminal.java @@ -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; @@ -24,6 +26,8 @@ public class JnaWinSysTerminal extends AbstractWindowsTerminal { private static final Pointer consoleIn = Kernel32.INSTANCE.GetStdHandle(Kernel32.STD_INPUT_HANDLE); private static final Pointer consoleOut = Kernel32.INSTANCE.GetStdHandle(Kernel32.STD_OUTPUT_HANDLE); + private int prevButtonState; + public JnaWinSysTerminal(String name, boolean nativeSignals) throws IOException { this(name, nativeSignals, SignalHandler.SIG_DFL); } @@ -55,6 +59,8 @@ public Size getSize() { return new Size(info.windowWidth(), info.windowHeight()); } + private char[] mouse = new char[] { '\033', '[', 'M', ' ', ' ', ' ' }; + protected byte[] readConsoleInput() { // XXX does how many events to read in one call matter? Kernel32.INPUT_RECORD[] events = null; @@ -92,8 +98,6 @@ protected byte[] readConsoleInput() { sb.append(keyEvent.uChar.UnicodeChar); } } else { - // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx - // TODO: numpad keys, modifiers String escapeSequence = getEscapeSequence(keyEvent.wVirtualKeyCode); if (escapeSequence != null) { for (int k = 0; k < keyEvent.wRepeatCount; k++) { @@ -113,6 +117,40 @@ protected byte[] readConsoleInput() { } } else if (event.EventType == Kernel32.INPUT_RECORD.WINDOW_BUFFER_SIZE_EVENT) { raise(Signal.WINCH); + } else if (event.EventType == Kernel32.INPUT_RECORD.MOUSE_EVENT) { + Kernel32.MOUSE_EVENT_RECORD mouseEvent = event.Event.MouseEvent; + int dwEventFlags = mouseEvent.dwEventFlags; + int dwButtonState = mouseEvent.dwButtonState; + if (tracking == MouseTracking.Off + || tracking == MouseTracking.Normal && dwEventFlags == Kernel32.MOUSE_MOVED + || tracking == MouseTracking.Button && dwEventFlags == Kernel32.MOUSE_MOVED && dwButtonState == 0) { + continue; + } + int cb = 0; + dwEventFlags &= ~ Kernel32.DOUBLE_CLICK; // Treat double-clicks as normal + if (dwEventFlags == Kernel32.MOUSE_WHEELED) { + cb |= 64; + if ((dwButtonState >> 16) < 0) { + cb |= 1; + } + } else if (dwEventFlags == Kernel32.MOUSE_HWHEELED) { + continue; + } else if ((dwButtonState & Kernel32.FROM_LEFT_1ST_BUTTON_PRESSED) != 0) { + cb |= 0x00; + } else if ((dwButtonState & Kernel32.RIGHTMOST_BUTTON_PRESSED) != 0) { + cb |= 0x01; + } else if ((dwButtonState & Kernel32.FROM_LEFT_2ND_BUTTON_PRESSED) != 0) { + cb |= 0x02; + } else { + cb |= 0x03; + } + int cx = mouseEvent.dwMousePosition.X; + int cy = mouseEvent.dwMousePosition.Y; + mouse[3] = (char) (' ' + cb); + mouse[4] = (char) (' ' + cx + 1); + mouse[5] = (char) (' ' + cy + 1); + sb.append(mouse); + prevButtonState = dwButtonState; } } return sb.toString().getBytes(); @@ -126,10 +164,18 @@ private Kernel32.INPUT_RECORD[] doReadConsoleInput() throws IOException { switch (ir[i].EventType) { case Kernel32.INPUT_RECORD.KEY_EVENT: case Kernel32.INPUT_RECORD.WINDOW_BUFFER_SIZE_EVENT: + case Kernel32.INPUT_RECORD.MOUSE_EVENT: return ir; } } 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); + } + } diff --git a/src/main/java/org/jline/terminal/impl/jna/win/Kernel32.java b/src/main/java/org/jline/terminal/impl/jna/win/Kernel32.java index ebe17db80..c8da4142d 100644 --- a/src/main/java/org/jline/terminal/impl/jna/win/Kernel32.java +++ b/src/main/java/org/jline/terminal/impl/jna/win/Kernel32.java @@ -51,6 +51,18 @@ interface Kernel32 extends StdCallLibrary { int BACKGROUND_RED = 0x0040; int BACKGROUND_INTENSITY = 0x0080; + // Button state + int FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001; + int RIGHTMOST_BUTTON_PRESSED = 0x0002; + int FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004; + int FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008; + int FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010; + + // Event flags + int MOUSE_MOVED = 0x0001; + int DOUBLE_CLICK = 0x0002; + int MOUSE_WHEELED = 0x0004; + int MOUSE_HWHEELED = 0x0008; // HANDLE WINAPI GetStdHandle( // __in DWORD nStdHandle @@ -378,7 +390,7 @@ class INPUT_RECORD extends Structure { public static class EventUnion extends Union { public KEY_EVENT_RECORD KeyEvent; - // MOUSE_EVENT_RECORD MouseEvent; + public MOUSE_EVENT_RECORD MouseEvent; // WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent; // MENU_EVENT_RECORD MenuEvent; // FOCUS_EVENT_RECORD FocusEvent; @@ -391,6 +403,9 @@ public void read() { case KEY_EVENT: Event.setType(KEY_EVENT_RECORD.class); break; + case MOUSE_EVENT: + Event.setType(MOUSE_EVENT_RECORD.class); + break; } super.read(); } @@ -430,6 +445,20 @@ protected java.util.List getFieldOrder() { } } + class MOUSE_EVENT_RECORD extends Structure { + public COORD dwMousePosition; + public int dwButtonState; + public int dwControlKeyState; + public int dwEventFlags; + + private static String[] fieldOrder = { "dwMousePosition", "dwButtonState", "dwControlKeyState", "dwEventFlags"}; + + @Override + protected java.util.List getFieldOrder() { + return java.util.Arrays.asList(fieldOrder); + } + } + // typedef struct _SMALL_RECT { // SHORT Left; // SHORT Top; diff --git a/src/main/java/org/jline/utils/ExecHelper.java b/src/main/java/org/jline/utils/ExecHelper.java index 0f02715f0..0836682e9 100644 --- a/src/main/java/org/jline/utils/ExecHelper.java +++ b/src/main/java/org/jline/utils/ExecHelper.java @@ -36,6 +36,9 @@ public static String exec(boolean redirectInput, final String... cmd) throws IOE String result = waitAndCapture(p); Log.trace("Result: ", result); if (p.exitValue() != 0) { + if (result.endsWith("\n")) { + result = result.substring(0, result.length() - 1); + } throw new IOException("Error executing '" + String.join(" ", (CharSequence[]) cmd) + "': " + result); } return result; diff --git a/src/main/java/org/jline/utils/InfoCmp.java b/src/main/java/org/jline/utils/InfoCmp.java index e46d016dd..cd26079d7 100644 --- a/src/main/java/org/jline/utils/InfoCmp.java +++ b/src/main/java/org/jline/utils/InfoCmp.java @@ -599,7 +599,8 @@ public static void parseInfoCmp( "\tsmul=\\E[4m,\n" + "\tkdch1=\\E[3~, kich1=\\E[2~, kend=\\E[4~, knp=\\E[6~, kpp=\\E[5~,\n" + "\tkf1=\\EOP, kf2=\\EOQ, kf3=\\EOR, kf4=\\EOS, kf5=\\E[15~, kf6=\\E[17~,\n" + - "\tkf7=\\E[18~, kf8=\\E[19~, kf9=\\E[20~, kf10=\\E[21~, kf11=\\E[23~, kf12=\\E[24~,\n"; + "\tkf7=\\E[18~, kf8=\\E[19~, kf9=\\E[20~, kf10=\\E[21~, kf11=\\E[23~, kf12=\\E[24~,\n" + + "\tkmous=\\E[M,"; public static final String ANSI_CAPS = "ansi|ansi/pc-term compatible with color,\n" + @@ -731,6 +732,7 @@ public static void parseInfoCmp( "\tu8=\\E[?1;2c, u9=\\E[c, vpa=\\E[%i%p1%dd,"; static { + setDefaultInfoCmp("dumb", ANSI_CAPS); setDefaultInfoCmp("ansi", ANSI_CAPS); setDefaultInfoCmp("xterm", XTERM_CAPS); setDefaultInfoCmp("xterm-256color", XTERM_256COLOR_CAPS); diff --git a/src/main/java/org/jline/utils/Log.java b/src/main/java/org/jline/utils/Log.java index 636b39d93..3b5eabdeb 100644 --- a/src/main/java/org/jline/utils/Log.java +++ b/src/main/java/org/jline/utils/Log.java @@ -10,6 +10,7 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; @@ -27,6 +28,10 @@ public static void trace(final Object... messages) { log(Level.FINEST, messages); } + public static void debug(Supplier supplier) { + log(Level.FINE, supplier); + } + public static void debug(final Object... messages) { log(Level.FINE, messages); } @@ -47,7 +52,7 @@ public static void error(final Object... messages) { * Helper to support rendering messages. */ static void render(final PrintStream out, final Object message) { - if (message.getClass().isArray()) { + if (message != null && message.getClass().isArray()) { Object[] array = (Object[]) message; out.print("["); @@ -64,8 +69,7 @@ static void render(final PrintStream out, final Object message) { } } - static void log(final Level level, final Object... messages) { - Logger logger = Logger.getLogger("org.jline"); + static LogRecord createRecord(final Level level, final Object... messages) { Throwable cause = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos); @@ -81,7 +85,26 @@ static void log(final Level level, final Object... messages) { ps.close(); LogRecord r = new LogRecord(level, baos.toString()); r.setThrown(cause); - logger.log(r); + return r; + } + + static LogRecord createRecord(final Level level, final Supplier message) { + return new LogRecord(level, message.get()); + } + + static void log(final Level level, final Supplier message) { + logr(level, () -> createRecord(level, message)); + } + + static void log(final Level level, final Object... messages) { + logr(level, () -> createRecord(level, messages)); + } + + static void logr(final Level level, final Supplier record) { + Logger logger = Logger.getLogger("org.jline"); + if (logger.isLoggable(level)) { + logger.log(record.get()); + } } } \ No newline at end of file diff --git a/src/main/java/org/jline/utils/OSUtils.java b/src/main/java/org/jline/utils/OSUtils.java index 0dc4cf76c..bbf1289eb 100644 --- a/src/main/java/org/jline/utils/OSUtils.java +++ b/src/main/java/org/jline/utils/OSUtils.java @@ -16,7 +16,8 @@ public class OSUtils { public static final boolean IS_CYGWIN = IS_WINDOWS && System.getenv("PWD") != null - && System.getenv("PWD").startsWith("/"); + && System.getenv("PWD").startsWith("/") + && !"cygwin".equals(System.getenv("TERM")); public static final boolean IS_OSX = System.getProperty("os.name").toLowerCase().contains("mac"); diff --git a/src/main/resources/org/jline/editor/nano-browser-help.txt b/src/main/resources/org/jline/builtins/nano-browser-help.txt similarity index 100% rename from src/main/resources/org/jline/editor/nano-browser-help.txt rename to src/main/resources/org/jline/builtins/nano-browser-help.txt diff --git a/src/main/resources/org/jline/editor/nano-main-help.txt b/src/main/resources/org/jline/builtins/nano-main-help.txt similarity index 100% rename from src/main/resources/org/jline/editor/nano-main-help.txt rename to src/main/resources/org/jline/builtins/nano-main-help.txt diff --git a/src/main/resources/org/jline/editor/nano-read-help.txt b/src/main/resources/org/jline/builtins/nano-read-help.txt similarity index 100% rename from src/main/resources/org/jline/editor/nano-read-help.txt rename to src/main/resources/org/jline/builtins/nano-read-help.txt diff --git a/src/main/resources/org/jline/editor/nano-search-help.txt b/src/main/resources/org/jline/builtins/nano-search-help.txt similarity index 100% rename from src/main/resources/org/jline/editor/nano-search-help.txt rename to src/main/resources/org/jline/builtins/nano-search-help.txt diff --git a/src/main/resources/org/jline/editor/nano-write-help.txt b/src/main/resources/org/jline/builtins/nano-write-help.txt similarity index 100% rename from src/main/resources/org/jline/editor/nano-write-help.txt rename to src/main/resources/org/jline/builtins/nano-write-help.txt diff --git a/src/test/java/org/jline/example/Example.java b/src/test/java/org/jline/example/Example.java index 02f6ac52f..9124c0566 100644 --- a/src/test/java/org/jline/example/Example.java +++ b/src/test/java/org/jline/example/Example.java @@ -13,19 +13,12 @@ import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.util.Map; +import java.util.*; import org.jline.keymap.KeyMap; -import org.jline.reader.Binding; -import org.jline.reader.Completer; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.EndOfFileException; -import org.jline.reader.ParsedLine; -import org.jline.reader.Reference; -import org.jline.reader.UserInterruptException; +import org.jline.reader.*; +import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.LineReaderImpl; -import org.jline.reader.Macro; import org.jline.reader.impl.completer.ArgumentCompleter; import org.jline.reader.impl.completer.FileNameCompleter; import org.jline.reader.impl.completer.StringsCompleter; @@ -79,6 +72,7 @@ public static void main(String[] args) throws IOException { } Completer completer = null; + Parser parser = null; int index = 0; label: @@ -118,11 +112,38 @@ public static void main(String[] args) throws IOException { case "simple": completer = new StringsCompleter("foo", "bar", "baz"); break label; + case "quotes": + DefaultParser p = new DefaultParser(); + p.setEofOnUnclosedQuote(true); + parser = p; + break label; case "foo": completer = new ArgumentCompleter( new StringsCompleter("foo11", "foo12", "foo13"), new StringsCompleter("foo21", "foo22", "foo23")); break label; + case "param": + completer = (reader, line, candidates) -> { + if (line.wordIndex() == 0) { + candidates.add(new Candidate("Command1")); + } else if (line.words().get(0).equals("Command1")) { + if (line.words().get(line.wordIndex() - 1).equals("Option1")) { + candidates.add(new Candidate("Param1")); + candidates.add(new Candidate("Param2")); + } else { + if (line.wordIndex() == 1) { + candidates.add(new Candidate("Option1")); + } + if (!line.words().contains("Option2")) { + candidates.add(new Candidate("Option2")); + } + if (!line.words().contains("Option3")) { + candidates.add(new Candidate("Option3")); + } + } + } + }; + break label; case "color": color = true; prompt = new AttributedStringBuilder() @@ -163,6 +184,7 @@ public static void main(String[] args) throws IOException { LineReader reader = LineReaderBuilder.builder() .terminal(terminal) .completer(completer) + .parser(parser) .build(); while (true) { @@ -265,4 +287,5 @@ else if ("sleep".equals(pl.word())) { t.printStackTrace(); } } + } diff --git a/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java b/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java index c3757d4a5..19cc5b294 100644 --- a/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java +++ b/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java @@ -12,6 +12,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; import java.util.stream.IntStream; import org.jline.reader.LineReader; @@ -41,26 +43,58 @@ public void tearDown() throws IOException { Files.deleteIfExists(Paths.get("test")); } - private void doTestFileHistory(int count) { - reader.setVariable(LineReader.HISTORY_FILE, Paths.get("test")); + + + private void doTestFileHistory(int count, CyclicBarrier barrier) { DefaultHistory history = new DefaultHistory(reader); + try { + barrier.await(); + } catch (InterruptedException | BrokenBarrierException e) { + throw new RuntimeException(e); + } + assertEquals(count, history.size()); IntStream.range(0, count) .forEach(i -> history.add("cmd" + i)); - history.save(); + // we need to synchronize here + // if we don't, multiple writes can occur at the same time and some + // history items may be lost, we'd have to use a file lock to fix that + // but that's not testable + // what we're testing here is the fact that only *new* items are + // written to the file incrementally and that we're not rewriting the + // whole file + synchronized (reader) { + history.save(); + } } @Test public void testFileHistory() throws Exception { - doTestFileHistory(3); - List ts = IntStream.range(0, 3) - .mapToObj(i -> new Thread(() -> doTestFileHistory(3))) + reader.setVariable(LineReader.HISTORY_FILE, Paths.get("test")); + reader.unsetOpt(LineReader.Option.HISTORY_INCREMENTAL); + + int cmdsPerThread = 3; + int nbThreads = 3; + + DefaultHistory history = new DefaultHistory(reader); + IntStream.range(0, cmdsPerThread) + .forEach(i -> history.add("cmd" + i)); + history.save(); + + List lines = Files.readAllLines(Paths.get("test")); + assertEquals(cmdsPerThread, lines.size()); + + final CyclicBarrier barrier = new CyclicBarrier(nbThreads); + List ts = IntStream.range(0, nbThreads) + .mapToObj(i -> new Thread(() -> { + doTestFileHistory(cmdsPerThread, barrier); + })) .collect(toList()); ts.forEach(Thread::start); for (Thread t : ts) { t.join(); } - List lines = Files.readAllLines(Paths.get("test")); - assertEquals(3 * 4, lines.size()); + lines = Files.readAllLines(Paths.get("test")); + assertEquals(cmdsPerThread * (nbThreads + 1), lines.size()); } } diff --git a/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java b/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java index dfc52edfc..6b3451928 100644 --- a/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java +++ b/src/test/java/org/jline/terminal/impl/ExternalTerminalTest.java @@ -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; @@ -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(2, cursor.getX()); + assertEquals(1, cursor.getY()); + assertEquals("a\033b", sb.toString()); + assertEquals('f', console.reader().read()); + } }