You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2014/05/15 19:22:02 UTC
[1/2] git commit: [KARAF-2983] Support window size change signals in
Terminal
Repository: karaf
Updated Branches:
refs/heads/master e5e50310a -> 57ec802bc
[KARAF-2983] Support window size change signals in Terminal
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/a5450aa2
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/a5450aa2
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/a5450aa2
Branch: refs/heads/master
Commit: a5450aa241cb6e15d430ad4792d924f2636bd3f1
Parents: e5e5031
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Thu May 15 19:09:32 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Thu May 15 19:09:32 2014 +0200
----------------------------------------------------------------------
.../apache/karaf/shell/api/console/Signal.java | 81 +++++++++++++++++
.../karaf/shell/api/console/SignalListener.java | 31 +++++++
.../karaf/shell/api/console/Terminal.java | 27 ++++++
.../shell/impl/console/ConsoleSessionImpl.java | 8 ++
.../karaf/shell/impl/console/JLineTerminal.java | 57 +++++++++++-
.../impl/console/osgi/LocalConsoleManager.java | 5 +-
.../shell/support/terminal/SignalSupport.java | 96 ++++++++++++++++++++
.../org/apache/karaf/shell/ssh/SshTerminal.java | 12 ++-
.../karaf/webconsole/gogo/WebTerminal.java | 3 +-
9 files changed, 311 insertions(+), 9 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/api/console/Signal.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/console/Signal.java b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Signal.java
new file mode 100644
index 0000000..1e4a563
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Signal.java
@@ -0,0 +1,81 @@
+/*
+ * 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 org.apache.karaf.shell.api.console;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum Signal {
+
+ HUP(1),
+ INT(2),
+ QUIT(3),
+ ILL(4),
+ TRAP(5),
+ IOT(6),
+ BUS(7),
+ FPE(8),
+ KILL(9),
+ USR1(10),
+ SEGV(11),
+ USR2(12),
+ PIPE(13),
+ ALRM(14),
+ TERM(15),
+ STKFLT(16),
+ CHLD(17),
+ CONT(18),
+ STOP(19),
+ TSTP(20),
+ TTIN(21),
+ TTOU(22),
+ URG(23),
+ XCPU(24),
+ XFSZ(25),
+ VTALRM(26),
+ PROF(27),
+ WINCH(28),
+ IO(29),
+ PWR(30);
+
+ private static final Map<String, Signal> lookupTable = new HashMap<String, Signal>(40);
+
+ static {
+ // registering the signals in the lookup table to allow faster
+ // string based signal lookups
+ for (Signal s : Signal.values()) {
+ lookupTable.put(s.name(), s);
+ }
+ }
+
+ public static Signal get(String name) {
+ return lookupTable.get(name);
+ }
+
+ private final int numeric;
+
+ private Signal(int numeric) {
+ this.numeric = numeric;
+ }
+
+ public int getNumeric() {
+ return numeric;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/api/console/SignalListener.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/console/SignalListener.java b/shell/core/src/main/java/org/apache/karaf/shell/api/console/SignalListener.java
new file mode 100644
index 0000000..3f6927a
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/console/SignalListener.java
@@ -0,0 +1,31 @@
+/*
+ * 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 org.apache.karaf.shell.api.console;
+
+/**
+ * Define a listener to receive signals
+ */
+public interface SignalListener {
+
+ /**
+ *
+ * @param signal
+ */
+ void signal(Signal signal);
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/api/console/Terminal.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/console/Terminal.java b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Terminal.java
index 3549ec7..c0ee911 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/api/console/Terminal.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Terminal.java
@@ -18,6 +18,8 @@
*/
package org.apache.karaf.shell.api.console;
+import java.util.EnumSet;
+
/**
* Session terminal.
*/
@@ -48,4 +50,29 @@ public interface Terminal {
*/
void setEchoEnabled(boolean enabled);
+ /**
+ * Add a qualified listener for the specific signal
+ * @param listener the listener to register
+ * @param signal the signal the listener is interested in
+ */
+ void addSignalListener(SignalListener listener, Signal... signal);
+
+ /**
+ * Add a qualified listener for the specific set of signal
+ * @param listener the listener to register
+ * @param signals the signals the listener is interested in
+ */
+ void addSignalListener(SignalListener listener, EnumSet<Signal> signals);
+
+ /**
+ * Add a global listener for all signals
+ * @param listener the listener to register
+ */
+ void addSignalListener(SignalListener listener);
+
+ /**
+ * Remove a previously registered listener for all the signals it was registered
+ * @param listener the listener to remove
+ */
+ void removeSignalListener(SignalListener listener);
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
index 1673f37..dbfc59a 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
@@ -19,6 +19,7 @@
package org.apache.karaf.shell.impl.console;
import java.io.CharArrayWriter;
+import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -233,6 +234,13 @@ public class ConsoleSessionImpl implements Session {
if (closeCallback != null) {
closeCallback.run();
}
+ if (terminal instanceof Closeable) {
+ try {
+ ((Closeable) terminal).close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
}
public void run() {
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
index d000885..aaf4eb6 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/JLineTerminal.java
@@ -18,17 +18,26 @@
*/
package org.apache.karaf.shell.impl.console;
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import org.apache.karaf.shell.api.console.Signal;
import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.terminal.SignalSupport;
/**
-* Created by gnodet on 27/02/14.
-*/
-public class JLineTerminal implements Terminal {
+ * Created by gnodet on 27/02/14.
+ */
+public class JLineTerminal extends SignalSupport implements Terminal, Closeable {
private final jline.Terminal terminal;
public JLineTerminal(jline.Terminal terminal) {
this.terminal = terminal;
+ registerSignalHandler();
}
public jline.Terminal getTerminal() {
@@ -59,4 +68,46 @@ public class JLineTerminal implements Terminal {
public void setEchoEnabled(boolean enabled) {
terminal.setEchoEnabled(enabled);
}
+
+ @Override
+ public void close() throws IOException {
+ unregisterSignalHandler();
+ }
+
+ private void registerSignalHandler() {
+ try {
+ Class<?> signalClass = Class.forName("sun.misc.Signal");
+ Class<?> signalHandlerClass = Class.forName("sun.misc.SignalHandler");
+ // Implement signal handler
+ Object signalHandler = Proxy.newProxyInstance(getClass().getClassLoader(),
+ new Class<?>[]{signalHandlerClass}, new InvocationHandler() {
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ JLineTerminal.this.signal(Signal.WINCH);
+ return null;
+ }
+ }
+ );
+ // Register the signal handler, this code is equivalent to:
+ // Signal.handle(new Signal("CONT"), signalHandler);
+ signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), signalHandler);
+ } catch (Exception e) {
+ // Ignore this exception, if the above failed, the signal API is incompatible with what we're expecting
+
+ }
+ }
+
+ private void unregisterSignalHandler() {
+ try {
+ Class<?> signalClass = Class.forName("sun.misc.Signal");
+ Class<?> signalHandlerClass = Class.forName("sun.misc.SignalHandler");
+
+ Object signalHandler = signalHandlerClass.getField("SIG_DFL").get(null);
+ // Register the signal handler, this code is equivalent to:
+ // Signal.handle(new Signal("CONT"), signalHandler);
+ signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), signalHandler);
+ } catch (Exception e) {
+ // Ignore this exception, if the above failed, the signal API is incompatible with what we're expecting
+
+ }
+ }
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java
index 1c30dd0..e93d93e 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/LocalConsoleManager.java
@@ -67,7 +67,7 @@ public class LocalConsoleManager {
final Subject subject = createLocalKarafSubject();
- this.session = JaasHelper.<Session>doAs(subject, new PrivilegedAction<Session>() {
+ this.session = JaasHelper.doAs(subject, new PrivilegedAction<Session>() {
public Session run() {
String encoding = getEncoding();
session = sessionFactory.create(
@@ -95,8 +95,7 @@ public class LocalConsoleManager {
}
private String getEncoding() {
- String ctype = System.getenv("LC_CTYPE");
- String encoding = ctype;
+ String encoding = System.getenv("LC_CTYPE");
if (encoding != null && encoding.indexOf('.') > 0) {
encoding = encoding.substring(encoding.indexOf('.') + 1);
} else {
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/core/src/main/java/org/apache/karaf/shell/support/terminal/SignalSupport.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/support/terminal/SignalSupport.java b/shell/core/src/main/java/org/apache/karaf/shell/support/terminal/SignalSupport.java
new file mode 100644
index 0000000..39ff0f7
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/support/terminal/SignalSupport.java
@@ -0,0 +1,96 @@
+/*
+ * 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 org.apache.karaf.shell.support.terminal;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.apache.karaf.shell.api.console.Signal;
+import org.apache.karaf.shell.api.console.SignalListener;
+
+public class SignalSupport {
+
+ private final Map<Signal, Set<SignalListener>> listeners;
+
+ public SignalSupport() {
+ listeners = new ConcurrentHashMap<>(3);
+ }
+
+ public void addSignalListener(SignalListener listener, Signal... signals) {
+ if (signals == null) {
+ throw new IllegalArgumentException("signals may not be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ for (Signal s : signals) {
+ getSignalListeners(s, true).add(listener);
+ }
+ }
+
+ public void addSignalListener(SignalListener listener) {
+ addSignalListener(listener, EnumSet.allOf(Signal.class));
+ }
+
+ public void addSignalListener(SignalListener listener, EnumSet<Signal> signals) {
+ if (signals == null) {
+ throw new IllegalArgumentException("signals may not be null");
+ }
+ addSignalListener(listener, signals.toArray(new Signal[signals.size()]));
+ }
+
+ public void removeSignalListener(SignalListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ for (Signal s : EnumSet.allOf(Signal.class)) {
+ final Set<SignalListener> ls = getSignalListeners(s, false);
+ if (ls != null) {
+ ls.remove(listener);
+ }
+ }
+ }
+
+ public void signal(Signal signal) {
+ final Set<SignalListener> ls = getSignalListeners(signal, false);
+ if (ls != null) {
+ for (SignalListener l : ls) {
+ l.signal(signal);
+ }
+ }
+ }
+
+ protected Set<SignalListener> getSignalListeners(Signal signal, boolean create) {
+ Set<SignalListener> ls = listeners.get(signal);
+ if (ls == null && create) {
+ synchronized (listeners) {
+ ls = listeners.get(signal);
+ if (ls == null) {
+ ls = new CopyOnWriteArraySet<>();
+ listeners.put(signal, ls);
+ }
+ }
+ }
+ // may be null in case create=false
+ return ls;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java
----------------------------------------------------------------------
diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java
index fa24169..17291d0 100644
--- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java
+++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java
@@ -18,16 +18,23 @@
*/
package org.apache.karaf.shell.ssh;
+import org.apache.karaf.shell.api.console.Signal;
import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.support.terminal.SignalSupport;
import org.apache.sshd.server.Environment;
-public class SshTerminal implements Terminal {
+public class SshTerminal extends SignalSupport implements Terminal {
private Environment environment;
-
public SshTerminal(Environment environment) {
this.environment = environment;
+ this.environment.addSignalListener(new org.apache.sshd.server.SignalListener() {
+ @Override
+ public void signal(org.apache.sshd.server.Signal signal) {
+ SshTerminal.this.signal(Signal.WINCH);
+ }
+ }, org.apache.sshd.server.Signal.WINCH);
}
@Override
@@ -66,4 +73,5 @@ public class SshTerminal implements Terminal {
public void setEchoEnabled(boolean enabled) {
// TODO: how to disable echo over ssh ?
}
+
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/a5450aa2/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/WebTerminal.java
----------------------------------------------------------------------
diff --git a/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/WebTerminal.java b/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/WebTerminal.java
index 865a528..f0dd828 100644
--- a/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/WebTerminal.java
+++ b/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/WebTerminal.java
@@ -17,8 +17,9 @@
package org.apache.karaf.webconsole.gogo;
import org.apache.karaf.shell.api.console.Terminal;
+ import org.apache.karaf.shell.support.terminal.SignalSupport;
-public class WebTerminal implements Terminal {
+public class WebTerminal extends SignalSupport implements Terminal {
private int width;
private int height;
[2/2] git commit: [KARAF-2069] Provide a less pager command
Posted by gn...@apache.org.
[KARAF-2069] Provide a less pager command
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/57ec802b
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/57ec802b
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/57ec802b
Branch: refs/heads/master
Commit: 57ec802bcd36caec74d87165e2d700e118ee68d4
Parents: a5450aa
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Thu May 15 19:10:30 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Thu May 15 19:10:30 2014 +0200
----------------------------------------------------------------------
.../karaf/shell/commands/impl/LessAction.java | 918 +++++++++++++++++++
1 file changed, 918 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/karaf/blob/57ec802b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
----------------------------------------------------------------------
diff --git a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
new file mode 100644
index 0000000..4185c06
--- /dev/null
+++ b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LessAction.java
@@ -0,0 +1,918 @@
+/*
+ * 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 org.apache.karaf.shell.commands.impl;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+import jline.console.KeyMap;
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.Signal;
+import org.apache.karaf.shell.api.console.SignalListener;
+import org.apache.karaf.shell.api.console.Terminal;
+import org.jledit.jline.NonBlockingInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Command(scope = "shell", name = "less", description = "File pager.")
+@Service
+public class LessAction implements Action, SignalListener {
+
+ private static final int ESCAPE = 27;
+ public static final int ESCAPE_TIMEOUT = 100;
+ public static final int READ_EXPIRED = -2;
+
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Option(name = "-e", aliases = "--quit-at-eof")
+ boolean quitAtSecondEof;
+
+ @Option(name = "-E", aliases = "--QUIT-AT-EOF")
+ boolean quitAtFirstEof;
+
+ @Option(name = "-N", aliases = "--LINE-NUMBERS")
+ boolean printLineNumbers;
+
+ @Option(name = "-q", aliases = {"--quiet", "--silent"})
+ boolean quiet;
+
+ @Option(name = "-Q", aliases = {"--QUIET", "--SILENT"})
+ boolean veryQuiet;
+
+ @Option(name = "-S", aliases = "--chop-long-lines")
+ boolean chopLongLines;
+
+ @Option(name = "-i", aliases = "--ignore-case")
+ boolean ignoreCaseCond;
+
+ @Option(name = "-I", aliases = "--IGNORE-CASE")
+ boolean ignoreCaseAlways;
+
+ @Argument(multiValued = true)
+ List<File> files;
+
+ @Reference
+ Terminal terminal;
+
+ @Reference
+ Session session;
+
+ BufferedReader reader;
+
+ NonBlockingInputStream consoleInput;
+ Reader consoleReader;
+
+ KeyMap keys;
+
+ int firstLineInMemory = 0;
+ List<String> lines = new ArrayList<>();
+
+ int firstLineToDisplay = 0;
+ int firstColumnToDisplay = 0;
+ int offsetInLine = 0;
+
+ String message;
+ final StringBuilder buffer = new StringBuilder();
+ final StringBuilder opBuffer = new StringBuilder();
+ final Stack<Character> pushBackChar = new Stack<>();
+ Thread displayThread;
+ final AtomicBoolean redraw = new AtomicBoolean();
+
+ final Map<String, Operation> options = new TreeMap<>();
+
+ int window;
+ int halfWindow;
+
+ int nbEof;
+
+ String pattern;
+
+ @Override
+ public Object execute() throws Exception {
+ InputStream in;
+ if (files != null && !files.isEmpty()) {
+ message = files.get(0).toString();
+ in = new FileInputStream(files.get(0));
+ } else {
+ in = System.in;
+ }
+ reader = new BufferedReader(new InputStreamReader(new InterruptibleInputStream(in)));
+ try {
+ if (terminal == null || !isTty(System.out)) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ System.out.println(line);
+ checkInterrupted();
+ }
+ return null;
+ } else {
+ boolean echo = terminal.isEchoEnabled();
+ terminal.setEchoEnabled(false);
+ terminal.addSignalListener(this, Signal.WINCH);
+ try {
+ window = terminal.getHeight() - 1;
+ halfWindow = window / 2;
+ keys = new KeyMap("less", false);
+ bindKeys(keys);
+ consoleInput = new NonBlockingInputStream(session.getKeyboard(), true);
+ consoleReader = new InputStreamReader(consoleInput);
+
+ // Use alternate buffer
+ System.out.print("\u001B[?1049h");
+ System.out.flush();
+
+ displayThread = new Thread() {
+ @Override
+ public void run() {
+ redrawLoop();
+ }
+ };
+ displayThread.start();
+ redraw();
+ checkInterrupted();
+
+ options.put("-e", Operation.OPT_QUIT_AT_SECOND_EOF);
+ options.put("--quit-at-eof", Operation.OPT_QUIT_AT_SECOND_EOF);
+ options.put("-E", Operation.OPT_QUIT_AT_FIRST_EOF);
+ options.put("-QUIT-AT-EOF", Operation.OPT_QUIT_AT_FIRST_EOF);
+ options.put("-N", Operation.OPT_PRINT_LINES);
+ options.put("--LINE-NUMBERS", Operation.OPT_PRINT_LINES);
+ options.put("-q", Operation.OPT_QUIET);
+ options.put("--quiet", Operation.OPT_QUIET);
+ options.put("--silent", Operation.OPT_QUIET);
+ options.put("-Q", Operation.OPT_VERY_QUIET);
+ options.put("--QUIET", Operation.OPT_VERY_QUIET);
+ options.put("--SILENT", Operation.OPT_VERY_QUIET);
+ options.put("-S", Operation.OPT_CHOP_LONG_LINES);
+ options.put("--chop-long-lines", Operation.OPT_CHOP_LONG_LINES);
+ options.put("-i", Operation.OPT_IGNORE_CASE_COND);
+ options.put("--ignore-case", Operation.OPT_IGNORE_CASE_COND);
+ options.put("-I", Operation.OPT_IGNORE_CASE_ALWAYS);
+ options.put("--IGNORE-CASE", Operation.OPT_IGNORE_CASE_ALWAYS);
+
+ Operation op;
+ do {
+ checkInterrupted();
+
+ op = null;
+ //
+ // Option edition
+ //
+ if (buffer.length() > 0 && buffer.charAt(0) == '-') {
+ int c = consoleReader.read();
+ message = null;
+ if (buffer.length() == 1) {
+ buffer.append((char) c);
+ if (c != '-') {
+ op = options.get(buffer.toString());
+ if (op == null) {
+ message = "There is no " + printable(buffer.toString()) + " option";
+ buffer.setLength(0);
+ }
+ }
+ } else if (c == '\r') {
+ op = options.get(buffer.toString());
+ if (op == null) {
+ message = "There is no " + printable(buffer.toString()) + " option";
+ buffer.setLength(0);
+ }
+ } else {
+ buffer.append((char) c);
+ Map<String, Operation> matching = new HashMap<>();
+ for (Map.Entry<String, Operation> entry : options.entrySet()) {
+ if (entry.getKey().startsWith(buffer.toString())) {
+ matching.put(entry.getKey(), entry.getValue());
+ }
+ }
+ switch (matching.size()) {
+ case 0:
+ buffer.setLength(0);
+ break;
+ case 1:
+ buffer.setLength(0);
+ buffer.append(matching.keySet().iterator().next());
+ break;
+ }
+ }
+
+ }
+ //
+ // Pattern edition
+ //
+ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == '?')) {
+ int c = consoleReader.read();
+ message = null;
+ if (c == '\r') {
+ pattern = buffer.toString().substring(1);
+ if (buffer.charAt(0) == '/') {
+ moveToNextMatch();
+ } else {
+ moveToPreviousMatch();
+ }
+ buffer.setLength(0);
+ } else {
+ buffer.append((char) c);
+ }
+ }
+ //
+ // Command reading
+ //
+ else {
+ Object obj = readOperation();
+ message = null;
+ if (obj instanceof Character) {
+ char c = (char) obj;
+ // Enter option mode or pattern edit mode
+ if (c == '-' || c == '/' || c == '?') {
+ buffer.setLength(0);
+ }
+ buffer.append((char) obj);
+ } else if (obj instanceof Operation) {
+ op = (Operation) obj;
+ }
+ }
+ if (op != null) {
+ switch (op) {
+ case FORWARD_ONE_LINE:
+ moveForward(getStrictPositiveNumberInBuffer(1));
+ break;
+ case BACKWARD_ONE_LINE:
+ moveBackward(getStrictPositiveNumberInBuffer(1));
+ break;
+ case FORWARD_ONE_WINDOW_OR_LINES:
+ moveForward(getStrictPositiveNumberInBuffer(window));
+ break;
+ case FORWARD_ONE_WINDOW_AND_SET:
+ window = getStrictPositiveNumberInBuffer(window);
+ moveForward(window);
+ break;
+ case FORWARD_ONE_WINDOW_NO_STOP:
+ moveForward(window);
+ // TODO: handle no stop
+ break;
+ case FORWARD_HALF_WINDOW_AND_SET:
+ halfWindow = getStrictPositiveNumberInBuffer(halfWindow);
+ moveForward(halfWindow);
+ break;
+ case BACKWARD_ONE_WINDOW_AND_SET:
+ window = getStrictPositiveNumberInBuffer(window);
+ moveBackward(window);
+ break;
+ case BACKWARD_ONE_WINDOW_OR_LINES:
+ moveBackward(getStrictPositiveNumberInBuffer(window));
+ break;
+ case BACKWARD_HALF_WINDOW_AND_SET:
+ halfWindow = getStrictPositiveNumberInBuffer(halfWindow);
+ moveBackward(halfWindow);
+ break;
+ case GO_TO_FIRST_LINE_OR_N:
+ // TODO: handle number
+ firstLineToDisplay = firstLineInMemory;
+ offsetInLine = 0;
+ break;
+ case GO_TO_LAST_LINE_OR_N:
+ // TODO: handle number
+ moveForward(Integer.MAX_VALUE);
+ break;
+ case LEFT_ONE_HALF_SCREEN:
+ firstColumnToDisplay = Math.max(0, firstColumnToDisplay - terminal.getWidth() / 2);
+ break;
+ case RIGHT_ONE_HALF_SCREEN:
+ firstColumnToDisplay += terminal.getWidth() / 2;
+ break;
+ case REPEAT_SEARCH_BACKWARD:
+ case REPEAT_SEARCH_BACKWARD_SPAN_FILES:
+ moveToPreviousMatch();
+ break;
+ case REPEAT_SEARCH_FORWARD:
+ case REPEAT_SEARCH_FORWARD_SPAN_FILES:
+ moveToNextMatch();
+ break;
+ case UNDO_SEARCH:
+ pattern = null;
+ break;
+ case OPT_PRINT_LINES:
+ buffer.setLength(0);
+ printLineNumbers = !printLineNumbers;
+ message = printLineNumbers ? "Constantly display line numbers" : "Don't use line numbers";
+ break;
+ case OPT_QUIET:
+ buffer.setLength(0);
+ quiet = !quiet;
+ veryQuiet = false;
+ message = quiet ? "Ring the bell for errors but not at eof/bof" : "Ring the bell for errors AND at eof/bof";
+ break;
+ case OPT_VERY_QUIET:
+ buffer.setLength(0);
+ veryQuiet = !veryQuiet;
+ quiet = false;
+ message = veryQuiet ? "Never ring the bell" : "Ring the bell for errors AND at eof/bof";
+ break;
+ case OPT_CHOP_LONG_LINES:
+ buffer.setLength(0);
+ offsetInLine = 0;
+ chopLongLines = !chopLongLines;
+ message = chopLongLines ? "Chop long lines" : "Fold long lines";
+ break;
+ case OPT_IGNORE_CASE_COND:
+ ignoreCaseCond = !ignoreCaseCond;
+ ignoreCaseAlways = false;
+ message = ignoreCaseCond ? "Ignore case in searches" : "Case is significant in searches";
+ break;
+ case OPT_IGNORE_CASE_ALWAYS:
+ ignoreCaseAlways = !ignoreCaseAlways;
+ ignoreCaseCond = false;
+ message = ignoreCaseAlways ? "Ignore case in searches and in patterns" : "Case is significant in searches";
+ break;
+ }
+ buffer.setLength(0);
+ }
+ redraw();
+ if (quitAtFirstEof && nbEof > 0 || quitAtSecondEof && nbEof > 1) {
+ op = Operation.EXIT;
+ }
+ } while (op != Operation.EXIT);
+ } catch (InterruptedException ie) {
+ log.debug("Interrupted by user");
+ } finally {
+ terminal.setEchoEnabled(echo);
+ terminal.removeSignalListener(this);
+ consoleInput.shutdown();
+ displayThread.interrupt();
+ displayThread.join();
+ // Use main buffer
+ System.out.print("\u001B[?1049l");
+ // Clear line
+ System.out.println();
+ System.out.flush();
+ }
+ }
+ } finally {
+ reader.close();
+ }
+ return null;
+ }
+
+ private void moveToNextMatch() throws IOException {
+ Pattern compiled = getPattern();
+ if (compiled != null) {
+ for (int lineNumber = firstLineToDisplay + 1; ; lineNumber++) {
+ String line = getLine(lineNumber);
+ if (line == null) {
+ break;
+ } else if (compiled.matcher(line).find()) {
+ firstLineToDisplay = lineNumber;
+ offsetInLine = 0;
+ return;
+ }
+ }
+ }
+ message = "Pattern not found";
+ }
+
+ private void moveToPreviousMatch() throws IOException {
+ Pattern compiled = getPattern();
+ if (compiled != null) {
+ for (int lineNumber = firstLineToDisplay - 1; lineNumber >= firstLineInMemory; lineNumber--) {
+ String line = getLine(lineNumber);
+ if (line == null) {
+ break;
+ } else if (compiled.matcher(line).find()) {
+ firstLineToDisplay = lineNumber;
+ offsetInLine = 0;
+ return;
+ }
+ }
+ }
+ message = "Pattern not found";
+ }
+
+ private String printable(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == ESCAPE) {
+ sb.append("ESC");
+ } else if (c < 32) {
+ sb.append('^').append((char) (c + '@'));
+ } else if (c < 128) {
+ sb.append(c);
+ } else {
+ sb.append('\\').append(String.format("03o"));
+ }
+ }
+ return sb.toString();
+ }
+
+ void moveForward(int lines) throws IOException {
+ int width = terminal.getWidth() - (printLineNumbers ? 8 : 0);
+ int height = terminal.getHeight();
+ while (--lines >= 0) {
+
+ int lastLineToDisplay = firstLineToDisplay;
+ if (firstColumnToDisplay > 0 || chopLongLines) {
+ lastLineToDisplay += height - 1;
+ } else {
+ int off = offsetInLine;
+ for (int l = 0; l < height - 1; l++) {
+ String line = getLine(lastLineToDisplay);
+ if (line.length() > off + width) {
+ off += width;
+ } else {
+ off = 0;
+ lastLineToDisplay++;
+ }
+ }
+ }
+ if (getLine(lastLineToDisplay) == null) {
+ eof();
+ return;
+ }
+
+ String line = getLine(firstLineToDisplay);
+ if (line.length() > width + offsetInLine) {
+ offsetInLine += width;
+ } else {
+ offsetInLine = 0;
+ firstLineToDisplay++;
+ }
+ }
+ }
+
+ void moveBackward(int lines) throws IOException {
+ int width = terminal.getWidth() - (printLineNumbers ? 8 : 0);
+ while (--lines >= 0) {
+ if (offsetInLine > 0) {
+ offsetInLine = Math.max(0, offsetInLine - width);
+ } else if (firstLineInMemory < firstLineToDisplay) {
+ firstLineToDisplay--;
+ String line = getLine(firstLineToDisplay);
+ offsetInLine = line.length() - line.length() % width;
+ } else {
+ bof();
+ return;
+ }
+ }
+ }
+
+ private void eof() {
+ nbEof++;
+ message = "(END)";
+ if (!quiet && !veryQuiet && !quitAtFirstEof && !quitAtSecondEof) {
+ System.out.print((char) 0x07);
+ System.out.flush();
+ }
+ }
+
+ private void bof() {
+ if (!quiet && !veryQuiet) {
+ System.out.print((char) 0x07);
+ System.out.flush();
+ }
+ }
+
+ int getStrictPositiveNumberInBuffer(int def) {
+ try {
+ int n = Integer.parseInt(buffer.toString());
+ return (n > 0) ? n : def;
+ } catch (NumberFormatException e) {
+ return def;
+ } finally {
+ buffer.setLength(0);
+ }
+ }
+
+ void redraw() {
+ synchronized (redraw) {
+ redraw.set(true);
+ redraw.notifyAll();
+ }
+ }
+
+ void redrawLoop() {
+ synchronized (redraw) {
+ for (; ; ) {
+ try {
+ if (redraw.compareAndSet(true, false)) {
+ display();
+ } else {
+ redraw.wait();
+ }
+ } catch (Exception e) {
+ return;
+ }
+ }
+ }
+ }
+
+ void display() throws IOException {
+ System.out.println();
+ int width = terminal.getWidth() - (printLineNumbers ? 8 : 0);
+ int height = terminal.getHeight();
+ int inputLine = firstLineToDisplay;
+ String curLine = null;
+ Pattern compiled = getPattern();
+ for (int terminalLine = 0; terminalLine < height - 1; terminalLine++) {
+ if (curLine == null) {
+ curLine = getLine(inputLine++);
+ if (curLine == null) {
+ curLine = "";
+ }
+ if (compiled != null) {
+ curLine = compiled.matcher(curLine).replaceAll("\033[7m$1\033[0m");
+ }
+ }
+ String toDisplay;
+ if (firstColumnToDisplay > 0 || chopLongLines) {
+ int off = firstColumnToDisplay;
+ if (terminalLine == 0 && offsetInLine > 0) {
+ off = Math.max(offsetInLine, off);
+ }
+ toDisplay = ansiSubstring(curLine, off, off + width);
+ curLine = null;
+ } else {
+ if (terminalLine == 0 && offsetInLine > 0) {
+ curLine = ansiSubstring(curLine, offsetInLine, Integer.MAX_VALUE);
+ }
+ toDisplay = ansiSubstring(curLine, 0, width);
+ curLine = ansiSubstring(curLine, width, Integer.MAX_VALUE);
+ if (curLine.isEmpty()) {
+ curLine = null;
+ }
+ }
+ if (printLineNumbers) {
+ System.out.print(String.format("%7d ", inputLine));
+ }
+ System.out.println(toDisplay);
+ }
+ System.out.flush();
+ if (message != null) {
+ System.out.print("\033[7m" + message + " \033[0m");
+ } else if (buffer.length() > 0) {
+ System.out.print(" " + buffer);
+ } else if (opBuffer.length() > 0) {
+ System.out.print(" " + printable(opBuffer.toString()));
+ } else {
+ System.out.print(":");
+ }
+ System.out.flush();
+ }
+
+ private Pattern getPattern() {
+ Pattern compiled = null;
+ if (pattern != null) {
+ boolean insensitive = ignoreCaseAlways || ignoreCaseCond && pattern.toLowerCase().equals(pattern);
+ compiled = Pattern.compile("(" + pattern + ")", insensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : 0);
+ }
+ return compiled;
+ }
+
+ private String ansiSubstring(String curLine, int begin, int end) {
+ int printPos = 0;
+ int csi = 0;
+ char sgr = '0';
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < curLine.length() && printPos < end; i++) {
+ char c = curLine.charAt(i);
+ if (csi == 0 && c == '\033') {
+ csi++;
+ } else if (csi == 1 && c == '[') {
+ csi++;
+ } else if (csi == 2) {
+ sgr = c;
+ csi++;
+ } else if (csi == 3 && c == 'm') {
+ csi = 0;
+ if (printPos >= begin) {
+ sb.append("\033[").append(sgr).append("m");
+ }
+ } else {
+ if (printPos == begin && sgr != '0') {
+ sb.append("\033[7m");
+ }
+ if (printPos >= begin && printPos < end) {
+ sb.append(c);
+ }
+ ++printPos;
+ }
+ }
+ if (sgr != '0') {
+ sb.append("\033[0m");
+ }
+ return sb.toString();
+ }
+
+ String getLine(int line) throws IOException {
+ while (line <= lines.size()) {
+ String str = reader.readLine();
+ if (str != null) {
+ lines.add(str);
+ } else {
+ break;
+ }
+ }
+ if (line < lines.size()) {
+ return lines.get(line);
+ }
+ return null;
+ }
+
+ @Override
+ public void signal(Signal signal) {
+ // Ugly hack to force the jline unix terminal to retrieve the width/height of the terminal
+ // because results are cached for 1 second.
+ try {
+ Field field = terminal.getClass().getDeclaredField("terminal");
+ field.setAccessible(true);
+ Object jlineTerminal = field.get(terminal);
+ field = jlineTerminal.getClass().getSuperclass().getDeclaredField("settings");
+ field.setAccessible(true);
+ Object settings = field.get(jlineTerminal);
+ field = settings.getClass().getDeclaredField("configLastFetched");
+ field.setAccessible(true);
+ field.setLong(settings, 0L);
+ } catch (Throwable t) {
+ // Ignore
+ }
+ redraw();
+ }
+
+ protected boolean isTty(OutputStream out) {
+ try {
+ Method mth = out.getClass().getDeclaredMethod("getCurrent");
+ mth.setAccessible(true);
+ Object current = mth.invoke(out);
+ return current == session.getConsole();
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ /**
+ * This is for long running commands to be interrupted by ctrl-c
+ *
+ * @throws InterruptedException
+ */
+ public static void checkInterrupted() throws InterruptedException {
+ Thread.yield();
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException();
+ }
+ }
+
+
+ protected Object readOperation() throws IOException {
+ int c = pushBackChar.isEmpty() ? consoleReader.read() : pushBackChar.pop();
+ if (c == -1) {
+ return null;
+ }
+ opBuffer.append((char) c);
+
+ Object o = keys.getBound(opBuffer);
+ if (o == jline.console.Operation.DO_LOWERCASE_VERSION) {
+ opBuffer.setLength(opBuffer.length() - 1);
+ opBuffer.append(Character.toLowerCase((char) c));
+ o = keys.getBound(opBuffer);
+ }
+
+ if (o instanceof KeyMap) {
+ if (c == ESCAPE
+ && pushBackChar.isEmpty()
+ && consoleInput.isNonBlockingEnabled()
+ && consoleInput.peek(ESCAPE_TIMEOUT) == READ_EXPIRED) {
+ o = ((KeyMap) o).getAnotherKey();
+ if (o == null || o instanceof KeyMap) {
+ return null;
+ }
+ opBuffer.setLength(0);
+ } else {
+ return null;
+ }
+ }
+
+ while (o == null && opBuffer.length() > 0) {
+ c = opBuffer.charAt(opBuffer.length() - 1);
+ opBuffer.setLength(opBuffer.length() - 1);
+ Object o2 = keys.getBound(opBuffer);
+ if (o2 instanceof KeyMap) {
+ o = ((KeyMap) o2).getAnotherKey();
+ if (o != null) {
+ pushBackChar.push((char) c);
+ }
+ }
+ }
+
+ if (o != null) {
+ opBuffer.setLength(0);
+ pushBackChar.clear();
+ }
+ return o;
+ }
+
+
+ private void bindKeys(KeyMap map) {
+ // Arrow keys bindings
+ map.bind("\033[0A", Operation.BACKWARD_ONE_LINE);
+ map.bind("\033[0B", Operation.LEFT_ONE_HALF_SCREEN);
+ map.bind("\033[0C", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\033[0D", Operation.FORWARD_ONE_LINE);
+
+ map.bind("\340\110", Operation.BACKWARD_ONE_LINE);
+ map.bind("\340\113", Operation.LEFT_ONE_HALF_SCREEN);
+ map.bind("\340\115", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\340\120", Operation.FORWARD_ONE_LINE);
+ map.bind("\000\110", Operation.BACKWARD_ONE_LINE);
+ map.bind("\000\113", Operation.LEFT_ONE_HALF_SCREEN);
+ map.bind("\000\115", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\000\120", Operation.FORWARD_ONE_LINE);
+
+ map.bind("\033[A", Operation.BACKWARD_ONE_LINE);
+ map.bind("\033[B", Operation.FORWARD_ONE_LINE);
+ map.bind("\033[C", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\033[D", Operation.LEFT_ONE_HALF_SCREEN);
+
+ map.bind("\033[OA", Operation.BACKWARD_ONE_LINE);
+ map.bind("\033[OB", Operation.FORWARD_ONE_LINE);
+ map.bind("\033[OC", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\033[OD", Operation.LEFT_ONE_HALF_SCREEN);
+
+ map.bind("\0340H", Operation.BACKWARD_ONE_LINE);
+ map.bind("\0340P", Operation.FORWARD_ONE_LINE);
+ map.bind("\0340M", Operation.RIGHT_ONE_HALF_SCREEN);
+ map.bind("\0340K", Operation.LEFT_ONE_HALF_SCREEN);
+
+ map.bind("h", Operation.HELP);
+ map.bind("H", Operation.HELP);
+
+ map.bind("q", Operation.EXIT);
+ map.bind(":q", Operation.EXIT);
+ map.bind("Q", Operation.EXIT);
+ map.bind(":Q", Operation.EXIT);
+ map.bind("ZZ", Operation.EXIT);
+
+ map.bind("e", Operation.FORWARD_ONE_LINE);
+ map.bind(ctrl('E'), Operation.FORWARD_ONE_LINE);
+ map.bind("j", Operation.FORWARD_ONE_LINE);
+ map.bind(ctrl('N'), Operation.FORWARD_ONE_LINE);
+ map.bind("\r", Operation.FORWARD_ONE_LINE);
+
+ map.bind("y", Operation.BACKWARD_ONE_LINE);
+ map.bind(ctrl('Y'), Operation.BACKWARD_ONE_LINE);
+ map.bind("k", Operation.BACKWARD_ONE_LINE);
+ map.bind(ctrl('K'), Operation.BACKWARD_ONE_LINE);
+ map.bind(ctrl('P'), Operation.BACKWARD_ONE_LINE);
+
+ map.bind("f", Operation.FORWARD_ONE_WINDOW_OR_LINES);
+ map.bind(ctrl('F'), Operation.FORWARD_ONE_WINDOW_OR_LINES);
+ map.bind(ctrl('V'), Operation.FORWARD_ONE_WINDOW_OR_LINES);
+ map.bind(" ", Operation.FORWARD_ONE_WINDOW_OR_LINES);
+
+ map.bind("b", Operation.BACKWARD_ONE_WINDOW_OR_LINES);
+ map.bind(ctrl('B'), Operation.BACKWARD_ONE_WINDOW_OR_LINES);
+ map.bind("\033v", Operation.BACKWARD_ONE_WINDOW_OR_LINES);
+
+ map.bind("z", Operation.FORWARD_ONE_WINDOW_AND_SET);
+
+ map.bind("w", Operation.BACKWARD_ONE_WINDOW_AND_SET);
+
+ map.bind("\033 ", Operation.FORWARD_ONE_WINDOW_NO_STOP);
+
+ map.bind("d", Operation.FORWARD_HALF_WINDOW_AND_SET);
+ map.bind(ctrl('D'), Operation.FORWARD_HALF_WINDOW_AND_SET);
+
+ map.bind("u", Operation.BACKWARD_HALF_WINDOW_AND_SET);
+ map.bind(ctrl('U'), Operation.BACKWARD_HALF_WINDOW_AND_SET);
+
+ map.bind("\033)", Operation.RIGHT_ONE_HALF_SCREEN);
+
+ map.bind("\033(", Operation.LEFT_ONE_HALF_SCREEN);
+
+ map.bind("F", Operation.FORWARD_FOREVER);
+
+ map.bind("n", Operation.REPEAT_SEARCH_FORWARD);
+ map.bind("N", Operation.REPEAT_SEARCH_BACKWARD);
+ map.bind("\033n", Operation.REPEAT_SEARCH_FORWARD_SPAN_FILES);
+ map.bind("\033N", Operation.REPEAT_SEARCH_BACKWARD_SPAN_FILES);
+ map.bind("\033u", Operation.UNDO_SEARCH);
+
+ map.bind("g", Operation.GO_TO_FIRST_LINE_OR_N);
+ map.bind("<", Operation.GO_TO_FIRST_LINE_OR_N);
+ map.bind("\033<", Operation.GO_TO_FIRST_LINE_OR_N);
+
+ map.bind("G", Operation.GO_TO_LAST_LINE_OR_N);
+ map.bind(">", Operation.GO_TO_LAST_LINE_OR_N);
+ map.bind("\033>", Operation.GO_TO_LAST_LINE_OR_N);
+
+ for (char c : "-/0123456789?".toCharArray()) {
+ map.bind("" + c, c);
+ }
+ }
+
+ String ctrl(char c) {
+ return "" + ((char) (c & 0x1f));
+ }
+
+ static enum Operation {
+
+ // General
+ HELP,
+ EXIT,
+
+ // Moving
+ FORWARD_ONE_LINE,
+ BACKWARD_ONE_LINE,
+ FORWARD_ONE_WINDOW_OR_LINES,
+ BACKWARD_ONE_WINDOW_OR_LINES,
+ FORWARD_ONE_WINDOW_AND_SET,
+ BACKWARD_ONE_WINDOW_AND_SET,
+ FORWARD_ONE_WINDOW_NO_STOP,
+ FORWARD_HALF_WINDOW_AND_SET,
+ BACKWARD_HALF_WINDOW_AND_SET,
+ LEFT_ONE_HALF_SCREEN,
+ RIGHT_ONE_HALF_SCREEN,
+ FORWARD_FOREVER,
+ REPAINT,
+ REPAINT_AND_DISCARD,
+
+ // Searching
+ REPEAT_SEARCH_FORWARD,
+ REPEAT_SEARCH_BACKWARD,
+ REPEAT_SEARCH_FORWARD_SPAN_FILES,
+ REPEAT_SEARCH_BACKWARD_SPAN_FILES,
+ UNDO_SEARCH,
+
+ // Jumping
+ GO_TO_FIRST_LINE_OR_N,
+ GO_TO_LAST_LINE_OR_N,
+ GO_TO_PERCENT_OR_N,
+ GO_TO_NEXT_TAG,
+ GO_TO_PREVIOUS_TAG,
+ FIND_CLOSE_BRACKET,
+ FIND_OPEN_BRACKET,
+
+ // Options
+ OPT_PRINT_LINES,
+ OPT_CHOP_LONG_LINES,
+ OPT_QUIT_AT_FIRST_EOF,
+ OPT_QUIT_AT_SECOND_EOF,
+ OPT_QUIET,
+ OPT_VERY_QUIET,
+ OPT_IGNORE_CASE_COND,
+ OPT_IGNORE_CASE_ALWAYS,
+
+ }
+
+ static class InterruptibleInputStream extends FilterInputStream {
+ InterruptibleInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException();
+ }
+ return super.read(b, off, len);
+ }
+ }
+
+}