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 scrollback lines.
124 private List
<DisplayLine
> scrollback
;
127 * The last seen display lines.
129 private List
<DisplayLine
> display
;
132 * If true, the display has changed and needs updating.
134 private volatile boolean dirty
= true;
137 * Time that the display was last updated.
139 private long lastUpdateTime
= 0;
142 * If true, hide the mouse after typing a keystroke.
144 private boolean hideMouseWhenTyping
= true;
147 * If true, the mouse should not be displayed because a keystroke was
150 private boolean typingHidMouse
= false;
153 * The return value from the emulator.
155 private int exitValue
= -1;
158 * Title to expose to a window.
160 private String title
= "";
163 * Action to perform when the terminal exits.
165 private TAction closeAction
= null;
167 // ------------------------------------------------------------------------
168 // Constructors -----------------------------------------------------------
169 // ------------------------------------------------------------------------
172 * Public constructor spawns a custom command line.
174 * @param parent parent widget
175 * @param x column relative to parent
176 * @param y row relative to parent
177 * @param commandLine the command line to execute
179 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
180 final String commandLine
) {
182 this(parent
, x
, y
, commandLine
.split("\\s+"));
186 * Public constructor spawns a custom command line.
188 * @param parent parent widget
189 * @param x column relative to parent
190 * @param y row relative to parent
191 * @param command the command line to execute
193 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
194 final String
[] command
) {
196 this(parent
, x
, y
, command
, null);
200 * Public constructor spawns a custom command line.
202 * @param parent parent widget
203 * @param x column relative to parent
204 * @param y row relative to parent
205 * @param command the command line to execute
206 * @param closeAction action to perform when the shell sxits
208 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
209 final String
[] command
, final TAction closeAction
) {
211 this(parent
, x
, y
, 80, 24, command
, closeAction
);
215 * Public constructor spawns a custom command line.
217 * @param parent parent widget
218 * @param x column relative to parent
219 * @param y row relative to parent
220 * @param width width of widget
221 * @param height height of widget
222 * @param command the command line to execute
223 * @param closeAction action to perform when the shell sxits
225 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
226 final int width
, final int height
, final String
[] command
,
227 final TAction closeAction
) {
229 super(parent
, x
, y
, width
, height
);
231 this.closeAction
= closeAction
;
232 if (closeAction
!= null) {
233 this.closeAction
.data
= this;
236 String
[] fullCommand
;
238 // Spawn a shell and pass its I/O to the other constructor.
239 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
240 && (System
.getProperty("jexer.TTerminal.ptypipe").
244 fullCommand
= new String
[command
.length
+ 1];
245 fullCommand
[0] = "ptypipe";
246 System
.arraycopy(command
, 0, fullCommand
, 1, command
.length
);
247 } else if (System
.getProperty("os.name").startsWith("Windows")) {
248 fullCommand
= new String
[3];
249 fullCommand
[0] = "cmd";
250 fullCommand
[1] = "/c";
251 fullCommand
[2] = stringArrayToString(command
);
252 } else if (System
.getProperty("os.name").startsWith("Mac")) {
253 fullCommand
= new String
[6];
254 fullCommand
[0] = "script";
255 fullCommand
[1] = "-q";
256 fullCommand
[2] = "-F";
257 fullCommand
[3] = "/dev/null";
258 fullCommand
[4] = "-c";
259 fullCommand
[5] = stringArrayToString(command
);
261 // Default: behave like Linux
262 fullCommand
= new String
[5];
263 fullCommand
[0] = "script";
264 fullCommand
[1] = "-fqe";
265 fullCommand
[2] = "/dev/null";
266 fullCommand
[3] = "-c";
267 fullCommand
[4] = stringArrayToString(command
);
269 spawnShell(fullCommand
);
273 * Public constructor spawns a shell.
275 * @param parent parent widget
276 * @param x column relative to parent
277 * @param y row relative to parent
279 public TTerminalWidget(final TWidget parent
, final int x
, final int y
) {
280 this(parent
, x
, y
, (TAction
) null);
284 * Public constructor spawns a shell.
286 * @param parent parent widget
287 * @param x column relative to parent
288 * @param y row relative to parent
289 * @param closeAction action to perform when the shell sxits
291 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
292 final TAction closeAction
) {
294 this(parent
, x
, y
, 80, 24, closeAction
);
298 * Public constructor spawns a shell.
300 * @param parent parent widget
301 * @param x column relative to parent
302 * @param y row relative to parent
303 * @param width width of widget
304 * @param height height of widget
305 * @param closeAction action to perform when the shell sxits
307 public TTerminalWidget(final TWidget parent
, final int x
, final int y
,
308 final int width
, final int height
, final TAction closeAction
) {
310 super(parent
, x
, y
, width
, height
);
312 this.closeAction
= closeAction
;
313 if (closeAction
!= null) {
314 this.closeAction
.data
= this;
317 if (System
.getProperty("jexer.TTerminal.shell") != null) {
318 String shell
= System
.getProperty("jexer.TTerminal.shell");
319 if (shell
.trim().startsWith("ptypipe")) {
322 spawnShell(shell
.split("\\s+"));
326 String cmdShellWindows
= "cmd.exe";
328 // You cannot run a login shell in a bare Process interactively, due
329 // to libc's behavior of buffering when stdin/stdout aren't a tty.
330 // Use 'script' instead to run a shell in a pty. And because BSD and
331 // GNU differ on the '-f' vs '-F' flags, we need two different
333 String cmdShellGNU
= "script -fqe /dev/null";
334 String cmdShellBSD
= "script -q -F /dev/null";
336 // ptypipe is another solution that permits dynamic window resizing.
337 String cmdShellPtypipe
= "ptypipe /bin/bash --login";
339 // Spawn a shell and pass its I/O to the other constructor.
340 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
341 && (System
.getProperty("jexer.TTerminal.ptypipe").
345 spawnShell(cmdShellPtypipe
.split("\\s+"));
346 } else if (System
.getProperty("os.name").startsWith("Windows")) {
347 spawnShell(cmdShellWindows
.split("\\s+"));
348 } else if (System
.getProperty("os.name").startsWith("Mac")) {
349 spawnShell(cmdShellBSD
.split("\\s+"));
350 } else if (System
.getProperty("os.name").startsWith("Linux")) {
351 spawnShell(cmdShellGNU
.split("\\s+"));
353 // When all else fails, assume GNU.
354 spawnShell(cmdShellGNU
.split("\\s+"));
358 // ------------------------------------------------------------------------
359 // TScrollableWidget ------------------------------------------------------
360 // ------------------------------------------------------------------------
363 * Draw the display buffer.
367 int width
= getDisplayWidth();
369 boolean syncEmulator
= false;
370 if ((System
.currentTimeMillis() - lastUpdateTime
>= 25)
373 // Too much time has passed, draw it all.
375 } else if (emulator
.isReading() && (dirty
== false)) {
376 // Wait until the emulator has brought more data in.
377 syncEmulator
= false;
378 } else if (!emulator
.isReading() && (dirty
== true)) {
379 // The emulator won't receive more data, update the display.
383 if ((syncEmulator
== true)
384 || (scrollback
== null)
387 // We want to minimize the amount of time we have the emulator
388 // locked. Grab a copy of its display.
389 synchronized (emulator
) {
390 // Update the scroll bars
393 if ((scrollback
== null) || emulator
.isReading()) {
394 scrollback
= copyBuffer(emulator
.getScrollbackBuffer());
395 display
= copyBuffer(emulator
.getDisplayBuffer());
397 width
= emulator
.getWidth();
402 // Put together the visible rows
403 int visibleHeight
= getHeight();
404 int visibleBottom
= scrollback
.size() + display
.size()
405 + getVerticalValue();
406 assert (visibleBottom
>= 0);
408 List
<DisplayLine
> preceedingBlankLines
= new ArrayList
<DisplayLine
>();
409 int visibleTop
= visibleBottom
- visibleHeight
;
410 if (visibleTop
< 0) {
411 for (int i
= visibleTop
; i
< 0; i
++) {
412 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
416 assert (visibleTop
>= 0);
418 List
<DisplayLine
> displayLines
= new ArrayList
<DisplayLine
>();
419 displayLines
.addAll(scrollback
);
420 displayLines
.addAll(display
);
422 List
<DisplayLine
> visibleLines
= new ArrayList
<DisplayLine
>();
423 visibleLines
.addAll(preceedingBlankLines
);
424 visibleLines
.addAll(displayLines
.subList(visibleTop
,
427 visibleHeight
-= visibleLines
.size();
428 assert (visibleHeight
>= 0);
430 // Now draw the emulator screen
432 for (DisplayLine line
: visibleLines
) {
433 int widthMax
= width
;
434 if (line
.isDoubleWidth()) {
437 if (widthMax
> getWidth()) {
438 widthMax
= getWidth();
440 for (int i
= 0; i
< widthMax
; i
++) {
441 Cell ch
= line
.charAt(i
);
444 putCharXY(i
, row
, ch
);
448 Cell newCell
= new Cell(ch
);
449 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
450 newCell
.setReverse(false);
452 if (ch
.getForeColorRGB() < 0) {
453 newCell
.setBackColor(ch
.getForeColor());
454 newCell
.setBackColorRGB(-1);
456 newCell
.setBackColorRGB(ch
.getForeColorRGB());
458 if (ch
.getBackColorRGB() < 0) {
459 newCell
.setForeColor(ch
.getBackColor());
460 newCell
.setForeColorRGB(-1);
462 newCell
.setForeColorRGB(ch
.getBackColorRGB());
465 if (line
.isDoubleWidth()) {
466 putDoubleWidthCharXY(line
, (i
* 2), row
, newCell
);
468 putCharXY(i
, row
, newCell
);
472 if (row
== getHeight()) {
473 // Don't overwrite the box edge
477 CellAttributes background
= new CellAttributes();
478 // Fill in the blank lines on bottom
479 for (int i
= 0; i
< visibleHeight
; i
++) {
480 hLineXY(0, i
+ row
, getWidth(), ' ', background
);
486 * Handle widget close.
489 public void close() {
492 terminateShellChildProcess();
499 * Handle window/screen resize events.
501 * @param resize resize event
504 public void onResize(final TResizeEvent resize
) {
505 // Let TWidget set my size.
506 super.onResize(resize
);
508 // Synchronize against the emulator so we don't stomp on its reader
510 synchronized (emulator
) {
512 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
513 // Resize the scroll bars
517 // Get out of scrollback
521 emulator
.setWidth(getWidth());
522 emulator
.setHeight(getHeight());
524 emulator
.writeRemote("\033[8;" + getHeight() + ";" +
528 // Pass the correct text cell width/height to the emulator
529 if (getScreen() != null) {
530 emulator
.setTextWidth(getScreen().getTextWidth());
531 emulator
.setTextHeight(getScreen().getTextHeight());
536 } // synchronized (emulator)
540 * Resize scrollbars for a new width/height.
543 public void reflowData() {
545 // Synchronize against the emulator so we don't stomp on its reader
547 synchronized (emulator
) {
549 // Pull cursor information
552 // Vertical scrollbar
553 setTopValue(getHeight()
554 - (emulator
.getScrollbackBuffer().size()
555 + emulator
.getDisplayBuffer().size()));
556 setVerticalBigChange(getHeight());
558 } // synchronized (emulator)
564 * @param keypress keystroke event
567 public void onKeypress(final TKeypressEvent keypress
) {
568 if (hideMouseWhenTyping
) {
569 typingHidMouse
= true;
572 // Scrollback up/down
573 if (keypress
.equals(kbShiftPgUp
)
574 || keypress
.equals(kbCtrlPgUp
)
575 || keypress
.equals(kbAltPgUp
)
577 bigVerticalDecrement();
580 if (keypress
.equals(kbShiftPgDn
)
581 || keypress
.equals(kbCtrlPgDn
)
582 || keypress
.equals(kbAltPgDn
)
584 bigVerticalIncrement();
588 if (emulator
.isReading()) {
589 // Get out of scrollback
591 emulator
.addUserEvent(keypress
);
593 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
594 // this is kBEnter then also send kbCtrlJ.
595 if (keypress
.equals(kbEnter
)) {
596 if (System
.getProperty("os.name").startsWith("Windows")
597 && (System
.getProperty("jexer.TTerminal.cmdHack",
598 "true").equals("true"))
600 emulator
.addUserEvent(new TKeypressEvent(kbCtrlJ
));
608 // Process is closed, honor "normal" TUI keystrokes
609 super.onKeypress(keypress
);
613 * Handle mouse press events.
615 * @param mouse mouse button press event
618 public void onMouseDown(final TMouseEvent mouse
) {
619 if (hideMouseWhenTyping
) {
620 typingHidMouse
= false;
623 // If the emulator is tracking mouse buttons, it needs to see wheel
625 if (emulator
.getMouseProtocol() == ECMA48
.MouseProtocol
.OFF
) {
626 if (mouse
.isMouseWheelUp()) {
630 if (mouse
.isMouseWheelDown()) {
635 if (mouseOnEmulator(mouse
)) {
636 emulator
.addUserEvent(mouse
);
641 // Emulator didn't consume it, pass it on
642 super.onMouseDown(mouse
);
646 * Handle mouse release events.
648 * @param mouse mouse button release event
651 public void onMouseUp(final TMouseEvent mouse
) {
652 if (hideMouseWhenTyping
) {
653 typingHidMouse
= false;
656 if (mouseOnEmulator(mouse
)) {
657 emulator
.addUserEvent(mouse
);
662 // Emulator didn't consume it, pass it on
663 super.onMouseUp(mouse
);
667 * Handle mouse motion events.
669 * @param mouse mouse motion event
672 public void onMouseMotion(final TMouseEvent mouse
) {
673 if (hideMouseWhenTyping
) {
674 typingHidMouse
= false;
677 if (mouseOnEmulator(mouse
)) {
678 emulator
.addUserEvent(mouse
);
683 // Emulator didn't consume it, pass it on
684 super.onMouseMotion(mouse
);
687 // ------------------------------------------------------------------------
688 // TTerminalWidget --------------------------------------------------------
689 // ------------------------------------------------------------------------
692 * Get the desired window title.
696 public String
getTitle() {
701 * Returns true if this widget does not want the application-wide mouse
702 * cursor drawn over it.
704 * @return true if this widget does not want the application-wide mouse
705 * cursor drawn over it
707 public boolean hasHiddenMouse() {
708 return (emulator
.hasHiddenMousePointer() || typingHidMouse
);
712 * See if the terminal is still running.
714 * @return if true, we are still connected to / reading from the remote
717 public boolean isReading() {
718 return emulator
.isReading();
722 * Convert a string array to a whitespace-separated string.
724 * @param array the string array
725 * @return a single string
727 private String
stringArrayToString(final String
[] array
) {
728 StringBuilder sb
= new StringBuilder(array
[0].length());
729 for (int i
= 0; i
< array
.length
; i
++) {
731 if (i
< array
.length
- 1) {
735 return sb
.toString();
741 * @param command the command line to execute
743 private void spawnShell(final String
[] command
) {
746 System.err.printf("spawnShell(): '%s'\n",
747 stringArrayToString(command));
750 // We will have vScroller for its data fields and mouse event
751 // handling, but do not want to draw it.
752 vScroller
= new TVScroller(null, getWidth(), 0, getHeight());
753 vScroller
.setVisible(false);
756 title
= i18n
.getString("windowTitle");
759 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
762 ProcessBuilder pb
= new ProcessBuilder(command
);
763 Map
<String
, String
> env
= pb
.environment();
764 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
765 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
766 env
.put("COLUMNS", "80");
767 env
.put("LINES", "24");
768 pb
.redirectErrorStream(true);
770 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
771 shell
.getOutputStream(), this);
772 } catch (IOException e
) {
773 messageBox(i18n
.getString("errorLaunchingShellTitle"),
774 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
778 // Setup the scroll bars
779 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
782 // Hide mouse when typing option
783 if (System
.getProperty("jexer.TTerminal.hideMouseWhenTyping",
784 "true").equals("false")) {
786 hideMouseWhenTyping
= false;
791 * Terminate the child of the 'script' process used on POSIX. This may
794 private void terminateShellChildProcess() {
796 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
797 /* get the PID on unix/linux systems */
799 Field field
= shell
.getClass().getDeclaredField("pid");
800 field
.setAccessible(true);
801 pid
= field
.getInt(shell
);
802 } catch (Throwable e
) {
803 // SQUASH, this didn't work. Just bail out quietly.
808 // shell.destroy() works successfully at killing this side of
809 // 'script'. But we need to make sure the other side (child
810 // process) is also killed.
811 String
[] cmdKillIt
= {
812 "pkill", "-P", Integer
.toString(pid
)
815 Runtime
.getRuntime().exec(cmdKillIt
);
816 } catch (Throwable e
) {
817 // SQUASH, this didn't work. Just bail out quietly.
824 * Hook for subclasses to be notified of the shell termination.
826 public void onShellExit() {
827 TApplication app
= getApplication();
829 app
.invokeLater(new Runnable() {
831 if (closeAction
!= null) {
834 if (getApplication() != null) {
835 getApplication().postEvent(new TMenuEvent(
844 * Copy out variables from the emulator that TTerminal has to expose on
847 private void readEmulatorState() {
848 // Synchronize against the emulator so we don't stomp on its reader
850 synchronized (emulator
) {
852 setCursorX(emulator
.getCursorX());
853 setCursorY(emulator
.getCursorY()
854 + (getHeight() - emulator
.getHeight())
855 - getVerticalValue());
856 setCursorVisible(emulator
.isCursorVisible());
857 if (getCursorX() > getWidth()) {
858 setCursorVisible(false);
860 if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
861 setCursorVisible(false);
863 if (emulator
.getScreenTitle().length() > 0) {
864 // Only update the title if the shell is still alive
866 title
= emulator
.getScreenTitle();
870 // Check to see if the shell has died.
871 if (!emulator
.isReading() && (shell
!= null)) {
873 int rc
= shell
.exitValue();
874 // The emulator exited on its own, all is fine
875 title
= MessageFormat
.format(i18n
.
876 getString("windowTitleCompleted"), title
, rc
);
881 } catch (IllegalThreadStateException e
) {
882 // The emulator thread has exited, but the shell Process
883 // hasn't figured that out yet. Do nothing, we will see
884 // this in a future tick.
886 } else if (emulator
.isReading() && (shell
!= null)) {
887 // The shell might be dead, let's check
889 int rc
= shell
.exitValue();
890 // If we got here, the shell died.
891 title
= MessageFormat
.format(i18n
.
892 getString("windowTitleCompleted"), title
, rc
);
897 } catch (IllegalThreadStateException e
) {
898 // The shell is still running, do nothing.
902 } // synchronized (emulator)
906 * Check if a mouse press/release/motion event coordinate is over the
909 * @param mouse a mouse-based event
910 * @return whether or not the mouse is on the emulator
912 private boolean mouseOnEmulator(final TMouseEvent mouse
) {
914 if (!emulator
.isReading()) {
918 if ((mouse
.getX() >= 0)
919 && (mouse
.getX() < getWidth() - 1)
920 && (mouse
.getY() >= 0)
921 && (mouse
.getY() < getHeight())
929 * Copy a display buffer.
931 * @param buffer the buffer to copy
932 * @return a deep copy of the buffer's data
934 private List
<DisplayLine
> copyBuffer(final List
<DisplayLine
> buffer
) {
935 ArrayList
<DisplayLine
> result
= new ArrayList
<DisplayLine
>(buffer
.size());
936 for (DisplayLine line
: buffer
) {
937 result
.add(new DisplayLine(line
));
943 * Draw glyphs for a double-width or double-height VT100 cell to two
946 * @param line the line this VT100 cell is in
947 * @param x the X position to draw the left half to
948 * @param y the Y position to draw to
949 * @param cell the cell to draw
951 private void putDoubleWidthCharXY(final DisplayLine line
, final int x
,
952 final int y
, final Cell cell
) {
954 int textWidth
= getScreen().getTextWidth();
955 int textHeight
= getScreen().getTextHeight();
956 boolean cursorBlinkVisible
= true;
958 if (getScreen() instanceof SwingTerminal
) {
959 SwingTerminal terminal
= (SwingTerminal
) getScreen();
960 cursorBlinkVisible
= terminal
.getCursorBlinkVisible();
961 } else if (getScreen() instanceof ECMA48Terminal
) {
962 ECMA48Terminal terminal
= (ECMA48Terminal
) getScreen();
964 if (!terminal
.hasSixel()) {
965 // The backend does not have sixel support, draw this as text
967 putCharXY(x
, y
, cell
);
968 putCharXY(x
+ 1, y
, ' ', cell
);
971 cursorBlinkVisible
= blinkState
;
973 // We don't know how to dray glyphs to this screen, draw them as
974 // text and bail out.
975 putCharXY(x
, y
, cell
);
976 putCharXY(x
+ 1, y
, ' ', cell
);
980 if ((textWidth
!= lastTextWidth
) || (textHeight
!= lastTextHeight
)) {
981 // Screen size has changed, reset the font.
982 setupFont(textHeight
);
983 lastTextWidth
= textWidth
;
984 lastTextHeight
= textHeight
;
986 assert (doubleFont
!= null);
989 if (line
.getDoubleHeight() == 1) {
990 // Double-height top half: don't draw the underline.
991 Cell newCell
= new Cell(cell
);
992 newCell
.setUnderline(false);
993 image
= doubleFont
.getImage(newCell
, textWidth
* 2, textHeight
* 2,
996 image
= doubleFont
.getImage(cell
, textWidth
* 2, textHeight
* 2,
1000 // Now that we have the double-wide glyph drawn, copy the right
1001 // pieces of it to the cells.
1002 Cell left
= new Cell(cell
);
1003 Cell right
= new Cell(cell
);
1005 BufferedImage leftImage
= null;
1006 BufferedImage rightImage
= null;
1008 System.err.println("image " + image + " textWidth " + textWidth +
1009 " textHeight " + textHeight);
1012 switch (line
.getDoubleHeight()) {
1014 // Top half double height
1015 leftImage
= image
.getSubimage(0, 0, textWidth
, textHeight
);
1016 rightImage
= image
.getSubimage(textWidth
, 0, textWidth
, textHeight
);
1019 // Bottom half double height
1020 leftImage
= image
.getSubimage(0, textHeight
, textWidth
, textHeight
);
1021 rightImage
= image
.getSubimage(textWidth
, textHeight
,
1022 textWidth
, textHeight
);
1025 // Either single height double-width, or error fallback
1026 BufferedImage wideImage
= new BufferedImage(textWidth
* 2,
1027 textHeight
, BufferedImage
.TYPE_INT_ARGB
);
1028 Graphics2D grWide
= wideImage
.createGraphics();
1029 grWide
.drawImage(image
, 0, 0, wideImage
.getWidth(),
1030 wideImage
.getHeight(), null);
1032 leftImage
= wideImage
.getSubimage(0, 0, textWidth
, textHeight
);
1033 rightImage
= wideImage
.getSubimage(textWidth
, 0, textWidth
,
1037 left
.setImage(leftImage
);
1038 right
.setImage(rightImage
);
1039 // Since we have image data, ditch the character here. Otherwise, a
1040 // drawBoxShadow() over the terminal window will show the characters
1041 // which looks wrong.
1044 putCharXY(x
, y
, left
);
1045 putCharXY(x
+ 1, y
, right
);
1049 * Set up the double-width font.
1051 * @param fontSize the size of font to request for the single-width font.
1052 * The double-width font will be 2x this value.
1054 private void setupFont(final int fontSize
) {
1055 doubleFont
= GlyphMaker
.getInstance(fontSize
* 2);
1057 // Special case: the ECMA48 backend needs to have a timer to drive
1059 if (getScreen() instanceof jexer
.backend
.ECMA48Terminal
) {
1061 // Blink every 500 millis.
1063 getApplication().addTimer(millis
, true,
1066 blinkState
= !blinkState
;
1067 getApplication().doRepaint();
1076 // ------------------------------------------------------------------------
1077 // DisplayListener --------------------------------------------------------
1078 // ------------------------------------------------------------------------
1081 * Called by emulator when fresh data has come in.
1083 public void displayChanged() {
1085 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
1089 * Function to call to obtain the display width.
1091 * @return the number of columns in the display
1093 public int getDisplayWidth() {
1101 * Function to call to obtain the display height.
1103 * @return the number of rows in the display
1105 public int getDisplayHeight() {