
org.aesh.readline.PagingSupport Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source
* Copyright 2018 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed 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.aesh.readline;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.function.Consumer;
import org.aesh.readline.action.ActionDecoder;
import org.aesh.readline.history.History;
import org.aesh.readline.history.InMemoryHistory;
import org.aesh.readline.terminal.Key;
import org.aesh.readline.terminal.impl.WinSysTerminal;
import org.aesh.readline.util.Parser;
import org.aesh.terminal.Attributes;
import org.aesh.terminal.Connection;
import org.aesh.terminal.tty.Signal;
import org.aesh.terminal.tty.Size;
import org.aesh.terminal.utils.ANSI;
import org.aesh.terminal.utils.Config;
/**
*
* @author [email protected]
*/
public class PagingSupport {
private class Paging {
private boolean notFound;
private boolean searchingMode;
private int currentLine;
private int allLines;
private int lastScrolledLines;
private List lines;
private final String[] splitLines;
private int jumpIndex = -1;
private String pattern;
private int max;
private boolean paging;
// Starting Windows 10, alternate buffer is supported.
private final boolean alternateSupported;
Paging(String output, Size termSize) {
// '\R' will match any line break.
// -1 to keep empty lines at the end of content.
splitLines = output.split("\\R", -1);
lines = buildLines(termSize);
lastScrolledLines = lines.size();
max = termSize.getHeight() - 1;
if (Config.isWindows()) {
// forcePaging is used by tests.
alternateSupported = WinSysTerminal.isVTSupported();
} else {
alternateSupported = true;
}
if (lines.size() > max) {
if (alternateSupported) {
getConnection().write(ANSI.ALTERNATE_BUFFER);
clearScreen();
}
paging = true;
}
}
private List buildLines(Size size) {
List lst = new ArrayList<>();
int width = Config.isWindows() ? size.getWidth() - 1 : size.getWidth();
for (String l : splitLines) {
String remaining = l;
do {
String st = remaining.substring(0, Math.min(remaining.length(), width));
lst.add(st);
remaining = remaining.substring(Math.min(remaining.length(), width));
} while (!remaining.isEmpty());
}
return lst;
}
int getMax() {
return max;
}
void pagingDone() {
if (paging && alternateSupported) {
//Print the output to main buffer (from start until the last scrolled position)
getConnection().write(ANSI.MAIN_BUFFER);
printScrolledLines();
}
}
boolean needPrompt() {
return (currentLine > getMax() - 1 && jumpIndex == -1) || (endBuffer() && searchingMode);
}
boolean inWorkflow() {
return allLines < lines.size() || searchingMode;
}
void exit() {
lastScrolledLines = allLines;
allLines = lines.size();
searchingMode = false;
}
void pageDown() {
notFound = false;
currentLine = 0;
// Exit the workflow.
if (endBuffer()) {
exit();
}
}
void pageUp() {
if (!alternateSupported) {
return;
}
clearScreen();
notFound = false;
currentLine = 0;
if (allLines > 2 * getMax()) {
//Move one screen up
allLines -= 2 * getMax();
} else {
//Move to the start of input
allLines = 0;
}
}
void previousMatch() {
if (!alternateSupported) {
return;
}
if (!searchingMode && searchHistory.size() != 0) {
int[] p = searchHistory.get(searchHistory.size() - 1);
pattern = Parser.fromCodePoints(p);
searchingMode = true;
}
if (searchingMode) {
if (allLines <= getMax()) {
notFound = true;
}
int previous = previousMatch(pattern, lines, allLines - getMax() - 1);
if (previous >= 0) {
jumpIndex = allLines - previous - 1;
notFound = false;
resetScreen();
} else {
notFound = true;
}
}
}
private int previousMatch(String pattern, List lines, int currentLine) {
int previous = 0;
for (int i = currentLine; i >= 0; i--) {
String l = lines.get(i);
if (l.contains(pattern)) {
return previous;
}
previous += 1;
}
return -1;
}
private void nextMatch() {
if (!alternateSupported) {
return;
}
if (searchingMode) {
if (endBuffer()) {
notFound = true;
} else {
int start = allLines - getMax() < 0 ? 0 : allLines - getMax();
int next = nextMatch(pattern, lines, start + 1);
if (next >= 0) {
// We need to redraw everything from start in case
// some matches are already displayed and need highlighting
jumpIndex = Math.min(allLines + next + 1, lines.size());
notFound = false;
resetScreen();
} else {
notFound = true;
}
}
} else if (searchHistory.size() != 0) {
int[] p = searchHistory.get(searchHistory.size() - 1);
doSearch(Parser.fromCodePoints(p));
}
}
private int nextMatch(String pattern, List lines, int currentLine) {
int next = 0;
for (int i = currentLine; i < lines.size(); i++) {
String l = lines.get(i);
if (l.contains(pattern)) {
return next;
}
next += 1;
}
return -1;
}
private void search() throws InterruptedException, IOException {
if (!alternateSupported) {
return;
}
doSearch(readPattern());
}
private void doSearch(String pattern) {
// The complete buffer needs to be redrawn to clear the pattern prompt
// and to highlight possible matches already displayed.
if (pattern == null || pattern.isEmpty()) {
// needed to redraw in order to clear pattern prompt.
jumpIndex = allLines;
} else {
this.pattern = pattern;
int start = allLines - getMax() < 0 ? 0 : allLines - getMax();
int next = nextMatch(pattern, lines, start);
if (next >= 0) {
jumpIndex = Math.min(allLines + next, lines.size());
searchingMode = true;
notFound = false;
} else {
notFound = true;
// do we have something from the beginning
int n = nextMatch(pattern, lines, 0);
if (n >= 0) {
searchingMode = true;
}
// needed to redraw in order to clear pattern prompt.
jumpIndex = allLines;
}
}
resetScreen();
}
private void lineUp() {
if (!alternateSupported) {
return;
}
notFound = false;
if (allLines > getMax()) {
currentLine = 0;
//Move one line up
allLines -= getMax() + 1;
clearScreen();
}
}
private void lineDown() {
notFound = false;
// Exit the workflow
if (endBuffer()) {
exit();
}
currentLine -= 1;
}
private int getPercentage() {
return (allLines * 100) / lines.size();
}
private String nextCurrentLine() {
String line = lines.get(allLines);
currentLine += 1;
allLines += 1;
if (jumpIndex == allLines) {
currentLine = getMax();
jumpIndex = -1;
}
return line;
}
private boolean endBuffer() {
return allLines == lines.size();
}
private void redraw(Size size) {
if (!alternateSupported) {
return;
}
int oldMax = max;
max = size.getHeight() - 1;
lines = buildLines(size);
if (lines.size() > max) {
jumpIndex = allLines + (max - oldMax);
} else {
jumpIndex = -1;
}
lastScrolledLines = lines.size();
resetScreen();
while (inWorkflow() && !needPrompt()) {
printCurrentLine();
}
// Redraw the prompt in all cases, even if resizing expose everything.
drawPrompt();
}
private void printCurrentLine() {
String l = nextCurrentLine();
if (searchingMode) {
displayHightlighted(pattern, l);
} else {
getConnection().write(l + Config.getLineSeparator());
}
}
private void printScrolledLines() {
//Print the output to main buffer (from start until the last scrolled position)
for (int i = 0; i < lastScrolledLines; i++) {
String l = lines.get(i);
getConnection().write(l + Config.getLineSeparator());
}
}
private void drawPrompt() {
if (notFound) {
getConnection().write(ANSI.INVERT_BACKGROUND);
getConnection().write("Pattern not found");
getConnection().write(ANSI.RESET);
} else {
getConnection().write("--More(" + getPercentage() + "%)--");
}
}
private void goHome() {
if (!alternateSupported) {
return;
}
notFound = false;
resetScreen();
}
private void goEnd() {
notFound = false;
// Jump to the size - 1 line to not exit
// the paging.
if (allLines < lines.size() - 1) {
jumpIndex = lines.size() - 1;
}
}
private void resetScreen() {
currentLine = 0;
allLines = 0;
clearScreen();
}
}
private static final EnumMap RED_PATTERN_READLINE_FLAGS
= new EnumMap<>(ReadlineFlag.class);
static {
RED_PATTERN_READLINE_FLAGS.put(ReadlineFlag.NO_PROMPT_REDRAW_ON_INTR,
Integer.MAX_VALUE);
}
private final History searchHistory = new InMemoryHistory();
private Paging paging;
private final Connection connection;
private final Readline readline;
private StringBuilder outputCollector;
private final boolean search;
public PagingSupport(Connection connection, boolean search) {
this(connection, null, search);
}
@Deprecated
public PagingSupport(Connection connection, Readline readline, boolean search) {
this.connection = connection;
this.readline = readline;
this.search = search;
Consumer consumer = connection.getSizeHandler();
connection.setSizeHandler(new Consumer() {
@Override
public void accept(Size t) {
if (consumer != null) {
consumer.accept(t);
}
if (paging != null) {
paging.redraw(t);
}
}
});
}
public boolean isPagingOutputActive() {
return paging != null && paging.paging;
}
public void reset() {
outputCollector = null;
}
public void addContent(String content) {
if (outputCollector == null) {
outputCollector = new StringBuilder();
}
outputCollector.append(content);
}
private Connection getConnection() {
return connection;
}
private Readline getReadline() {
return readline == null ? new Readline() : readline;
}
private StringBuilder getOutputCollector() {
return outputCollector;
}
private void clearScreen() {
if (getConnection() != null) {
getConnection().stdoutHandler().accept(ANSI.CLEAR_SCREEN);
}
}
private void displayHightlighted(String pattern, String l) {
int index = l.indexOf(pattern);
while (index >= 0) {
getConnection().write(l.substring(0, index));
getConnection().write(ANSI.INVERT_BACKGROUND);
getConnection().write(pattern);
getConnection().write(ANSI.RESET);
l = l.substring(index + pattern.length());
index = l.indexOf(pattern);
}
getConnection().write(l + Config.getLineSeparator());
}
public void printCollectedOutput() {
if (getOutputCollector() == null || getOutputCollector().length() == 0) {
return;
}
String line = getOutputCollector().toString();
if (line.isEmpty()) {
return;
}
if (search) {
printAndSearchCollectedOuput(line);
} else {
printCollectedOutput(line);
}
}
private void printCollectedOutput(String line) {
try {
// '\R' will match any line break.
// -1 to keep empty lines at the end of content.
String[] lines = line.split("\\R", -1);
int max = connection.size().getHeight();
int currentLines = 0;
int allLines = 0;
while (allLines < lines.length) {
if (currentLines > max - 2) {
try {
connection.write(ANSI.CURSOR_SAVE);
int percentage = (allLines * 100) / lines.length;
connection.write("--More(" + percentage + "%)--");
Key k = read();
connection.write(ANSI.CURSOR_RESTORE);
connection.stdoutHandler().accept(ANSI.ERASE_LINE_FROM_CURSOR);
if (k == null) { // interrupted, exit.
allLines = lines.length;
} else {
switch (k) {
case SPACE: {
currentLines = 0;
break;
}
case ENTER:
case CTRL_M: { // On Mac, CTRL_M...
currentLines -= 1;
break;
}
case q: {
allLines = lines.length;
break;
}
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
} else {
String l = lines[allLines];
currentLines += 1;
allLines += 1;
// Do not add an extra \n
// The \n has been added by the previous line.
if (allLines == lines.length) {
if (l.isEmpty()) {
continue;
}
}
connection.write(l + Config.getLineSeparator());
}
}
} finally {
outputCollector = null;
}
}
private void printAndSearchCollectedOuput(String line) {
try {
paging = new Paging(line, getConnection().size());
while (paging.inWorkflow()) {
if (paging.needPrompt()) {
try {
getConnection().write(ANSI.CURSOR_SAVE);
paging.drawPrompt();
Key k = read();
getConnection().write(ANSI.CURSOR_RESTORE);
getConnection().stdoutHandler().accept(ANSI.ERASE_LINE_FROM_CURSOR);
if (k == null) { // interrupted, exit.
paging.exit();
} else {
switch (k) {
case SPACE:
case PGDOWN_2:
case PGDOWN: {
paging.pageDown();
break;
}
case BACKSLASH:
case PGUP_2:
case PGUP: {
paging.pageUp();
break;
}
case N: {
paging.previousMatch();
break;
}
case n: {
paging.nextMatch();
break;
}
case SLASH: {
paging.search();
break;
}
case SEMI_COLON:
case UP_2:
case UP: {
paging.lineUp();
break;
}
case DOWN:
case DOWN_2:
case ENTER:
case CTRL_M: { // On Mac, CTRL_M...
paging.lineDown();
break;
}
case HOME:
case HOME_2:
case g: {
paging.goHome();
break;
}
case END:
case END_2:
case END_3:
case G: {
paging.goEnd();
break;
}
case Q:
case ESC:
case q: {
paging.exit();
break;
}
}
}
} catch (InterruptedException ex) {
getConnection().write(ANSI.CURSOR_RESTORE);
getConnection().stdoutHandler().accept(ANSI.ERASE_LINE_FROM_CURSOR);
paging.exit();
//throw new RuntimeException(ex);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
} else {
paging.printCurrentLine();
}
}
} finally {
paging.pagingDone();
paging = null;
outputCollector = null;
}
}
private Key read() throws InterruptedException {
ActionDecoder decoder = new ActionDecoder();
final Key[] key = {null};
CountDownLatch latch = new CountDownLatch(1);
Attributes attributes = getConnection().enterRawMode();
// We need to set the interrupt SignalHandler to interrupt the reading.
Consumer prevHandler = getConnection().getSignalHandler();
getConnection().setSignalHandler((signal) -> {
// Interrupting the current reading thread.
switch (signal) {
case INT: {
latch.countDown();
}
}
});
try {
getConnection().setStdinHandler(keys -> {
decoder.add(keys);
if (decoder.hasNext()) {
key[0] = Key.findStartKey(decoder.next().buffer().array());
latch.countDown();
}
});
try {
// Wait until interrupted
latch.await();
} finally {
getConnection().setStdinHandler(null);
}
} finally {
getConnection().setAttributes(attributes);
getConnection().setSignalHandler(prevHandler);
}
return key[0];
}
private String readPattern() throws InterruptedException, IOException {
CountDownLatch latch = new CountDownLatch(1);
String[] out = new String[1];
// We need to set the interrupt SignalHandler to be aware of the interrupt
Consumer prevHandler = getConnection().getSignalHandler();
getConnection().setSignalHandler((signal) -> {
prevHandler.accept(signal);
switch (signal) {
case INT: {
latch.countDown();
}
}
});
getReadline().readline(getConnection(), new Prompt("/", (Character) null), newLine -> {
out[0] = newLine;
latch.countDown();
}, null, null, searchHistory, null, RED_PATTERN_READLINE_FLAGS);
try {
latch.await();
} finally {
getConnection().setSignalHandler(prevHandler);
}
// At this point, if the current thread has been interrupted
// then an exception has been thrown and we are not reaching this point.
// Throws interruptedException and not take into account the returned null
// value.
if (out[0] == null) {
throw new InterruptedException();
}
return out[0];
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy