2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
32 import java
.awt
.FontMetrics
;
33 import java
.awt
.Graphics2D
;
34 import java
.awt
.image
.BufferedImage
;
36 import java
.io
.InputStream
;
37 import java
.io
.IOException
;
38 import java
.lang
.reflect
.Field
;
39 import java
.text
.MessageFormat
;
40 import java
.util
.ArrayList
;
41 import java
.util
.HashMap
;
42 import java
.util
.List
;
44 import java
.util
.ResourceBundle
;
46 import jexer
.backend
.ECMA48Terminal
;
47 import jexer
.backend
.GlyphMaker
;
48 import jexer
.backend
.MultiScreen
;
49 import jexer
.backend
.SwingTerminal
;
50 import jexer
.bits
.Cell
;
51 import jexer
.bits
.CellAttributes
;
52 import jexer
.event
.TKeypressEvent
;
53 import jexer
.event
.TMenuEvent
;
54 import jexer
.event
.TMouseEvent
;
55 import jexer
.event
.TResizeEvent
;
56 import jexer
.menu
.TMenu
;
57 import jexer
.tterminal
.DisplayLine
;
58 import jexer
.tterminal
.DisplayListener
;
59 import jexer
.tterminal
.ECMA48
;
60 import static jexer
.TKeypress
.*;
63 * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
65 public class TTerminalWidget
extends TScrollableWidget
66 implements DisplayListener
{
71 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TTerminalWidget
.class.getName());
73 // ------------------------------------------------------------------------
74 // Variables --------------------------------------------------------------
75 // ------------------------------------------------------------------------
80 private ECMA48 emulator
;
83 * The Process created by the shell spawning constructor.
85 private Process shell
;
88 * If true, we are using the ptypipe utility to support dynamic window
89 * resizing. ptypipe is available at
90 * https://gitlab.com/klamonte/ptypipe .
92 private boolean ptypipe
= false;
97 private GlyphMaker doubleFont
;
100 * Last text width value.
102 private int lastTextWidth
= -1;
105 * Last text height value.
107 private int lastTextHeight
= -1;
110 * The blink state, used only by ECMA48 backend and when double-width
111 * chars must be drawn.
113 private boolean blinkState
= true;
116 * Timer flag, used only by ECMA48 backend and when double-width chars
119 private boolean haveTimer
= false;
122 * The last seen visible display.
124 private List
<DisplayLine
> display
;
127 * If true, the display has changed and needs updating.
129 private volatile boolean dirty
= true;
132 * Time that the display was last updated.
134 private long lastUpdateTime
= 0;
137 * If true, hide the mouse after typing a keystroke.
139 private boolean hideMouseWhenTyping
= true;
142 * If true, the mouse should not be displayed because a keystroke was
145 private boolean typingHidMouse
= false;
148 * The return value from the emulator.
150 private int exitValue
= -1;
153 * Title to expose to a window.
155 private String title
= "";
158 * Action to perform when the terminal exits.
160 private TAction closeAction
= null;
162 // ------------------------------------------------------------------------
163 // Constructors -----------------------------------------------------------
164 // ------------------------------------------------------------------------
167 * Public constructor spawns a custom command line.
169 * @param parent parent widget
170 * @param x column relative to parent
171 * @param y row relative to parent
172 * @param commandLine the command line to execute
174 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
175 final String commandLine
) {
177 this(parent
, x
, y
, commandLine
.split("\\s+"));
181 * Public constructor spawns a custom command line.
183 * @param parent parent widget
184 * @param x column relative to parent
185 * @param y row relative to parent
186 * @param command the command line to execute
188 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
189 final String
[] command
) {
191 this(parent
, x
, y
, command
, null);
195 * Public constructor spawns a custom command line.
197 * @param parent parent widget
198 * @param x column relative to parent
199 * @param y row relative to parent
200 * @param command the command line to execute
201 * @param closeAction action to perform when the shell sxits
203 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
204 final String
[] command
, final TAction closeAction
) {
206 this(parent
, x
, y
, 80, 24, command
, closeAction
);
210 * Public constructor spawns a custom command line.
212 * @param parent parent widget
213 * @param x column relative to parent
214 * @param y row relative to parent
215 * @param width width of widget
216 * @param height height of widget
217 * @param command the command line to execute
218 * @param closeAction action to perform when the shell sxits
220 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
221 final int width
, final int height
, final String
[] command
,
222 final TAction closeAction
) {
224 super(parent
, x
, y
, width
, height
);
226 this.closeAction
= closeAction
;
228 String
[] fullCommand
;
230 // Spawn a shell and pass its I/O to the other constructor.
231 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
232 && (System
.getProperty("jexer.TTerminal.ptypipe").
236 fullCommand
= new String
[command
.length
+ 1];
237 fullCommand
[0] = "ptypipe";
238 System
.arraycopy(command
, 0, fullCommand
, 1, command
.length
);
239 } else if (System
.getProperty("os.name").startsWith("Windows")) {
240 fullCommand
= new String
[3];
241 fullCommand
[0] = "cmd";
242 fullCommand
[1] = "/c";
243 fullCommand
[2] = stringArrayToString(command
);
244 } else if (System
.getProperty("os.name").startsWith("Mac")) {
245 fullCommand
= new String
[6];
246 fullCommand
[0] = "script";
247 fullCommand
[1] = "-q";
248 fullCommand
[2] = "-F";
249 fullCommand
[3] = "/dev/null";
250 fullCommand
[4] = "-c";
251 fullCommand
[5] = stringArrayToString(command
);
253 // Default: behave like Linux
254 fullCommand
= new String
[5];
255 fullCommand
[0] = "script";
256 fullCommand
[1] = "-fqe";
257 fullCommand
[2] = "/dev/null";
258 fullCommand
[3] = "-c";
259 fullCommand
[4] = stringArrayToString(command
);
261 spawnShell(fullCommand
);
265 * Public constructor spawns a shell.
267 * @param parent parent widget
268 * @param x column relative to parent
269 * @param y row relative to parent
271 public TTerminalWidget(final TWidget parent
, final int x
, final int y
) {
272 this(parent
, x
, y
, (TAction
) null);
276 * Public constructor spawns a shell.
278 * @param parent parent widget
279 * @param x column relative to parent
280 * @param y row relative to parent
281 * @param closeAction action to perform when the shell sxits
283 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
284 final TAction closeAction
) {
286 this(parent
, x
, y
, 80, 24, closeAction
);
290 * Public constructor spawns a shell.
292 * @param parent parent widget
293 * @param x column relative to parent
294 * @param y row relative to parent
295 * @param width width of widget
296 * @param height height of widget
297 * @param closeAction action to perform when the shell sxits
299 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
300 final int width
, final int height
, final TAction closeAction
) {
302 super(parent
, x
, y
, width
, height
);
304 this.closeAction
= closeAction
;
306 if (System
.getProperty("jexer.TTerminal.shell") != null) {
307 String shell
= System
.getProperty("jexer.TTerminal.shell");
308 if (shell
.trim().startsWith("ptypipe")) {
311 spawnShell(shell
.split("\\s+"));
315 String cmdShellWindows
= "cmd.exe";
317 // You cannot run a login shell in a bare Process interactively, due
318 // to libc's behavior of buffering when stdin/stdout aren't a tty.
319 // Use 'script' instead to run a shell in a pty. And because BSD and
320 // GNU differ on the '-f' vs '-F' flags, we need two different
322 String cmdShellGNU
= "script -fqe /dev/null";
323 String cmdShellBSD
= "script -q -F /dev/null";
325 // ptypipe is another solution that permits dynamic window resizing.
326 String cmdShellPtypipe
= "ptypipe /bin/bash --login";
328 // Spawn a shell and pass its I/O to the other constructor.
329 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
330 && (System
.getProperty("jexer.TTerminal.ptypipe").
334 spawnShell(cmdShellPtypipe
.split("\\s+"));
335 } else if (System
.getProperty("os.name").startsWith("Windows")) {
336 spawnShell(cmdShellWindows
.split("\\s+"));
337 } else if (System
.getProperty("os.name").startsWith("Mac")) {
338 spawnShell(cmdShellBSD
.split("\\s+"));
339 } else if (System
.getProperty("os.name").startsWith("Linux")) {
340 spawnShell(cmdShellGNU
.split("\\s+"));
342 // When all else fails, assume GNU.
343 spawnShell(cmdShellGNU
.split("\\s+"));
347 // ------------------------------------------------------------------------
348 // Event handlers ---------------------------------------------------------
349 // ------------------------------------------------------------------------
352 * Handle window/screen resize events.
354 * @param resize resize event
357 public void onResize(final TResizeEvent resize
) {
358 // Let TWidget set my size.
359 super.onResize(resize
);
361 if (emulator
== null) {
365 // Synchronize against the emulator so we don't stomp on its reader
367 synchronized (emulator
) {
369 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
370 // Resize the scroll bars
374 // Get out of scrollback
378 emulator
.setWidth(getWidth());
379 emulator
.setHeight(getHeight());
381 emulator
.writeRemote("\033[8;" + getHeight() + ";" +
385 // Pass the correct text cell width/height to the emulator
386 if (getScreen() != null) {
387 emulator
.setTextWidth(getScreen().getTextWidth());
388 emulator
.setTextHeight(getScreen().getTextHeight());
393 } // synchronized (emulator)
399 * @param keypress keystroke event
402 public void onKeypress(final TKeypressEvent keypress
) {
403 if (hideMouseWhenTyping
) {
404 typingHidMouse
= true;
407 // Scrollback up/down
408 if (keypress
.equals(kbShiftPgUp
)
409 || keypress
.equals(kbCtrlPgUp
)
410 || keypress
.equals(kbAltPgUp
)
412 bigVerticalDecrement();
416 if (keypress
.equals(kbShiftPgDn
)
417 || keypress
.equals(kbCtrlPgDn
)
418 || keypress
.equals(kbAltPgDn
)
420 bigVerticalIncrement();
425 if ((emulator
!= null) && (emulator
.isReading())) {
426 // Get out of scrollback
428 emulator
.addUserEvent(keypress
);
430 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
431 // this is kBEnter then also send kbCtrlJ.
432 if (keypress
.equals(kbEnter
)) {
433 if (System
.getProperty("os.name").startsWith("Windows")
434 && (System
.getProperty("jexer.TTerminal.cmdHack",
435 "true").equals("true"))
437 emulator
.addUserEvent(new TKeypressEvent(kbCtrlJ
));
445 // Process is closed, honor "normal" TUI keystrokes
446 super.onKeypress(keypress
);
450 * Handle mouse press events.
452 * @param mouse mouse button press event
455 public void onMouseDown(final TMouseEvent mouse
) {
456 if (hideMouseWhenTyping
) {
457 typingHidMouse
= false;
460 if (emulator
!= null) {
461 // If the emulator is tracking mouse buttons, it needs to see
463 if (emulator
.getMouseProtocol() == ECMA48
.MouseProtocol
.OFF
) {
464 if (mouse
.isMouseWheelUp()) {
469 if (mouse
.isMouseWheelDown()) {
475 if (mouseOnEmulator(mouse
)) {
476 emulator
.addUserEvent(mouse
);
482 // Emulator didn't consume it, pass it on
483 super.onMouseDown(mouse
);
487 * Handle mouse release events.
489 * @param mouse mouse button release event
492 public void onMouseUp(final TMouseEvent mouse
) {
493 if (hideMouseWhenTyping
) {
494 typingHidMouse
= false;
497 if ((emulator
!= null) && (mouseOnEmulator(mouse
))) {
498 emulator
.addUserEvent(mouse
);
503 // Emulator didn't consume it, pass it on
504 super.onMouseUp(mouse
);
508 * Handle mouse motion events.
510 * @param mouse mouse motion event
513 public void onMouseMotion(final TMouseEvent mouse
) {
514 if (hideMouseWhenTyping
) {
515 typingHidMouse
= false;
518 if ((emulator
!= null) && (mouseOnEmulator(mouse
))) {
519 emulator
.addUserEvent(mouse
);
524 // Emulator didn't consume it, pass it on
525 super.onMouseMotion(mouse
);
528 // ------------------------------------------------------------------------
529 // TScrollableWidget ------------------------------------------------------
530 // ------------------------------------------------------------------------
533 * Draw the display buffer.
537 if (emulator
== null) {
541 int width
= getDisplayWidth();
543 boolean syncEmulator
= false;
544 if ((System
.currentTimeMillis() - lastUpdateTime
>= 20)
547 // Too much time has passed, draw it all.
549 } else if (emulator
.isReading() && (dirty
== false)) {
550 // Wait until the emulator has brought more data in.
551 syncEmulator
= false;
552 } else if (!emulator
.isReading() && (dirty
== true)) {
553 // The emulator won't receive more data, update the display.
557 if ((syncEmulator
== true)
560 // We want to minimize the amount of time we have the emulator
561 // locked. Grab a copy of its display.
562 synchronized (emulator
) {
563 // Update the scroll bars
567 // We lost the connection, onShellExit() called an action
568 // that ultimately removed this widget from the UI
569 // hierarchy, so no one cares if we update the display.
574 if ((display
== null) || emulator
.isReading()) {
575 display
= emulator
.getVisibleDisplay(getHeight(),
576 -getVerticalValue());
577 assert (display
.size() == getHeight());
579 width
= emulator
.getWidth();
584 // Now draw the emulator screen
586 for (DisplayLine line
: display
) {
587 int widthMax
= width
;
588 if (line
.isDoubleWidth()) {
591 if (widthMax
> getWidth()) {
592 widthMax
= getWidth();
594 for (int i
= 0; i
< widthMax
; i
++) {
595 Cell ch
= line
.charAt(i
);
598 putCharXY(i
, row
, ch
);
602 Cell newCell
= new Cell(ch
);
603 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
604 newCell
.setReverse(false);
606 if (ch
.getForeColorRGB() < 0) {
607 newCell
.setBackColor(ch
.getForeColor());
608 newCell
.setBackColorRGB(-1);
610 newCell
.setBackColorRGB(ch
.getForeColorRGB());
612 if (ch
.getBackColorRGB() < 0) {
613 newCell
.setForeColor(ch
.getBackColor());
614 newCell
.setForeColorRGB(-1);
616 newCell
.setForeColorRGB(ch
.getBackColorRGB());
619 if (line
.isDoubleWidth()) {
620 putDoubleWidthCharXY(line
, (i
* 2), row
, newCell
);
622 putCharXY(i
, row
, newCell
);
630 * Set current value of the vertical scroll.
632 * @param value the new scroll value
635 public void setVerticalValue(final int value
) {
636 super.setVerticalValue(value
);
641 * Perform a small step change up.
644 public void verticalDecrement() {
645 super.verticalDecrement();
650 * Perform a small step change down.
653 public void verticalIncrement() {
654 super.verticalIncrement();
659 * Perform a big step change up.
661 public void bigVerticalDecrement() {
662 super.bigVerticalDecrement();
667 * Perform a big step change down.
669 public void bigVerticalIncrement() {
670 super.bigVerticalIncrement();
675 * Go to the top edge of the vertical scroller.
677 public void toTop() {
683 * Go to the bottom edge of the vertical scroller.
685 public void toBottom() {
691 * Handle widget close.
694 public void close() {
695 if (emulator
!= null) {
699 terminateShellChildProcess();
706 * Resize scrollbars for a new width/height.
709 public void reflowData() {
710 if (emulator
== null) {
714 // Synchronize against the emulator so we don't stomp on its reader
716 synchronized (emulator
) {
718 // Pull cursor information
721 // Vertical scrollbar
722 setTopValue(getHeight()
723 - (emulator
.getScrollbackBuffer().size()
724 + emulator
.getDisplayBuffer().size()));
725 setVerticalBigChange(getHeight());
727 } // synchronized (emulator)
730 // ------------------------------------------------------------------------
731 // TTerminalWidget --------------------------------------------------------
732 // ------------------------------------------------------------------------
735 * Get the desired window title.
739 public String
getTitle() {
744 * Returns true if this widget does not want the application-wide mouse
745 * cursor drawn over it.
747 * @return true if this widget does not want the application-wide mouse
748 * cursor drawn over it
750 public boolean hasHiddenMouse() {
751 if (emulator
== null) {
754 return (emulator
.hasHiddenMousePointer() || typingHidMouse
);
758 * See if the terminal is still running.
760 * @return if true, we are still connected to / reading from the remote
763 public boolean isReading() {
764 if (emulator
== null) {
767 return emulator
.isReading();
771 * Convert a string array to a whitespace-separated string.
773 * @param array the string array
774 * @return a single string
776 private String
stringArrayToString(final String
[] array
) {
777 StringBuilder sb
= new StringBuilder(array
[0].length());
778 for (int i
= 0; i
< array
.length
; i
++) {
780 if (i
< array
.length
- 1) {
784 return sb
.toString();
790 * @param command the command line to execute
792 private void spawnShell(final String
[] command
) {
795 System.err.printf("spawnShell(): '%s'\n",
796 stringArrayToString(command));
799 // We will have vScroller for its data fields and mouse event
800 // handling, but do not want to draw it.
801 vScroller
= new TVScroller(null, getWidth(), 0, getHeight());
802 vScroller
.setVisible(false);
805 title
= i18n
.getString("windowTitle");
808 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
811 ProcessBuilder pb
= new ProcessBuilder(command
);
812 Map
<String
, String
> env
= pb
.environment();
813 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
814 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
815 env
.put("COLUMNS", "80");
816 env
.put("LINES", "24");
817 pb
.redirectErrorStream(true);
819 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
820 shell
.getOutputStream(), this);
821 } catch (IOException e
) {
822 messageBox(i18n
.getString("errorLaunchingShellTitle"),
823 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
827 // Setup the scroll bars
828 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
831 // Hide mouse when typing option
832 if (System
.getProperty("jexer.TTerminal.hideMouseWhenTyping",
833 "true").equals("false")) {
835 hideMouseWhenTyping
= false;
840 * Terminate the child of the 'script' process used on POSIX. This may
843 private void terminateShellChildProcess() {
845 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
846 /* get the PID on unix/linux systems */
848 Field field
= shell
.getClass().getDeclaredField("pid");
849 field
.setAccessible(true);
850 pid
= field
.getInt(shell
);
851 } catch (Throwable e
) {
852 // SQUASH, this didn't work. Just bail out quietly.
857 // shell.destroy() works successfully at killing this side of
858 // 'script'. But we need to make sure the other side (child
859 // process) is also killed.
860 String
[] cmdKillIt
= {
861 "pkill", "-P", Integer
.toString(pid
)
864 Runtime
.getRuntime().exec(cmdKillIt
);
865 } catch (Throwable e
) {
866 // SQUASH, this didn't work. Just bail out quietly.
873 * Hook for subclasses to be notified of the shell termination.
875 public void onShellExit() {
876 TApplication app
= getApplication();
878 if (closeAction
!= null) {
879 // We have to put this action inside invokeLater() because it
880 // could be executed during draw() when syncing with ECMA48.
881 app
.invokeLater(new Runnable() {
883 closeAction
.DO(TTerminalWidget
.this);
887 if (getApplication() != null) {
888 getApplication().postEvent(new TMenuEvent(
895 * Copy out variables from the emulator that TTerminal has to expose on
898 private void readEmulatorState() {
899 if (emulator
== null) {
903 // Synchronize against the emulator so we don't stomp on its reader
905 synchronized (emulator
) {
907 setCursorX(emulator
.getCursorX());
908 setCursorY(emulator
.getCursorY()
909 + (getHeight() - emulator
.getHeight())
910 - getVerticalValue());
911 setCursorVisible(emulator
.isCursorVisible());
912 if (getCursorX() > getWidth()) {
913 setCursorVisible(false);
915 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
916 setCursorVisible(false);
918 if (emulator
.getScreenTitle().length() > 0) {
919 // Only update the title if the shell is still alive
921 title
= emulator
.getScreenTitle();
925 // Check to see if the shell has died.
926 if (!emulator
.isReading() && (shell
!= null)) {
928 int rc
= shell
.exitValue();
929 // The emulator exited on its own, all is fine
930 title
= MessageFormat
.format(i18n
.
931 getString("windowTitleCompleted"), title
, rc
);
936 } catch (IllegalThreadStateException e
) {
937 // The emulator thread has exited, but the shell Process
938 // hasn't figured that out yet. Do nothing, we will see
939 // this in a future tick.
941 } else if (emulator
.isReading() && (shell
!= null)) {
942 // The shell might be dead, let's check
944 int rc
= shell
.exitValue();
945 // If we got here, the shell died.
946 title
= MessageFormat
.format(i18n
.
947 getString("windowTitleCompleted"), title
, rc
);
952 } catch (IllegalThreadStateException e
) {
953 // The shell is still running, do nothing.
957 } // synchronized (emulator)
961 * Check if a mouse press/release/motion event coordinate is over the
964 * @param mouse a mouse-based event
965 * @return whether or not the mouse is on the emulator
967 private boolean mouseOnEmulator(final TMouseEvent mouse
) {
968 if (emulator
== null) {
972 if (!emulator
.isReading()) {
976 if ((mouse
.getX() >= 0)
977 && (mouse
.getX() < getWidth() - 1)
978 && (mouse
.getY() >= 0)
979 && (mouse
.getY() < getHeight())
987 * Draw glyphs for a double-width or double-height VT100 cell to two
990 * @param line the line this VT100 cell is in
991 * @param x the X position to draw the left half to
992 * @param y the Y position to draw to
993 * @param cell the cell to draw
995 private void putDoubleWidthCharXY(final DisplayLine line
, final int x
,
996 final int y
, final Cell cell
) {
998 int textWidth
= getScreen().getTextWidth();
999 int textHeight
= getScreen().getTextHeight();
1000 boolean cursorBlinkVisible
= true;
1002 if (getScreen() instanceof SwingTerminal
) {
1003 SwingTerminal terminal
= (SwingTerminal
) getScreen();
1004 cursorBlinkVisible
= terminal
.getCursorBlinkVisible();
1005 } else if (getScreen() instanceof ECMA48Terminal
) {
1006 ECMA48Terminal terminal
= (ECMA48Terminal
) getScreen();
1008 if (!terminal
.hasSixel()) {
1009 // The backend does not have sixel support, draw this as text
1011 putCharXY(x
, y
, cell
);
1012 putCharXY(x
+ 1, y
, ' ', cell
);
1015 cursorBlinkVisible
= blinkState
;
1017 // We don't know how to dray glyphs to this screen, draw them as
1018 // text and bail out.
1019 putCharXY(x
, y
, cell
);
1020 putCharXY(x
+ 1, y
, ' ', cell
);
1024 if ((textWidth
!= lastTextWidth
) || (textHeight
!= lastTextHeight
)) {
1025 // Screen size has changed, reset the font.
1026 setupFont(textHeight
);
1027 lastTextWidth
= textWidth
;
1028 lastTextHeight
= textHeight
;
1030 assert (doubleFont
!= null);
1032 BufferedImage image
;
1033 if (line
.getDoubleHeight() == 1) {
1034 // Double-height top half: don't draw the underline.
1035 Cell newCell
= new Cell(cell
);
1036 newCell
.setUnderline(false);
1037 image
= doubleFont
.getImage(newCell
, textWidth
* 2, textHeight
* 2,
1038 cursorBlinkVisible
);
1040 image
= doubleFont
.getImage(cell
, textWidth
* 2, textHeight
* 2,
1041 cursorBlinkVisible
);
1044 // Now that we have the double-wide glyph drawn, copy the right
1045 // pieces of it to the cells.
1046 Cell left
= new Cell(cell
);
1047 Cell right
= new Cell(cell
);
1049 BufferedImage leftImage
= null;
1050 BufferedImage rightImage
= null;
1052 System.err.println("image " + image + " textWidth " + textWidth +
1053 " textHeight " + textHeight);
1056 switch (line
.getDoubleHeight()) {
1058 // Top half double height
1059 leftImage
= image
.getSubimage(0, 0, textWidth
, textHeight
);
1060 rightImage
= image
.getSubimage(textWidth
, 0, textWidth
, textHeight
);
1063 // Bottom half double height
1064 leftImage
= image
.getSubimage(0, textHeight
, textWidth
, textHeight
);
1065 rightImage
= image
.getSubimage(textWidth
, textHeight
,
1066 textWidth
, textHeight
);
1069 // Either single height double-width, or error fallback
1070 BufferedImage wideImage
= new BufferedImage(textWidth
* 2,
1071 textHeight
, BufferedImage
.TYPE_INT_ARGB
);
1072 Graphics2D grWide
= wideImage
.createGraphics();
1073 grWide
.drawImage(image
, 0, 0, wideImage
.getWidth(),
1074 wideImage
.getHeight(), null);
1076 leftImage
= wideImage
.getSubimage(0, 0, textWidth
, textHeight
);
1077 rightImage
= wideImage
.getSubimage(textWidth
, 0, textWidth
,
1081 left
.setImage(leftImage
);
1082 right
.setImage(rightImage
);
1083 // Since we have image data, ditch the character here. Otherwise, a
1084 // drawBoxShadow() over the terminal window will show the characters
1085 // which looks wrong.
1088 putCharXY(x
, y
, left
);
1089 putCharXY(x
+ 1, y
, right
);
1093 * Set up the double-width font.
1095 * @param fontSize the size of font to request for the single-width font.
1096 * The double-width font will be 2x this value.
1098 private void setupFont(final int fontSize
) {
1099 doubleFont
= GlyphMaker
.getInstance(fontSize
* 2);
1101 // Special case: the ECMA48 backend needs to have a timer to drive
1103 if (getScreen() instanceof jexer
.backend
.ECMA48Terminal
) {
1105 // Blink every 500 millis.
1107 getApplication().addTimer(millis
, true,
1110 blinkState
= !blinkState
;
1111 getApplication().doRepaint();
1120 // ------------------------------------------------------------------------
1121 // DisplayListener --------------------------------------------------------
1122 // ------------------------------------------------------------------------
1125 * Called by emulator when fresh data has come in.
1127 public void displayChanged() {
1129 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
1133 * Function to call to obtain the display width.
1135 * @return the number of columns in the display
1137 public int getDisplayWidth() {
1145 * Function to call to obtain the display height.
1147 * @return the number of rows in the display
1149 public int getDisplayHeight() {