| /** |
| * 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); |
| } |
| |
| } |