2 * This file is part of lanterna (http://code.google.com/p/lanterna/).
4 * lanterna is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 * Copyright (C) 2010-2015 Martin
19 package com
.googlecode
.lanterna
.screen
;
21 import com
.googlecode
.lanterna
.*;
22 import com
.googlecode
.lanterna
.graphics
.Scrollable
;
23 import com
.googlecode
.lanterna
.input
.KeyStroke
;
24 import com
.googlecode
.lanterna
.input
.KeyType
;
25 import com
.googlecode
.lanterna
.terminal
.ResizeListener
;
26 import com
.googlecode
.lanterna
.terminal
.Terminal
;
28 import java
.io
.IOException
;
29 import java
.util
.Comparator
;
30 import java
.util
.EnumSet
;
32 import java
.util
.TreeMap
;
35 * This is the default concrete implementation of the Screen interface, a buffered layer sitting on top of a Terminal.
36 * If you want to get started with the Screen layer, this is probably the class you want to use. Remember to start the
37 * screen before you can use it and stop it when you are done with it. This will place the terminal in private mode
38 * during the screen operations and leave private mode afterwards.
41 public class TerminalScreen
extends AbstractScreen
{
42 private final Terminal terminal
;
43 private boolean isStarted
;
44 private boolean fullRedrawHint
;
45 private ScrollHint scrollHint
;
48 * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
49 * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
50 * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
52 * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
53 * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
54 * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
56 * @param terminal Terminal object to create the DefaultScreen on top of
57 * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
59 public TerminalScreen(Terminal terminal
) throws IOException
{
60 this(terminal
, DEFAULT_CHARACTER
);
64 * Creates a new Screen on top of a supplied terminal, will query the terminal for its size. The screen is initially
65 * blank. The default character used for unused space (the newly initialized state of the screen and new areas after
66 * expanding the terminal size) will be a blank space in 'default' ANSI front- and background color.
68 * Before you can display the content of this buffered screen to the real underlying terminal, you must call the
69 * {@code startScreen()} method. This will ask the terminal to enter private mode (which is required for Screens to
70 * work properly). Similarly, when you are done, you should call {@code stopScreen()} which will exit private mode.
72 * @param terminal Terminal object to create the DefaultScreen on top of.
73 * @param defaultCharacter What character to use for the initial state of the screen and expanded areas
74 * @throws java.io.IOException If there was an underlying I/O error when querying the size of the terminal
76 public TerminalScreen(Terminal terminal
, TextCharacter defaultCharacter
) throws IOException
{
77 super(terminal
.getTerminalSize(), defaultCharacter
);
78 this.terminal
= terminal
;
79 this.terminal
.addResizeListener(new TerminalResizeListener());
80 this.isStarted
= false;
81 this.fullRedrawHint
= true;
85 public synchronized void startScreen() throws IOException
{
91 getTerminal().enterPrivateMode();
92 getTerminal().getTerminalSize();
93 getTerminal().clearScreen();
94 this.fullRedrawHint
= true;
95 TerminalPosition cursorPosition
= getCursorPosition();
96 if(cursorPosition
!= null) {
97 getTerminal().setCursorVisible(true);
98 getTerminal().setCursorPosition(cursorPosition
.getColumn(), cursorPosition
.getRow());
100 getTerminal().setCursorVisible(false);
105 public void stopScreen() throws IOException
{
109 public synchronized void stopScreen(boolean flushInput
) throws IOException
{
115 //Drain the input queue
118 keyStroke
= pollInput();
120 while(keyStroke
!= null && keyStroke
.getKeyType() != KeyType
.EOF
);
123 getTerminal().exitPrivateMode();
128 public synchronized void refresh(RefreshType refreshType
) throws IOException
{
132 if((refreshType
== RefreshType
.AUTOMATIC
&& fullRedrawHint
) || refreshType
== RefreshType
.COMPLETE
) {
134 fullRedrawHint
= false;
136 else if(refreshType
== RefreshType
.AUTOMATIC
&&
137 (scrollHint
== null || scrollHint
== ScrollHint
.INVALID
)) {
138 double threshold
= getTerminalSize().getRows() * getTerminalSize().getColumns() * 0.75;
139 if(getBackBuffer().isVeryDifferent(getFrontBuffer(), (int) threshold
)) {
149 getBackBuffer().copyTo(getFrontBuffer());
150 TerminalPosition cursorPosition
= getCursorPosition();
151 if(cursorPosition
!= null) {
152 getTerminal().setCursorVisible(true);
153 //If we are trying to move the cursor to the padding of a CJK character, put it on the actual character instead
154 if(cursorPosition
.getColumn() > 0 && TerminalTextUtils
.isCharCJK(getFrontBuffer().getCharacterAt(cursorPosition
.withRelativeColumn(-1)).getCharacter())) {
155 getTerminal().setCursorPosition(cursorPosition
.getColumn() - 1, cursorPosition
.getRow());
158 getTerminal().setCursorPosition(cursorPosition
.getColumn(), cursorPosition
.getRow());
161 getTerminal().setCursorVisible(false);
163 getTerminal().flush();
166 private void useScrollHint() throws IOException
{
167 if (scrollHint
== null) { return; }
170 if (scrollHint
== ScrollHint
.INVALID
) { return; }
171 Terminal term
= getTerminal();
172 if (term
instanceof Scrollable
) {
173 // just try and see if it cares:
174 scrollHint
.applyTo( (Scrollable
)term
);
175 // if that didn't throw, then update front buffer:
176 scrollHint
.applyTo( getFrontBuffer() );
179 catch (UnsupportedOperationException uoe
) { /* ignore */ }
180 finally { scrollHint
= null; }
183 private void refreshByDelta() throws IOException
{
184 Map
<TerminalPosition
, TextCharacter
> updateMap
= new TreeMap
<TerminalPosition
, TextCharacter
>(new ScreenPointComparator());
185 TerminalSize terminalSize
= getTerminalSize();
189 for(int y
= 0; y
< terminalSize
.getRows(); y
++) {
190 for(int x
= 0; x
< terminalSize
.getColumns(); x
++) {
191 TextCharacter backBufferCharacter
= getBackBuffer().getCharacterAt(x
, y
);
192 if(!backBufferCharacter
.equals(getFrontBuffer().getCharacterAt(x
, y
))) {
193 updateMap
.put(new TerminalPosition(x
, y
), backBufferCharacter
);
195 if(TerminalTextUtils
.isCharCJK(backBufferCharacter
.getCharacter())) {
196 x
++; //Skip the trailing padding
201 if(updateMap
.isEmpty()) {
204 TerminalPosition currentPosition
= updateMap
.keySet().iterator().next();
205 getTerminal().setCursorPosition(currentPosition
.getColumn(), currentPosition
.getRow());
207 TextCharacter firstScreenCharacterToUpdate
= updateMap
.values().iterator().next();
208 EnumSet
<SGR
> currentSGR
= firstScreenCharacterToUpdate
.getModifiers();
209 getTerminal().resetColorAndSGR();
210 for(SGR sgr
: currentSGR
) {
211 getTerminal().enableSGR(sgr
);
213 TextColor currentForegroundColor
= firstScreenCharacterToUpdate
.getForegroundColor();
214 TextColor currentBackgroundColor
= firstScreenCharacterToUpdate
.getBackgroundColor();
215 getTerminal().setForegroundColor(currentForegroundColor
);
216 getTerminal().setBackgroundColor(currentBackgroundColor
);
217 for(TerminalPosition position
: updateMap
.keySet()) {
218 if(!position
.equals(currentPosition
)) {
219 getTerminal().setCursorPosition(position
.getColumn(), position
.getRow());
220 currentPosition
= position
;
222 TextCharacter newCharacter
= updateMap
.get(position
);
223 if(!currentForegroundColor
.equals(newCharacter
.getForegroundColor())) {
224 getTerminal().setForegroundColor(newCharacter
.getForegroundColor());
225 currentForegroundColor
= newCharacter
.getForegroundColor();
227 if(!currentBackgroundColor
.equals(newCharacter
.getBackgroundColor())) {
228 getTerminal().setBackgroundColor(newCharacter
.getBackgroundColor());
229 currentBackgroundColor
= newCharacter
.getBackgroundColor();
231 for(SGR sgr
: SGR
.values()) {
232 if(currentSGR
.contains(sgr
) && !newCharacter
.getModifiers().contains(sgr
)) {
233 getTerminal().disableSGR(sgr
);
234 currentSGR
.remove(sgr
);
236 else if(!currentSGR
.contains(sgr
) && newCharacter
.getModifiers().contains(sgr
)) {
237 getTerminal().enableSGR(sgr
);
241 getTerminal().putCharacter(newCharacter
.getCharacter());
242 if(TerminalTextUtils
.isCharCJK(newCharacter
.getCharacter())) {
243 //CJK characters advances two columns
244 currentPosition
= currentPosition
.withRelativeColumn(2);
247 //Normal characters advances one column
248 currentPosition
= currentPosition
.withRelativeColumn(1);
253 private void refreshFull() throws IOException
{
254 getTerminal().setForegroundColor(TextColor
.ANSI
.DEFAULT
);
255 getTerminal().setBackgroundColor(TextColor
.ANSI
.DEFAULT
);
256 getTerminal().clearScreen();
257 getTerminal().resetColorAndSGR();
258 scrollHint
= null; // discard any scroll hint for full refresh
260 EnumSet
<SGR
> currentSGR
= EnumSet
.noneOf(SGR
.class);
261 TextColor currentForegroundColor
= TextColor
.ANSI
.DEFAULT
;
262 TextColor currentBackgroundColor
= TextColor
.ANSI
.DEFAULT
;
263 for(int y
= 0; y
< getTerminalSize().getRows(); y
++) {
264 getTerminal().setCursorPosition(0, y
);
265 int currentColumn
= 0;
266 for(int x
= 0; x
< getTerminalSize().getColumns(); x
++) {
267 TextCharacter newCharacter
= getBackBuffer().getCharacterAt(x
, y
);
268 if(newCharacter
.equals(DEFAULT_CHARACTER
)) {
272 if(!currentForegroundColor
.equals(newCharacter
.getForegroundColor())) {
273 getTerminal().setForegroundColor(newCharacter
.getForegroundColor());
274 currentForegroundColor
= newCharacter
.getForegroundColor();
276 if(!currentBackgroundColor
.equals(newCharacter
.getBackgroundColor())) {
277 getTerminal().setBackgroundColor(newCharacter
.getBackgroundColor());
278 currentBackgroundColor
= newCharacter
.getBackgroundColor();
280 for(SGR sgr
: SGR
.values()) {
281 if(currentSGR
.contains(sgr
) && !newCharacter
.getModifiers().contains(sgr
)) {
282 getTerminal().disableSGR(sgr
);
283 currentSGR
.remove(sgr
);
285 else if(!currentSGR
.contains(sgr
) && newCharacter
.getModifiers().contains(sgr
)) {
286 getTerminal().enableSGR(sgr
);
290 if(currentColumn
!= x
) {
291 getTerminal().setCursorPosition(x
, y
);
294 getTerminal().putCharacter(newCharacter
.getCharacter());
295 if(TerminalTextUtils
.isCharCJK(newCharacter
.getCharacter())) {
296 //CJK characters take up two columns
301 //Normal characters take up one column
309 * Returns the underlying {@code Terminal} interface that this Screen is using.
311 * <b>Be aware:</b> directly modifying the underlying terminal will most likely result in unexpected behaviour if
312 * you then go on and try to interact with the Screen. The Screen's back-buffer/front-buffer will not know about
313 * the operations you are going on the Terminal and won't be able to properly generate a refresh unless you enforce
314 * a {@code Screen.RefreshType.COMPLETE}, at which the entire terminal area will be repainted according to the
315 * back-buffer of the {@code Screen}.
316 * @return Underlying terminal used by the screen
318 @SuppressWarnings("WeakerAccess")
319 public Terminal
getTerminal() {
324 public KeyStroke
readInput() throws IOException
{
325 return terminal
.readInput();
329 public KeyStroke
pollInput() throws IOException
{
330 return terminal
.pollInput();
334 public synchronized void clear() {
336 fullRedrawHint
= true;
337 scrollHint
= ScrollHint
.INVALID
;
341 public synchronized TerminalSize
doResizeIfNecessary() {
342 TerminalSize newSize
= super.doResizeIfNecessary();
343 if(newSize
!= null) {
344 fullRedrawHint
= true;
350 * Perform the scrolling and save scroll-range and distance in order
351 * to be able to optimize Terminal-update later.
354 public void scrollLines(int firstLine
, int lastLine
, int distance
) {
355 // just ignore certain kinds of garbage:
356 if (distance
== 0 || firstLine
> lastLine
) { return; }
358 super.scrollLines(firstLine
, lastLine
, distance
);
360 // Save scroll hint for next refresh:
361 ScrollHint newHint
= new ScrollHint(firstLine
,lastLine
,distance
);
362 if (scrollHint
== null) {
363 // no scroll hint yet: use the new one:
364 scrollHint
= newHint
;
365 } else if (scrollHint
== ScrollHint
.INVALID
) {
366 // scroll ranges already inconsistent since latest refresh!
368 } else if (scrollHint
.matches(newHint
)) {
369 // same range: just accumulate distance:
370 scrollHint
.distance
+= newHint
.distance
;
372 // different scroll range: no scroll-optimization for next refresh
373 this.scrollHint
= ScrollHint
.INVALID
;
377 private class TerminalResizeListener
implements ResizeListener
{
379 public void onResized(Terminal terminal
, TerminalSize newSize
) {
380 addResizeRequest(newSize
);
384 private static class ScreenPointComparator
implements Comparator
<TerminalPosition
> {
386 public int compare(TerminalPosition o1
, TerminalPosition o2
) {
387 if(o1
.getRow() == o2
.getRow()) {
388 if(o1
.getColumn() == o2
.getColumn()) {
391 return new Integer(o1
.getColumn()).compareTo(o2
.getColumn());
394 return new Integer(o1
.getRow()).compareTo(o2
.getRow());
399 private static class ScrollHint
{
400 public static final ScrollHint INVALID
= new ScrollHint(-1,-1,0);
401 public int firstLine
, lastLine
, distance
;
403 public ScrollHint(int firstLine
, int lastLine
, int distance
) {
404 this.firstLine
= firstLine
;
405 this.lastLine
= lastLine
;
406 this.distance
= distance
;
409 public boolean matches(ScrollHint other
) {
410 return this.firstLine
== other
.firstLine
411 && this.lastLine
== other
.lastLine
;
414 public void applyTo( Scrollable scr
) throws IOException
{
415 scr
.scrollLines(firstLine
, lastLine
, distance
);