blob: 31a08e8cdaa21903d925c85539fb97a018a605fe [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.blur.shell;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import jline.Terminal;
import jline.console.ConsoleReader;
public class TableDisplay implements Closeable {
public static void main(String[] args) throws IOException, InterruptedException {
// System.out.println("\u001B[1mfoo\u001B[0m@bar\u001B[32m@baz\u001B[0m>");
// \u001B[0m normal
// \u001B[33m yellow
// System.out.println("\u001B[33mCOLUMN\u001B[0mnormal");
//
// for (int i = 0; i < 100; i++) {
// System.out.println("\u001B[0m" + i + "\u001B[" + i + "mfoo");
// }
ConsoleReader reader = new ConsoleReader();
TableDisplay tableDisplay = new TableDisplay(reader);
tableDisplay.setSeperator("|");
// Random random = new Random();
int maxX = 20;
int maxY = 100;
tableDisplay.setHeader(0, "");
for (int i = 1; i < maxX; i++) {
tableDisplay.setHeader(i, "col-" + i);
}
for (int i = 0; i < maxY; i++) {
tableDisplay.set(0, i, i);
}
final AtomicBoolean running = new AtomicBoolean(true);
tableDisplay.addKeyHook(new Runnable() {
@Override
public void run() {
synchronized (running) {
running.set(false);
running.notifyAll();
}
}
}, 'q');
try {
// int i = 0;
// while (true) {
// if (i >= 100000) {
// i = 0;
// }
// tableDisplay.set(random.nextInt(maxX) + 1, random.nextInt(maxY),
// random.nextLong());
// Thread.sleep(3000);
// i++;
// }
for (int x = 0; x < maxX; x++) {
for (int y = 0; y < maxY; y++) {
if (x == 7 && y == 7) {
tableDisplay.set(x, y, highlight(x + "," + y));
} else {
tableDisplay.set(x, y, x + "," + y);
}
}
}
while (running.get()) {
synchronized (running) {
running.wait(1000);
}
}
} finally {
tableDisplay.close();
}
}
private static String highlight(String s) {
// return s;
return "\u001B[33m" + s + "\u001B[0m";
}
public void addKeyHook(Runnable runnable, int c) {
_keyHookMap.put(c, runnable);
}
private static final String SEP = "|";
private final ConsoleReader _reader;
private final ConcurrentMap<Key, Object> _values = new ConcurrentHashMap<TableDisplay.Key, Object>();
private final ConcurrentMap<Integer, String> _header = new ConcurrentHashMap<Integer, String>();
private final ConcurrentMap<Integer, Integer> _maxWidth = new ConcurrentHashMap<Integer, Integer>();
private final AtomicBoolean _running = new AtomicBoolean(true);
private final Thread _paintThread;
private final Canvas _canvas;
private int _maxY;
private int _maxX;
private String _seperator = SEP;
private final Thread _inputReaderThread;
private final Map<Integer, Runnable> _keyHookMap = new ConcurrentHashMap<Integer, Runnable>();
private final AtomicBoolean _stopReadingInput = new AtomicBoolean(false);
private String _description = "";
private volatile long _lastPaint = 0;
private volatile boolean _dirty = true;
public void setDescription(String description) {
_description = description;
}
public void setSeperator(String seperator) {
_seperator = seperator;
}
public TableDisplay(ConsoleReader reader) {
_reader = reader;
_canvas = new Canvas(_reader);
_paintThread = new Thread(new Runnable() {
@Override
public void run() {
while (_running.get()) {
long now = System.currentTimeMillis();
synchronized (_canvas) {
if (_lastPaint + 1000 < now) {
try {
render(_canvas);
} catch (IOException e) {
e.printStackTrace();
}
_lastPaint = System.currentTimeMillis();
_dirty = false;
}
if (_dirty) {
try {
_canvas.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
_canvas.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
_paintThread.setDaemon(true);
_paintThread.setName("Render Thread");
_paintThread.start();
_inputReaderThread = new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream input = _reader.getInput();
int read;
while (!_stopReadingInput.get() && _running.get() && (read = input.read()) != -1) {
if (read == 27) {
if (input.read() == 91) {
read = input.read();
switch (read) {
case 68:
// left
_canvas.moveLeft();
break;
case 67:
// right
_canvas.moveRight();
break;
case 65:
// up
_canvas.moveUp();
break;
case 66:
// down
_canvas.moveDown();
break;
case 53:
// up
_canvas.pageUp();
break;
case 54:
// down
_canvas.pageDown();
break;
default:
break;
}
_lastPaint = 0;
}
} else {
Runnable runnable = _keyHookMap.get(read);
if (runnable != null) {
runnable.run();
}
}
paint();
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
_inputReaderThread.setName("Input Reader Thread");
_inputReaderThread.setDaemon(true);
_inputReaderThread.start();
}
private void render(Canvas canvas) throws IOException {
canvas.reset();
canvas.resetHeader();
canvas.appendHeader(_description);
canvas.endHeader();
buildHeaderOutput(canvas);
buildTableOutput(canvas);
canvas.write();
}
private void buildHeaderOutput(Canvas canvas) {
canvas.startHeader();
for (int x = 0; x < _maxX; x++) {
if (x != 0) {
canvas.appendHeader(_seperator);
}
Object value = _header.get(x);
int widthForColumn = getWidthForColumn(x);
bufferHeader(canvas, getString(value), widthForColumn);
}
canvas.endHeader();
}
private void buildTableOutput(Canvas canvas) {
for (int y = 0; y < _maxY; y++) {
canvas.startLine();
for (int x = 0; x < _maxX; x++) {
if (x != 0) {
canvas.append(_seperator);
}
Key key = new Key(x, y);
Object value = _values.get(key);
int widthForColumn = getWidthForColumn(x);
buffer(canvas, getString(value), widthForColumn);
}
canvas.endLine();
}
}
private int getWidthForColumn(int x) {
Integer width = _maxWidth.get(x);
if (width == null) {
return 0;
}
return width;
}
private void bufferHeader(Canvas canvas, String value, int width) {
canvas.appendHeader(value);
width -= getVisibleLength(value);
while (width > 0) {
canvas.appendHeader(' ');
width--;
}
}
private void buffer(Canvas canvas, String value, int width) {
canvas.append(value);
width -= getVisibleLength(value);
while (width > 0) {
canvas.append(' ');
width--;
}
}
public void setHeader(int x, String name) {
_header.put(x, name);
setMaxWidth(x, name);
paint();
}
public void set(int x, int y, Object value) {
if (value == null) {
remove(x, y);
return;
}
_values.put(new Key(x, y), value);
setMaxX(x);
setMaxY(y);
setMaxWidth(x, value);
paint();
}
private void paint() {
synchronized (_canvas) {
_canvas.notify();
_dirty = true;
}
}
private void setMaxWidth(int x, Object value) {
if (value != null) {
String s = getString(value);
int length = getVisibleLength(s);
while (true) {
Integer width = _maxWidth.get(x);
if (width == null) {
width = _maxWidth.putIfAbsent(x, length);
if (width == null) {
return;
}
}
if (width < length) {
if (_maxWidth.replace(x, width, length)) {
return;
}
} else {
return;
}
}
}
}
private int getVisibleLength(String s) {
int length = 0;
boolean color = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (color) {
if (c == 'm') {
color = false;
}
} else {
if (c == 27) {
color = true;
} else {
length++;
}
}
}
return length;
}
private String getString(Object value) {
if (value != null) {
return value.toString();
}
return "";
}
private void setMaxY(int y) {
if (_maxY < y + 1) {
_maxY = y + 1;
}
}
private void setMaxX(int x) {
if (_maxX < x + 1) {
_maxX = x + 1;
}
}
public void remove(int x, int y) {
_values.remove(new Key(x, y));
}
@Override
public void close() throws IOException {
if (_running.get()) {
_running.set(false);
_inputReaderThread.interrupt();
try {
_inputReaderThread.join();
} catch (InterruptedException e) {
throw new IOException(e);
}
}
}
public static class Key implements Comparable<Key> {
final int _x;
final int _y;
public Key(int x, int y) {
_x = x;
_y = y;
}
public int getX() {
return _x;
}
public int getY() {
return _y;
}
@Override
public int compareTo(Key o) {
if (_y == o._y) {
return _x - o._x;
}
return _y - o._y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + _x;
result = prime * result + _y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
if (_x != other._x)
return false;
if (_y != other._y)
return false;
return true;
}
}
public static class LineBuilder {
static class LineBuilderPart {
String _visible;
String _notVisible;
}
private List<LineBuilderPart> _parts = new ArrayList<LineBuilderPart>();
private boolean _readingColor;
private StringBuilder _currentNotVisible = new StringBuilder();
private StringBuilder _currentVisible = new StringBuilder();
public void reset() {
_currentNotVisible.setLength(0);
_currentVisible.setLength(0);
_parts.clear();
}
public void close() {
startNewPart();
}
public int visibleLength() {
int l = 0;
for (LineBuilderPart p : _parts) {
l += p._visible.length();
}
return l;
}
public String substring(int start, int end) {
StringBuilder b = new StringBuilder();
Iterator<LineBuilderPart> iterator = _parts.iterator();
LineBuilderPart current = iterator.next();
int partPos = 0;
boolean currentPartHasEmittedNonVisible = false;
for (int i = 0; i < end; i++) {
if (partPos >= current._visible.length()) {
current = iterator.next();
partPos = 0;
currentPartHasEmittedNonVisible = false;
}
if (isVisible(i, start, end)) {
if (!currentPartHasEmittedNonVisible) {
b.append(current._notVisible);
currentPartHasEmittedNonVisible = true;
}
b.append(current._visible.charAt(partPos));
}
partPos++;
}
return b.toString();
}
private boolean isVisible(int i, int start, int end) {
if (i >= start && i < end) {
return true;
}
return false;
}
public void append(char c) {
if (_readingColor) {
if (c == 'm') {
_readingColor = false;
_currentNotVisible.append(c);
} else {
_currentNotVisible.append(c);
}
} else {
if (c == 27) {
startNewPart();
_readingColor = true;
_currentNotVisible.append(c);
} else {
_currentVisible.append(c);
}
}
}
private void startNewPart() {
LineBuilderPart part = new LineBuilderPart();
part._notVisible = _currentNotVisible.toString();
part._visible = _currentVisible.toString();
_parts.add(part);
_currentNotVisible.setLength(0);
_currentVisible.setLength(0);
}
public void append(String s) {
for (int i = 0; i < s.length(); i++) {
append(s.charAt(i));
}
}
}
static class Canvas {
private final StringBuilder _screenBuilder = new StringBuilder();
private final LineBuilder _lineBuilder = new LineBuilder();
private final StringBuilder _headerScreenBuilder = new StringBuilder();
private final LineBuilder _headerLineBuilder = new LineBuilder();
private final ConsoleReader _reader;
private int _height;
private int _width;
private int _line;
private volatile int _posX = 0;
private volatile int _posY = 0;
private int _leftRightMoveSize;
private int _headerLine;
public Canvas(ConsoleReader reader) {
_reader = reader;
}
public void resetHeader() {
_headerLine = 0;
_headerScreenBuilder.setLength(0);
}
public void reset() {
_line = 0;
_screenBuilder.setLength(0);
Terminal terminal = _reader.getTerminal();
_height = terminal.getHeight() - 3;
_width = terminal.getWidth() - 2;
_leftRightMoveSize = _width / 4;
}
public void endLine() {
_lineBuilder.close();
int pos = _line - _posY;
if (pos >= 0 && pos < (_height - _headerLine)) {
int end = _posX + _width;
int s = _posX;
int e = Math.min(_lineBuilder.visibleLength(), end);
if (e > s) {
_screenBuilder.append(_lineBuilder.substring(s, e));
}
_screenBuilder.append('\n');
}
_line++;
_lineBuilder.reset();
}
public void endHeader() {
_headerLineBuilder.close();
int end = _posX + _width;
int s = _posX;
int e = Math.min(_headerLineBuilder.visibleLength(), end);
if (e > s) {
_headerScreenBuilder.append(_headerLineBuilder.substring(s, e));
}
_headerScreenBuilder.append('\n');
_headerLineBuilder.reset();
_headerLine++;
}
public void startLine() {
}
public void startHeader() {
}
public void appendHeader(char c) {
_headerLineBuilder.append(c);
}
public void appendHeader(String s) {
_headerLineBuilder.append(s);
}
public void append(char c) {
_lineBuilder.append(c);
}
public void append(String s) {
_lineBuilder.append(s);
}
public void write() throws IOException {
_reader.clearScreen();
Writer writer = _reader.getOutput();
writer.write(_headerScreenBuilder.toString());
writer.write(_screenBuilder.toString());
writer.flush();
}
public void moveLeft() {
if (_posX > 0) {
_posX -= _leftRightMoveSize;
}
}
public void moveRight() {
_posX += _leftRightMoveSize;
}
public void moveUp() {
if (_posY > 0) {
_posY--;
}
}
public void moveDown() {
_posY++;
}
public void pageUp() {
if (_posY < _height) {
_posY = 0;
} else {
_posY -= _height;
}
}
public void pageDown() {
_posY += _height;
}
}
public void setStopReadingInput(boolean b) {
_stopReadingInput.set(true);
}
}