| /* |
| * Copyright (c) 2002-2020, the original author or authors. |
| * |
| * This software is distributable under the BSD license. See the terms of the |
| * BSD license in the documentation provided with this software. |
| * |
| * https://opensource.org/licenses/BSD-3-Clause |
| */ |
| package jdk.internal.org.jline.utils; |
| |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| import jdk.internal.org.jline.terminal.Terminal; |
| import jdk.internal.org.jline.utils.InfoCmp.Capability; |
| |
| /** |
| * Handle display and visual cursor. |
| * |
| * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a> |
| */ |
| public class Display { |
| |
| /**OpenJDK: |
| * When true, when the cursor is moved to column 0, carriage_return capability is not used, |
| * but rather the cursor is moved left the appropriate number of positions. |
| * This is useful when there's something already printed on the first line (which is not |
| * specified as a prompt), for example when the user is providing an input. |
| */ |
| public static boolean DISABLE_CR = false; |
| |
| protected final Terminal terminal; |
| protected final boolean fullScreen; |
| protected List<AttributedString> oldLines = Collections.emptyList(); |
| protected int cursorPos; |
| private int columns; |
| private int columns1; // columns+1 |
| protected int rows; |
| protected boolean reset; |
| protected boolean delayLineWrap; |
| |
| protected final Map<Capability, Integer> cost = new HashMap<>(); |
| protected final boolean canScroll; |
| protected final boolean wrapAtEol; |
| protected final boolean delayedWrapAtEol; |
| protected final boolean cursorDownIsNewLine; |
| |
| public Display(Terminal terminal, boolean fullscreen) { |
| this.terminal = terminal; |
| this.fullScreen = fullscreen; |
| |
| this.canScroll = can(Capability.insert_line, Capability.parm_insert_line) |
| && can(Capability.delete_line, Capability.parm_delete_line); |
| this.wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin); |
| this.delayedWrapAtEol = this.wrapAtEol |
| && terminal.getBooleanCapability(Capability.eat_newline_glitch); |
| this.cursorDownIsNewLine = "\n".equals(Curses.tputs(terminal.getStringCapability(Capability.cursor_down))); |
| } |
| |
| /** |
| * If cursor is at right margin, don't wrap immediately. |
| * See <code>org.jline.reader.LineReader.Option#DELAY_LINE_WRAP</code>. |
| * @return <code>true</code> if line wrap is delayed, <code>false</code> otherwise |
| */ |
| public boolean delayLineWrap() { |
| return delayLineWrap; |
| } |
| public void setDelayLineWrap(boolean v) { delayLineWrap = v; } |
| |
| public void resize(int rows, int columns) { |
| if (this.rows != rows || this.columns != columns) { |
| this.rows = rows; |
| this.columns = columns; |
| this.columns1 = columns + 1; |
| oldLines = AttributedString.join(AttributedString.EMPTY, oldLines).columnSplitLength(columns, true, delayLineWrap()); |
| } |
| } |
| |
| public void reset() { |
| oldLines = Collections.emptyList(); |
| } |
| |
| /** |
| * Clears the whole screen. |
| * Use this method only when using full-screen / application mode. |
| */ |
| public void clear() { |
| if (fullScreen) { |
| reset = true; |
| } |
| } |
| |
| public void updateAnsi(List<String> newLines, int targetCursorPos) { |
| update(newLines.stream().map(AttributedString::fromAnsi).collect(Collectors.toList()), targetCursorPos); |
| } |
| |
| /** |
| * Update the display according to the new lines and flushes the output. |
| * @param newLines the lines to display |
| * @param targetCursorPos desired cursor position - see Size.cursorPos. |
| */ |
| public void update(List<AttributedString> newLines, int targetCursorPos) { |
| update(newLines, targetCursorPos, true); |
| } |
| |
| /** |
| * Update the display according to the new lines. |
| * @param newLines the lines to display |
| * @param targetCursorPos desired cursor position - see Size.cursorPos. |
| * @param flush whether the output should be flushed or not |
| */ |
| public void update(List<AttributedString> newLines, int targetCursorPos, boolean flush) { |
| if (reset) { |
| terminal.puts(Capability.clear_screen); |
| oldLines.clear(); |
| cursorPos = 0; |
| reset = false; |
| } |
| |
| // If dumb display, get rid of ansi sequences now |
| Integer cols = terminal.getNumericCapability(Capability.max_colors); |
| if (cols == null || cols < 8) { |
| newLines = newLines.stream().map(s -> new AttributedString(s.toString())) |
| .collect(Collectors.toList()); |
| } |
| |
| // Detect scrolling |
| if ((fullScreen || newLines.size() >= rows) && newLines.size() == oldLines.size() && canScroll) { |
| int nbHeaders = 0; |
| int nbFooters = 0; |
| // Find common headers and footers |
| int l = newLines.size(); |
| while (nbHeaders < l |
| && Objects.equals(newLines.get(nbHeaders), oldLines.get(nbHeaders))) { |
| nbHeaders++; |
| } |
| while (nbFooters < l - nbHeaders - 1 |
| && Objects.equals(newLines.get(newLines.size() - nbFooters - 1), oldLines.get(oldLines.size() - nbFooters - 1))) { |
| nbFooters++; |
| } |
| List<AttributedString> o1 = newLines.subList(nbHeaders, newLines.size() - nbFooters); |
| List<AttributedString> o2 = oldLines.subList(nbHeaders, oldLines.size() - nbFooters); |
| int[] common = longestCommon(o1, o2); |
| if (common != null) { |
| int s1 = common[0]; |
| int s2 = common[1]; |
| int sl = common[2]; |
| if (sl > 1 && s1 < s2) { |
| moveVisualCursorTo((nbHeaders + s1) * columns1); |
| int nb = s2 - s1; |
| deleteLines(nb); |
| for (int i = 0; i < nb; i++) { |
| oldLines.remove(nbHeaders + s1); |
| } |
| if (nbFooters > 0) { |
| moveVisualCursorTo((nbHeaders + s1 + sl) * columns1); |
| insertLines(nb); |
| for (int i = 0; i < nb; i++) { |
| oldLines.add(nbHeaders + s1 + sl, new AttributedString("")); |
| } |
| } |
| } else if (sl > 1 && s1 > s2) { |
| int nb = s1 - s2; |
| if (nbFooters > 0) { |
| moveVisualCursorTo((nbHeaders + s2 + sl) * columns1); |
| deleteLines(nb); |
| for (int i = 0; i < nb; i++) { |
| oldLines.remove(nbHeaders + s2 + sl); |
| } |
| } |
| moveVisualCursorTo((nbHeaders + s2) * columns1); |
| insertLines(nb); |
| for (int i = 0; i < nb; i++) { |
| oldLines.add(nbHeaders + s2, new AttributedString("")); |
| } |
| } |
| } |
| } |
| |
| int lineIndex = 0; |
| int currentPos = 0; |
| int numLines = Math.max(oldLines.size(), newLines.size()); |
| boolean wrapNeeded = false; |
| while (lineIndex < numLines) { |
| AttributedString oldLine = |
| lineIndex < oldLines.size() ? oldLines.get(lineIndex) |
| : AttributedString.NEWLINE; |
| AttributedString newLine = |
| lineIndex < newLines.size() ? newLines.get(lineIndex) |
| : AttributedString.NEWLINE; |
| currentPos = lineIndex * columns1; |
| int curCol = currentPos; |
| int oldLength = oldLine.length(); |
| int newLength = newLine.length(); |
| boolean oldNL = oldLength > 0 && oldLine.charAt(oldLength-1)=='\n'; |
| boolean newNL = newLength > 0 && newLine.charAt(newLength-1)=='\n'; |
| if (oldNL) { |
| oldLength--; |
| oldLine = oldLine.substring(0, oldLength); |
| } |
| if (newNL) { |
| newLength--; |
| newLine = newLine.substring(0, newLength); |
| } |
| if (wrapNeeded |
| && lineIndex == (cursorPos + 1) / columns1 |
| && lineIndex < newLines.size()) { |
| // move from right margin to next line's left margin |
| cursorPos++; |
| if (newLength == 0 || newLine.isHidden(0)) { |
| // go to next line column zero |
| rawPrint(new AttributedString(" \b")); |
| } else { |
| AttributedString firstChar = newLine.substring(0, 1); |
| // go to next line column one |
| rawPrint(firstChar); |
| cursorPos += firstChar.columnLength(); // normally 1 |
| newLine = newLine.substring(1, newLength); |
| newLength--; |
| if (oldLength > 0) { |
| oldLine = oldLine.substring(1, oldLength); |
| oldLength--; |
| } |
| currentPos = cursorPos; |
| } |
| } |
| List<DiffHelper.Diff> diffs = DiffHelper.diff(oldLine, newLine); |
| boolean ident = true; |
| boolean cleared = false; |
| for (int i = 0; i < diffs.size(); i++) { |
| DiffHelper.Diff diff = diffs.get(i); |
| int width = diff.text.columnLength(); |
| switch (diff.operation) { |
| case EQUAL: |
| if (!ident) { |
| cursorPos = moveVisualCursorTo(currentPos); |
| rawPrint(diff.text); |
| cursorPos += width; |
| currentPos = cursorPos; |
| } else { |
| currentPos += width; |
| } |
| break; |
| case INSERT: |
| if (i <= diffs.size() - 2 |
| && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { |
| cursorPos = moveVisualCursorTo(currentPos); |
| if (insertChars(width)) { |
| rawPrint(diff.text); |
| cursorPos += width; |
| currentPos = cursorPos; |
| break; |
| } |
| } else if (i <= diffs.size() - 2 |
| && diffs.get(i + 1).operation == DiffHelper.Operation.DELETE |
| && width == diffs.get(i + 1).text.columnLength()) { |
| moveVisualCursorTo(currentPos); |
| rawPrint(diff.text); |
| cursorPos += width; |
| currentPos = cursorPos; |
| i++; // skip delete |
| break; |
| } |
| moveVisualCursorTo(currentPos); |
| rawPrint(diff.text); |
| cursorPos += width; |
| currentPos = cursorPos; |
| ident = false; |
| break; |
| case DELETE: |
| if (cleared) { |
| continue; |
| } |
| if (currentPos - curCol >= columns) { |
| continue; |
| } |
| if (i <= diffs.size() - 2 |
| && diffs.get(i + 1).operation == DiffHelper.Operation.EQUAL) { |
| if (currentPos + diffs.get(i + 1).text.columnLength() < columns) { |
| moveVisualCursorTo(currentPos); |
| if (deleteChars(width)) { |
| break; |
| } |
| } |
| } |
| int oldLen = oldLine.columnLength(); |
| int newLen = newLine.columnLength(); |
| int nb = Math.max(oldLen, newLen) - (currentPos - curCol); |
| moveVisualCursorTo(currentPos); |
| if (!terminal.puts(Capability.clr_eol)) { |
| rawPrint(' ', nb); |
| cursorPos += nb; |
| } |
| cleared = true; |
| ident = false; |
| break; |
| } |
| } |
| lineIndex++; |
| boolean newWrap = ! newNL && lineIndex < newLines.size(); |
| if (targetCursorPos + 1 == lineIndex * columns1 |
| && (newWrap || ! delayLineWrap)) |
| targetCursorPos++; |
| boolean atRight = (cursorPos - curCol) % columns1 == columns; |
| wrapNeeded = false; |
| if (this.delayedWrapAtEol) { |
| boolean oldWrap = ! oldNL && lineIndex < oldLines.size(); |
| if (newWrap != oldWrap && ! (oldWrap && cleared)) { |
| moveVisualCursorTo(lineIndex*columns1-1, newLines); |
| if (newWrap) |
| wrapNeeded = true; |
| else |
| terminal.puts(Capability.clr_eol); |
| } |
| } else if (atRight) { |
| if (this.wrapAtEol) { |
| terminal.writer().write(" \b"); |
| cursorPos++; |
| } else { |
| terminal.puts(Capability.carriage_return); // CR / not newline. |
| cursorPos = curCol; |
| } |
| currentPos = cursorPos; |
| } |
| } |
| if (cursorPos != targetCursorPos) { |
| moveVisualCursorTo(targetCursorPos < 0 ? currentPos : targetCursorPos, newLines); |
| } |
| oldLines = newLines; |
| |
| if (flush) { |
| terminal.flush(); |
| } |
| } |
| |
| protected boolean deleteLines(int nb) { |
| return perform(Capability.delete_line, Capability.parm_delete_line, nb); |
| } |
| |
| protected boolean insertLines(int nb) { |
| return perform(Capability.insert_line, Capability.parm_insert_line, nb); |
| } |
| |
| protected boolean insertChars(int nb) { |
| return perform(Capability.insert_character, Capability.parm_ich, nb); |
| } |
| |
| protected boolean deleteChars(int nb) { |
| return perform(Capability.delete_character, Capability.parm_dch, nb); |
| } |
| |
| protected boolean can(Capability single, Capability multi) { |
| return terminal.getStringCapability(single) != null |
| || terminal.getStringCapability(multi) != null; |
| } |
| |
| protected boolean perform(Capability single, Capability multi, int nb) { |
| boolean hasMulti = terminal.getStringCapability(multi) != null; |
| boolean hasSingle = terminal.getStringCapability(single) != null; |
| if (hasMulti && (!hasSingle || cost(single) * nb > cost(multi))) { |
| terminal.puts(multi, nb); |
| return true; |
| } else if (hasSingle) { |
| for (int i = 0; i < nb; i++) { |
| terminal.puts(single); |
| } |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private int cost(Capability cap) { |
| return cost.computeIfAbsent(cap, this::computeCost); |
| } |
| |
| private int computeCost(Capability cap) { |
| String s = Curses.tputs(terminal.getStringCapability(cap), 0); |
| return s != null ? s.length() : Integer.MAX_VALUE; |
| } |
| |
| private static int[] longestCommon(List<AttributedString> l1, List<AttributedString> l2) { |
| int start1 = 0; |
| int start2 = 0; |
| int max = 0; |
| for (int i = 0; i < l1.size(); i++) { |
| for (int j = 0; j < l2.size(); j++) { |
| int x = 0; |
| while (Objects.equals(l1.get(i + x), l2.get(j + x))) { |
| x++; |
| if (((i + x) >= l1.size()) || ((j + x) >= l2.size())) break; |
| } |
| if (x > max) { |
| max = x; |
| start1 = i; |
| start2 = j; |
| } |
| } |
| } |
| return max != 0 ? new int[] { start1, start2, max } : null; |
| } |
| |
| /* |
| * Move cursor from cursorPos to argument, updating cursorPos |
| * We're at the right margin if {@code (cursorPos % columns1) == columns}. |
| * This method knows how to move both *from* and *to* the right margin. |
| */ |
| protected void moveVisualCursorTo(int targetPos, |
| List<AttributedString> newLines) { |
| if (cursorPos != targetPos) { |
| boolean atRight = (targetPos % columns1) == columns; |
| moveVisualCursorTo(targetPos - (atRight ? 1 : 0)); |
| if (atRight) { |
| // There is no portable way to move to the right margin |
| // except by writing a character in the right-most column. |
| int row = targetPos / columns1; |
| AttributedString lastChar = row >= newLines.size() ? AttributedString.EMPTY |
| : newLines.get(row).columnSubSequence(columns-1, columns); |
| if (lastChar.length() == 0) |
| rawPrint((int) ' '); |
| else |
| rawPrint(lastChar); |
| cursorPos++; |
| } |
| } |
| } |
| |
| /* |
| * Move cursor from cursorPos to argument, updating cursorPos |
| * We're at the right margin if {@code (cursorPos % columns1) == columns}. |
| * This method knows how to move *from* the right margin, |
| * but does not know how to move *to* the right margin. |
| * I.e. {@code (i1 % columns1) == column} is not allowed. |
| */ |
| protected int moveVisualCursorTo(int i1) { |
| int i0 = cursorPos; |
| if (i0 == i1) return i1; |
| int width = columns1; |
| int l0 = i0 / width; |
| int c0 = i0 % width; |
| int l1 = i1 / width; |
| int c1 = i1 % width; |
| if (c0 == columns) { // at right margin |
| terminal.puts(Capability.carriage_return); |
| c0 = 0; |
| } |
| if (l0 > l1) { |
| perform(Capability.cursor_up, Capability.parm_up_cursor, l0 - l1); |
| } else if (l0 < l1) { |
| // TODO: clean the following |
| if (fullScreen) { |
| if (!terminal.puts(Capability.parm_down_cursor, l1 - l0)) { |
| for (int i = l0; i < l1; i++) { |
| terminal.puts(Capability.cursor_down); |
| } |
| if (cursorDownIsNewLine) { |
| c0 = 0; |
| } |
| } |
| } else { |
| terminal.puts(Capability.carriage_return); |
| rawPrint('\n', l1 - l0); |
| c0 = 0; |
| } |
| } |
| if (c0 != 0 && c1 == 0 && !DISABLE_CR) { |
| terminal.puts(Capability.carriage_return); |
| } else if (c0 < c1) { |
| perform(Capability.cursor_right, Capability.parm_right_cursor, c1 - c0); |
| } else if (c0 > c1) { |
| perform(Capability.cursor_left, Capability.parm_left_cursor, c0 - c1); |
| } |
| cursorPos = i1; |
| return i1; |
| } |
| |
| void rawPrint(char c, int num) { |
| for (int i = 0; i < num; i++) { |
| rawPrint(c); |
| } |
| } |
| |
| void rawPrint(int c) { |
| terminal.writer().write(c); |
| } |
| |
| void rawPrint(AttributedString str) { |
| str.print(terminal); |
| } |
| |
| public int wcwidth(String str) { |
| return str != null ? AttributedString.fromAnsi(str).columnLength() : 0; |
| } |
| |
| } |