blob: 7cc3bb7eb8269fcace920cb2440324e80257f8ff [file] [log] [blame]
/*
* 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.flink.table.client.cli;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.table.client.gateway.SqlExecutionException;
import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.terminal.Attributes;
import org.jline.terminal.Attributes.LocalFlag;
import org.jline.terminal.Terminal;
import org.jline.terminal.Terminal.Signal;
import org.jline.terminal.Terminal.SignalHandler;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.InfoCmp.Capability;
import java.io.IOError;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.apache.flink.table.client.cli.CliUtils.repeatChar;
/**
* Framework for a CLI view with header, footer, and main part that is scrollable.
*
* @param <OP> supported list of operations
*/
public abstract class CliView<OP extends Enum<OP>, OUT> {
protected final CliClient client;
protected int offsetX;
protected int offsetY;
private volatile boolean isRunning;
private Thread inputThread;
private int width;
private int height;
private final BindingReader keyReader;
private AttributedString titleLine;
private List<AttributedString> headerLines;
private List<AttributedString> mainHeaderLines; // vertically scrollable
private List<AttributedString> mainLines;
private List<AttributedString> footerLines;
private int totalMainWidth;
private SqlExecutionException executionException;
private OUT result;
public CliView(CliClient client) {
this.client = client;
keyReader = new BindingReader(client.getTerminal().reader());
}
public void open() {
isRunning = true;
inputThread = Thread.currentThread();
// prepare terminal
final Tuple2<Attributes, Map<Signal, SignalHandler>> prev = prepareTerminal();
ensureTerminalFullScreen();
updateSize();
init();
synchronized (this) {
display();
}
final KeyMap<OP> keys = getKeys();
while (isRunning) {
final OP operation;
try {
operation = keyReader.readBinding(keys, null, true);
} catch (IOError e) {
break;
}
// refresh loop
if (operation == null) {
continue;
}
synchronized (this) {
try {
evaluate(operation, keyReader.getLastBinding());
} catch (SqlExecutionException e) {
// in case the evaluate method did not use the close method
close(e);
}
if (isRunning) {
// ensure full-screen again in case a sub-view has been opened in evaluate
ensureTerminalFullScreen();
display();
}
}
}
cleanUp();
// clean terminal
restoreTerminal(prev);
unsetTerminalFullScreen();
if (executionException != null) {
throw executionException;
}
}
public OUT getResult() {
return result;
}
// --------------------------------------------------------------------------------------------
protected boolean isRunning() {
return isRunning;
}
protected void close() {
if (isRunning) {
isRunning = false;
// break the input loop if this method is called from another thread
if (Thread.currentThread() != inputThread) {
inputThread.interrupt();
}
}
}
protected void close(SqlExecutionException e) {
executionException = e;
close();
}
protected void close(OUT result) {
this.result = result;
isRunning = false;
}
protected void display() {
// cache
final List<AttributedString> headerLines = getHeaderLines();
final List<AttributedString> mainHeaderLines = getMainHeaderLines();
final List<AttributedString> mainLines = getMainLines();
final List<AttributedString> footerLines = getFooterLines();
final int visibleMainHeight = getVisibleMainHeight();
final int totalMainWidth = getTotalMainWidth();
// create output
client.clearTerminal();
final List<String> lines = new ArrayList<>();
// title part
client.getTerminal().writer().println(computeTitleLine().toAnsi());
// header part
headerLines.forEach(l -> client.getTerminal().writer().println(l.toAnsi()));
// main part
// update vertical offset
if (visibleMainHeight > mainLines.size()) {
offsetY = 0; // enough space
} else {
offsetY = Math.min(mainLines.size() - visibleMainHeight, offsetY); // bound offset
}
// update horizontal offset
if (width > totalMainWidth) {
offsetX = 0; // enough space
} else {
offsetX = Math.min(totalMainWidth - width, offsetX); // bound offset
}
// create window
final List<AttributedString> windowedMainLines =
mainLines.subList(offsetY, Math.min(mainLines.size(), offsetY + visibleMainHeight));
// print window
Stream.concat(mainHeaderLines.stream(), windowedMainLines.stream()).forEach(l -> {
if (offsetX < l.length()) {
final AttributedString windowX = l.substring(offsetX, Math.min(l.length(), offsetX + width));
client.getTerminal().writer().println(windowX.toAnsi());
} else {
client.getTerminal().writer().println(); // nothing to show for this line
}
});
// footer part
final int emptyHeight = height - 1 - headerLines.size() - // -1 = title
windowedMainLines.size() - mainHeaderLines.size() - footerLines.size();
// padding
IntStream.range(0, emptyHeight).forEach(i -> client.getTerminal().writer().println());
// footer
IntStream.range(0, footerLines.size()).forEach((i) -> {
final AttributedString l = footerLines.get(i);
if (i == footerLines.size() - 1) {
client.getTerminal().writer().print(l.toAnsi());
} else {
client.getTerminal().writer().println(l.toAnsi());
}
});
client.getTerminal().flush();
}
protected void scrollLeft() {
if (offsetX > 0) {
offsetX -= 1;
}
}
protected void scrollRight() {
final int maxOffset = Math.max(0, getTotalMainWidth() - width);
if (offsetX < maxOffset) {
offsetX += 1;
}
}
protected void scrollUp() {
if (offsetY > 0) {
offsetY -= 1;
}
}
protected void scrollDown() {
scrollDown(1);
}
protected void scrollDown(int n) {
final int maxOffset = Math.max(0, getMainLines().size() - getVisibleMainHeight());
offsetY = Math.min(maxOffset, offsetY + n);
}
protected int getVisibleMainHeight() {
// -1 = title line
return height - 1 - getHeaderLines().size() - getMainHeaderLines().size() -
getFooterLines().size();
}
protected List<AttributedString> getHeaderLines() {
if (headerLines == null) {
headerLines = computeHeaderLines();
}
return headerLines;
}
protected List<AttributedString> getMainHeaderLines() {
if (mainHeaderLines == null) {
mainHeaderLines = computeMainHeaderLines();
totalMainWidth = computeTotalMainWidth();
}
return mainHeaderLines;
}
protected List<AttributedString> getMainLines() {
if (mainLines == null) {
mainLines = computeMainLines();
totalMainWidth = computeTotalMainWidth();
}
return mainLines;
}
protected List<AttributedString> getFooterLines() {
if (footerLines == null) {
footerLines = computeFooterLines();
}
return footerLines;
}
protected int getTotalMainWidth() {
if (totalMainWidth <= 0) {
totalMainWidth = computeTotalMainWidth();
}
return totalMainWidth;
}
protected AttributedString getTitleLine() {
if (titleLine == null) {
titleLine = computeTitleLine();
}
return titleLine;
}
/**
* Must be called when values in one or more parts have changed.
*/
protected void resetAllParts() {
titleLine = null;
headerLines = null;
mainHeaderLines = null;
mainLines = null;
footerLines = null;
totalMainWidth = 0;
}
/**
* Must be called when values in the main part (main header or main) have changed.
*/
protected void resetMainPart() {
mainHeaderLines = null;
mainLines = null;
totalMainWidth = 0;
}
protected int getWidth() {
return width;
}
protected int getHeight() {
return height;
}
// --------------------------------------------------------------------------------------------
private void updateSize() {
width = client.getWidth();
height = client.getHeight();
totalMainWidth = width;
resetAllParts();
}
private void ensureTerminalFullScreen() {
final Terminal terminal = client.getTerminal();
terminal.puts(Capability.enter_ca_mode);
terminal.puts(Capability.keypad_xmit);
terminal.puts(Capability.cursor_invisible);
}
private Tuple2<Attributes, Map<Signal, SignalHandler>> prepareTerminal() {
final Terminal terminal = client.getTerminal();
final Attributes prevAttributes = terminal.getAttributes();
// adopted from org.jline.builtins.Nano
// see also https://en.wikibooks.org/wiki/Serial_Programming/termios#Basic_Configuration_of_a_Serial_Interface
// no line processing
// canonical mode off, echo off, echo newline off, extended input processing off
Attributes newAttr = new Attributes(prevAttributes);
newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.ECHO, LocalFlag.IEXTEN), false);
// turn off input processing
newAttr.setInputFlags(EnumSet.of(Attributes.InputFlag.IXON, Attributes.InputFlag.ICRNL, Attributes.InputFlag.INLCR), false);
// one input byte is enough to return from read, inter-character timer off
newAttr.setControlChar(Attributes.ControlChar.VMIN, 1);
newAttr.setControlChar(Attributes.ControlChar.VTIME, 0);
newAttr.setControlChar(Attributes.ControlChar.VINTR, 0);
terminal.setAttributes(newAttr);
final Map<Signal, SignalHandler> prevSignals = new HashMap<>();
prevSignals.put(Signal.WINCH, terminal.handle(Signal.WINCH, this::handleSignal));
prevSignals.put(Signal.INT, terminal.handle(Signal.INT, this::handleSignal));
prevSignals.put(Signal.QUIT, terminal.handle(Signal.QUIT, this::handleSignal));
return Tuple2.of(prevAttributes, prevSignals);
}
private void restoreTerminal(Tuple2<Attributes, Map<Signal, SignalHandler>> prev) {
final Terminal terminal = client.getTerminal();
terminal.setAttributes(prev.f0);
prev.f1.forEach(terminal::handle);
}
private void unsetTerminalFullScreen() {
final Terminal terminal = client.getTerminal();
terminal.puts(Capability.exit_ca_mode);
terminal.puts(Capability.keypad_local);
terminal.puts(Capability.cursor_visible);
}
private int computeTotalMainWidth() {
final List<AttributedString> mainLines = getMainLines();
final List<AttributedString> mainHeaderLines = getMainHeaderLines();
final int max1 = mainLines.stream().mapToInt(AttributedString::length).max().orElse(0);
final int max2 = mainHeaderLines.stream().mapToInt(AttributedString::length).max().orElse(0);
return Math.max(max1, max2);
}
private AttributedString computeTitleLine() {
final String title = getTitle();
final AttributedStringBuilder titleLine = new AttributedStringBuilder();
titleLine.style(AttributedStyle.INVERSE);
final int totalMargin = width - title.length();
final int margin = totalMargin / 2;
repeatChar(titleLine, ' ', margin);
titleLine.append(title);
repeatChar(titleLine, ' ', margin + (totalMargin % 2));
return titleLine.toAttributedString();
}
private void handleSignal(Signal signal) {
synchronized (this) {
switch (signal) {
case INT:
close(new SqlExecutionException("Forced interrupt."));
break;
case QUIT:
close(new SqlExecutionException("Forced cancellation."));
break;
case WINCH:
updateSize();
if (isRunning) {
display();
}
break;
}
}
}
// --------------------------------------------------------------------------------------------
/**
* Starts threads if necessary.
*/
protected abstract void init();
protected abstract KeyMap<OP> getKeys();
protected abstract void evaluate(OP operation, String binding);
protected abstract String getTitle();
protected abstract List<AttributedString> computeHeaderLines();
protected abstract List<AttributedString> computeMainHeaderLines();
protected abstract List<AttributedString> computeMainLines();
protected abstract List<AttributedString> computeFooterLines();
protected abstract void cleanUp();
}