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
.MultiScreen
;
48 import jexer
.backend
.SwingTerminal
;
49 import jexer
.bits
.Cell
;
50 import jexer
.bits
.CellAttributes
;
51 import jexer
.event
.TKeypressEvent
;
52 import jexer
.event
.TMenuEvent
;
53 import jexer
.event
.TMouseEvent
;
54 import jexer
.event
.TResizeEvent
;
55 import jexer
.menu
.TMenu
;
56 import jexer
.tterminal
.DisplayLine
;
57 import jexer
.tterminal
.DisplayListener
;
58 import jexer
.tterminal
.ECMA48
;
59 import static jexer
.TKeypress
.*;
62 * TTerminalWindow exposes a ECMA-48 / ANSI X3.64 style terminal in a window.
64 public class TTerminalWindow
extends TScrollableWindow
65 implements DisplayListener
{
70 private static final ResourceBundle i18n
= ResourceBundle
.getBundle(TTerminalWindow
.class.getName());
72 // ------------------------------------------------------------------------
73 // Variables --------------------------------------------------------------
74 // ------------------------------------------------------------------------
79 private ECMA48 emulator
;
82 * The Process created by the shell spawning constructor.
84 private Process shell
;
87 * If true, we are using the ptypipe utility to support dynamic window
88 * resizing. ptypipe is available at
89 * https://gitlab.com/klamonte/ptypipe .
91 private boolean ptypipe
= false;
94 * If true, close the window when the shell exits.
96 private boolean closeOnExit
= false;
99 * System-dependent Y adjustment for text in the character cell
102 private int doubleTextAdjustY
= 0;
105 * System-dependent X adjustment for text in the character cell
108 private int doubleTextAdjustX
= 0;
111 * Descent of a character cell in pixels (double-height).
113 private int doubleMaxDescent
= 0;
118 private Font doubleFont
= null;
121 * Last text width value.
123 private int lastTextWidth
= -1;
126 * Last text height value.
128 private int lastTextHeight
= -1;
131 * A cache of previously-rendered double-width glyphs.
133 private Map
<Cell
, BufferedImage
> glyphCache
;
136 * A cache of previously-rendered double-width glyphs for blinking text,
137 * when it is not visible.
139 private Map
<Cell
, BufferedImage
> glyphCacheBlink
;
142 * The blink state, used only by ECMA48 backend and when double-width
143 * chars must be drawn.
145 private boolean blinkState
= true;
147 // ------------------------------------------------------------------------
148 // Constructors -----------------------------------------------------------
149 // ------------------------------------------------------------------------
152 * Public constructor spawns a custom command line.
154 * @param application TApplication that manages this window
155 * @param x column relative to parent
156 * @param y row relative to parent
157 * @param commandLine the command line to execute
159 public TTerminalWindow(final TApplication application
, final int x
,
160 final int y
, final String commandLine
) {
162 this(application
, x
, y
, RESIZABLE
, commandLine
.split("\\s+"),
163 System
.getProperty("jexer.TTerminal.closeOnExit",
164 "false").equals("true"));
168 * Public constructor spawns a custom command line.
170 * @param application TApplication that manages this window
171 * @param x column relative to parent
172 * @param y row relative to parent
173 * @param commandLine the command line to execute
174 * @param closeOnExit if true, close the window when the command exits
176 public TTerminalWindow(final TApplication application
, final int x
,
177 final int y
, final String commandLine
, final boolean closeOnExit
) {
179 this(application
, x
, y
, RESIZABLE
, commandLine
.split("\\s+"),
184 * Public constructor spawns a custom command line.
186 * @param application TApplication that manages this window
187 * @param x column relative to parent
188 * @param y row relative to parent
189 * @param flags mask of CENTERED, MODAL, or RESIZABLE
190 * @param command the command line to execute
192 public TTerminalWindow(final TApplication application
, final int x
,
193 final int y
, final int flags
, final String
[] command
) {
195 this(application
, x
, y
, flags
, command
,
196 System
.getProperty("jexer.TTerminal.closeOnExit",
197 "false").equals("true"));
201 * Public constructor spawns a custom command line.
203 * @param application TApplication that manages this window
204 * @param x column relative to parent
205 * @param y row relative to parent
206 * @param flags mask of CENTERED, MODAL, or RESIZABLE
207 * @param command the command line to execute
208 * @param closeOnExit if true, close the window when the command exits
210 public TTerminalWindow(final TApplication application
, final int x
,
211 final int y
, final int flags
, final String
[] command
,
212 final boolean closeOnExit
) {
214 super(application
, i18n
.getString("windowTitle"), x
, y
,
215 80 + 2, 24 + 2, flags
);
217 this.closeOnExit
= closeOnExit
;
219 String
[] fullCommand
;
221 // Spawn a shell and pass its I/O to the other constructor.
222 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
223 && (System
.getProperty("jexer.TTerminal.ptypipe").
227 fullCommand
= new String
[command
.length
+ 1];
228 fullCommand
[0] = "ptypipe";
229 System
.arraycopy(command
, 0, fullCommand
, 1, command
.length
);
230 } else if (System
.getProperty("os.name").startsWith("Windows")) {
231 fullCommand
= new String
[3];
232 fullCommand
[0] = "cmd";
233 fullCommand
[1] = "/c";
234 fullCommand
[2] = stringArrayToString(command
);
235 } else if (System
.getProperty("os.name").startsWith("Mac")) {
236 fullCommand
= new String
[6];
237 fullCommand
[0] = "script";
238 fullCommand
[1] = "-q";
239 fullCommand
[2] = "-F";
240 fullCommand
[3] = "/dev/null";
241 fullCommand
[4] = "-c";
242 fullCommand
[5] = stringArrayToString(command
);
244 // Default: behave like Linux
245 fullCommand
= new String
[5];
246 fullCommand
[0] = "script";
247 fullCommand
[1] = "-fqe";
248 fullCommand
[2] = "/dev/null";
249 fullCommand
[3] = "-c";
250 fullCommand
[4] = stringArrayToString(command
);
252 spawnShell(fullCommand
);
256 * Public constructor spawns a shell.
258 * @param application TApplication that manages this window
259 * @param x column relative to parent
260 * @param y row relative to parent
261 * @param flags mask of CENTERED, MODAL, or RESIZABLE
263 public TTerminalWindow(final TApplication application
, final int x
,
264 final int y
, final int flags
) {
266 this(application
, x
, y
, flags
,
267 System
.getProperty("jexer.TTerminal.closeOnExit",
268 "false").equals("true"));
273 * Public constructor spawns a shell.
275 * @param application TApplication that manages this window
276 * @param x column relative to parent
277 * @param y row relative to parent
278 * @param flags mask of CENTERED, MODAL, or RESIZABLE
279 * @param closeOnExit if true, close the window when the shell exits
281 public TTerminalWindow(final TApplication application
, final int x
,
282 final int y
, final int flags
, final boolean closeOnExit
) {
284 super(application
, i18n
.getString("windowTitle"), x
, y
,
285 80 + 2, 24 + 2, flags
);
287 this.closeOnExit
= closeOnExit
;
289 String cmdShellWindows
= "cmd.exe";
291 // You cannot run a login shell in a bare Process interactively, due
292 // to libc's behavior of buffering when stdin/stdout aren't a tty.
293 // Use 'script' instead to run a shell in a pty. And because BSD and
294 // GNU differ on the '-f' vs '-F' flags, we need two different
296 String cmdShellGNU
= "script -fqe /dev/null";
297 String cmdShellBSD
= "script -q -F /dev/null";
299 // ptypipe is another solution that permits dynamic window resizing.
300 String cmdShellPtypipe
= "ptypipe /bin/bash --login";
302 // Spawn a shell and pass its I/O to the other constructor.
303 if ((System
.getProperty("jexer.TTerminal.ptypipe") != null)
304 && (System
.getProperty("jexer.TTerminal.ptypipe").
308 spawnShell(cmdShellPtypipe
.split("\\s+"));
309 } else if (System
.getProperty("os.name").startsWith("Windows")) {
310 spawnShell(cmdShellWindows
.split("\\s+"));
311 } else if (System
.getProperty("os.name").startsWith("Mac")) {
312 spawnShell(cmdShellBSD
.split("\\s+"));
313 } else if (System
.getProperty("os.name").startsWith("Linux")) {
314 spawnShell(cmdShellGNU
.split("\\s+"));
316 // When all else fails, assume GNU.
317 spawnShell(cmdShellGNU
.split("\\s+"));
321 // ------------------------------------------------------------------------
322 // TScrollableWindow ------------------------------------------------------
323 // ------------------------------------------------------------------------
326 * Draw the display buffer.
330 // Synchronize against the emulator so we don't stomp on its reader
332 synchronized (emulator
) {
334 // Update the scroll bars
337 // Draw the box using my superclass
340 List
<DisplayLine
> scrollback
= emulator
.getScrollbackBuffer();
341 List
<DisplayLine
> display
= emulator
.getDisplayBuffer();
343 // Put together the visible rows
344 int visibleHeight
= getHeight() - 2;
345 int visibleBottom
= scrollback
.size() + display
.size()
346 + getVerticalValue();
347 assert (visibleBottom
>= 0);
349 List
<DisplayLine
> preceedingBlankLines
= new ArrayList
<DisplayLine
>();
350 int visibleTop
= visibleBottom
- visibleHeight
;
351 if (visibleTop
< 0) {
352 for (int i
= visibleTop
; i
< 0; i
++) {
353 preceedingBlankLines
.add(emulator
.getBlankDisplayLine());
357 assert (visibleTop
>= 0);
359 List
<DisplayLine
> displayLines
= new ArrayList
<DisplayLine
>();
360 displayLines
.addAll(scrollback
);
361 displayLines
.addAll(display
);
363 List
<DisplayLine
> visibleLines
= new ArrayList
<DisplayLine
>();
364 visibleLines
.addAll(preceedingBlankLines
);
365 visibleLines
.addAll(displayLines
.subList(visibleTop
,
368 visibleHeight
-= visibleLines
.size();
369 assert (visibleHeight
>= 0);
371 // Now draw the emulator screen
373 for (DisplayLine line
: visibleLines
) {
374 int widthMax
= emulator
.getWidth();
375 if (line
.isDoubleWidth()) {
378 if (widthMax
> getWidth() - 2) {
379 widthMax
= getWidth() - 2;
381 for (int i
= 0; i
< widthMax
; i
++) {
382 Cell ch
= line
.charAt(i
);
383 Cell newCell
= new Cell();
385 boolean reverse
= line
.isReverseColor() ^ ch
.isReverse();
386 newCell
.setReverse(false);
388 if (ch
.getForeColorRGB() < 0) {
389 newCell
.setBackColor(ch
.getForeColor());
390 newCell
.setBackColorRGB(-1);
392 newCell
.setBackColorRGB(ch
.getForeColorRGB());
394 if (ch
.getBackColorRGB() < 0) {
395 newCell
.setForeColor(ch
.getBackColor());
396 newCell
.setForeColorRGB(-1);
398 newCell
.setForeColorRGB(ch
.getBackColorRGB());
401 if (line
.isDoubleWidth()) {
402 putDoubleWidthCharXY(line
, (i
* 2) + 1, row
, newCell
);
404 putCharXY(i
+ 1, row
, newCell
);
408 if (row
== getHeight() - 1) {
409 // Don't overwrite the box edge
413 CellAttributes background
= new CellAttributes();
414 // Fill in the blank lines on bottom
415 for (int i
= 0; i
< visibleHeight
; i
++) {
416 hLineXY(1, i
+ row
, getWidth() - 2, ' ', background
);
419 } // synchronized (emulator)
424 * Handle window close.
427 public void onClose() {
430 terminateShellChildProcess();
437 * Handle window/screen resize events.
439 * @param resize resize event
442 public void onResize(final TResizeEvent resize
) {
444 // Synchronize against the emulator so we don't stomp on its reader
446 synchronized (emulator
) {
448 if (resize
.getType() == TResizeEvent
.Type
.WIDGET
) {
449 // Resize the scroll bars
453 // Get out of scrollback
457 emulator
.setWidth(getWidth() - 2);
458 emulator
.setHeight(getHeight() - 2);
460 emulator
.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
461 (getWidth() - 2) + "t");
466 } // synchronized (emulator)
470 * Resize scrollbars for a new width/height.
473 public void reflowData() {
475 // Synchronize against the emulator so we don't stomp on its reader
477 synchronized (emulator
) {
479 // Pull cursor information
482 // Vertical scrollbar
483 setTopValue(getHeight() - 2
484 - (emulator
.getScrollbackBuffer().size()
485 + emulator
.getDisplayBuffer().size()));
486 setVerticalBigChange(getHeight() - 2);
488 } // synchronized (emulator)
494 * @param keypress keystroke event
497 public void onKeypress(final TKeypressEvent keypress
) {
499 // Scrollback up/down
500 if (keypress
.equals(kbShiftPgUp
)
501 || keypress
.equals(kbCtrlPgUp
)
502 || keypress
.equals(kbAltPgUp
)
504 bigVerticalDecrement();
507 if (keypress
.equals(kbShiftPgDn
)
508 || keypress
.equals(kbCtrlPgDn
)
509 || keypress
.equals(kbAltPgDn
)
511 bigVerticalIncrement();
515 // Synchronize against the emulator so we don't stomp on its reader
517 synchronized (emulator
) {
518 if (emulator
.isReading()) {
519 // Get out of scrollback
521 emulator
.keypress(keypress
.getKey());
523 // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
524 // this is kBEnter then also send kbCtrlJ.
525 if (System
.getProperty("os.name").startsWith("Windows")) {
526 if (keypress
.equals(kbEnter
)) {
527 emulator
.keypress(kbCtrlJ
);
536 // Process is closed, honor "normal" TUI keystrokes
537 super.onKeypress(keypress
);
541 * Handle mouse press events.
543 * @param mouse mouse button press event
546 public void onMouseDown(final TMouseEvent mouse
) {
547 if (inWindowMove
|| inWindowResize
) {
548 // TWindow needs to deal with this.
549 super.onMouseDown(mouse
);
553 // If the emulator is tracking mouse buttons, it needs to see wheel
555 if (emulator
.getMouseProtocol() == ECMA48
.MouseProtocol
.OFF
) {
556 if (mouse
.isMouseWheelUp()) {
560 if (mouse
.isMouseWheelDown()) {
565 if (mouseOnEmulator(mouse
)) {
566 synchronized (emulator
) {
567 mouse
.setX(mouse
.getX() - 1);
568 mouse
.setY(mouse
.getY() - 1);
569 emulator
.mouse(mouse
);
575 // Emulator didn't consume it, pass it on
576 super.onMouseDown(mouse
);
580 * Handle mouse release events.
582 * @param mouse mouse button release event
585 public void onMouseUp(final TMouseEvent mouse
) {
586 if (inWindowMove
|| inWindowResize
) {
587 // TWindow needs to deal with this.
588 super.onMouseUp(mouse
);
592 if (mouseOnEmulator(mouse
)) {
593 synchronized (emulator
) {
594 mouse
.setX(mouse
.getX() - 1);
595 mouse
.setY(mouse
.getY() - 1);
596 emulator
.mouse(mouse
);
602 // Emulator didn't consume it, pass it on
603 super.onMouseUp(mouse
);
607 * Handle mouse motion events.
609 * @param mouse mouse motion event
612 public void onMouseMotion(final TMouseEvent mouse
) {
613 if (inWindowMove
|| inWindowResize
) {
614 // TWindow needs to deal with this.
615 super.onMouseMotion(mouse
);
619 if (mouseOnEmulator(mouse
)) {
620 synchronized (emulator
) {
621 mouse
.setX(mouse
.getX() - 1);
622 mouse
.setY(mouse
.getY() - 1);
623 emulator
.mouse(mouse
);
629 // Emulator didn't consume it, pass it on
630 super.onMouseMotion(mouse
);
633 // ------------------------------------------------------------------------
634 // TTerminalWindow --------------------------------------------------------
635 // ------------------------------------------------------------------------
638 * Claim the keystrokes the emulator will need.
640 private void addShortcutKeys() {
641 addShortcutKeypress(kbCtrlA
);
642 addShortcutKeypress(kbCtrlB
);
643 addShortcutKeypress(kbCtrlC
);
644 addShortcutKeypress(kbCtrlD
);
645 addShortcutKeypress(kbCtrlE
);
646 addShortcutKeypress(kbCtrlF
);
647 addShortcutKeypress(kbCtrlG
);
648 addShortcutKeypress(kbCtrlH
);
649 addShortcutKeypress(kbCtrlU
);
650 addShortcutKeypress(kbCtrlJ
);
651 addShortcutKeypress(kbCtrlK
);
652 addShortcutKeypress(kbCtrlL
);
653 addShortcutKeypress(kbCtrlM
);
654 addShortcutKeypress(kbCtrlN
);
655 addShortcutKeypress(kbCtrlO
);
656 addShortcutKeypress(kbCtrlP
);
657 addShortcutKeypress(kbCtrlQ
);
658 addShortcutKeypress(kbCtrlR
);
659 addShortcutKeypress(kbCtrlS
);
660 addShortcutKeypress(kbCtrlT
);
661 addShortcutKeypress(kbCtrlU
);
662 addShortcutKeypress(kbCtrlV
);
663 addShortcutKeypress(kbCtrlW
);
664 addShortcutKeypress(kbCtrlX
);
665 addShortcutKeypress(kbCtrlY
);
666 addShortcutKeypress(kbCtrlZ
);
667 addShortcutKeypress(kbF1
);
668 addShortcutKeypress(kbF2
);
669 addShortcutKeypress(kbF3
);
670 addShortcutKeypress(kbF4
);
671 addShortcutKeypress(kbF5
);
672 addShortcutKeypress(kbF6
);
673 addShortcutKeypress(kbF7
);
674 addShortcutKeypress(kbF8
);
675 addShortcutKeypress(kbF9
);
676 addShortcutKeypress(kbF10
);
677 addShortcutKeypress(kbF11
);
678 addShortcutKeypress(kbF12
);
679 addShortcutKeypress(kbAltA
);
680 addShortcutKeypress(kbAltB
);
681 addShortcutKeypress(kbAltC
);
682 addShortcutKeypress(kbAltD
);
683 addShortcutKeypress(kbAltE
);
684 addShortcutKeypress(kbAltF
);
685 addShortcutKeypress(kbAltG
);
686 addShortcutKeypress(kbAltH
);
687 addShortcutKeypress(kbAltU
);
688 addShortcutKeypress(kbAltJ
);
689 addShortcutKeypress(kbAltK
);
690 addShortcutKeypress(kbAltL
);
691 addShortcutKeypress(kbAltM
);
692 addShortcutKeypress(kbAltN
);
693 addShortcutKeypress(kbAltO
);
694 addShortcutKeypress(kbAltP
);
695 addShortcutKeypress(kbAltQ
);
696 addShortcutKeypress(kbAltR
);
697 addShortcutKeypress(kbAltS
);
698 addShortcutKeypress(kbAltT
);
699 addShortcutKeypress(kbAltU
);
700 addShortcutKeypress(kbAltV
);
701 addShortcutKeypress(kbAltW
);
702 addShortcutKeypress(kbAltX
);
703 addShortcutKeypress(kbAltY
);
704 addShortcutKeypress(kbAltZ
);
708 * Convert a string array to a whitespace-separated string.
710 * @param array the string array
711 * @return a single string
713 private String
stringArrayToString(final String
[] array
) {
714 StringBuilder sb
= new StringBuilder(array
[0].length());
715 for (int i
= 0; i
< array
.length
; i
++) {
717 if (i
< array
.length
- 1) {
721 return sb
.toString();
727 * @param command the command line to execute
729 private void spawnShell(final String
[] command
) {
732 System.err.printf("spawnShell(): '%s'\n",
733 stringArrayToString(command));
736 vScroller
= new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
740 ECMA48
.DeviceType deviceType
= ECMA48
.DeviceType
.XTERM
;
743 ProcessBuilder pb
= new ProcessBuilder(command
);
744 Map
<String
, String
> env
= pb
.environment();
745 env
.put("TERM", ECMA48
.deviceTypeTerm(deviceType
));
746 env
.put("LANG", ECMA48
.deviceTypeLang(deviceType
, "en"));
747 env
.put("COLUMNS", "80");
748 env
.put("LINES", "24");
749 pb
.redirectErrorStream(true);
751 emulator
= new ECMA48(deviceType
, shell
.getInputStream(),
752 shell
.getOutputStream(), this);
753 } catch (IOException e
) {
754 messageBox(i18n
.getString("errorLaunchingShellTitle"),
755 MessageFormat
.format(i18n
.getString("errorLaunchingShellText"),
759 // Setup the scroll bars
760 onResize(new TResizeEvent(TResizeEvent
.Type
.WIDGET
, getWidth(),
763 // Claim the keystrokes the emulator will need.
767 newStatusBar(i18n
.getString("statusBarRunning"));
769 // Pass the correct text cell width/height to the emulator
770 emulator
.setTextWidth(getScreen().getTextWidth());
771 emulator
.setTextHeight(getScreen().getTextHeight());
775 * Terminate the child of the 'script' process used on POSIX. This may
778 private void terminateShellChildProcess() {
780 if (shell
.getClass().getName().equals("java.lang.UNIXProcess")) {
781 /* get the PID on unix/linux systems */
783 Field field
= shell
.getClass().getDeclaredField("pid");
784 field
.setAccessible(true);
785 pid
= field
.getInt(shell
);
786 } catch (Throwable e
) {
787 // SQUASH, this didn't work. Just bail out quietly.
792 // shell.destroy() works successfully at killing this side of
793 // 'script'. But we need to make sure the other side (child
794 // process) is also killed.
795 String
[] cmdKillIt
= {
796 "pkill", "-P", Integer
.toString(pid
)
799 Runtime
.getRuntime().exec(cmdKillIt
);
800 } catch (Throwable e
) {
801 // SQUASH, this didn't work. Just bail out quietly.
808 * Called by emulator when fresh data has come in.
810 public void displayChanged() {
811 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
815 * Function to call to obtain the display width.
817 * @return the number of columns in the display
819 public int getDisplayWidth() {
821 return getWidth() - 2;
827 * Function to call to obtain the display height.
829 * @return the number of rows in the display
831 public int getDisplayHeight() {
833 return getHeight() - 2;
839 * Hook for subclasses to be notified of the shell termination.
841 public void onShellExit() {
845 getApplication().postEvent(new TMenuEvent(TMenu
.MID_REPAINT
));
849 * Copy out variables from the emulator that TTerminal has to expose on
852 private void readEmulatorState() {
853 // Synchronize against the emulator so we don't stomp on its reader
855 synchronized (emulator
) {
856 setHiddenMouse(emulator
.hasHiddenMousePointer());
858 setCursorX(emulator
.getCursorX() + 1);
859 setCursorY(emulator
.getCursorY() + 1
860 + (getHeight() - 2 - emulator
.getHeight())
861 - getVerticalValue());
862 setCursorVisible(emulator
.isCursorVisible());
863 if (getCursorX() > getWidth() - 2) {
864 setCursorVisible(false);
866 if ((getCursorY() > getHeight() - 2) || (getCursorY() < 0)) {
867 setCursorVisible(false);
869 if (emulator
.getScreenTitle().length() > 0) {
870 // Only update the title if the shell is still alive
872 setTitle(emulator
.getScreenTitle());
876 // Check to see if the shell has died.
877 if (!emulator
.isReading() && (shell
!= null)) {
879 int rc
= shell
.exitValue();
880 // The emulator exited on its own, all is fine
881 setTitle(MessageFormat
.format(i18n
.
882 getString("windowTitleCompleted"), getTitle(), rc
));
885 clearShortcutKeypresses();
886 statusBar
.setText(MessageFormat
.format(i18n
.
887 getString("statusBarCompleted"), rc
));
889 } catch (IllegalThreadStateException e
) {
890 // The emulator thread has exited, but the shell Process
891 // hasn't figured that out yet. Do nothing, we will see
892 // this in a future tick.
894 } else if (emulator
.isReading() && (shell
!= null)) {
895 // The shell might be dead, let's check
897 int rc
= shell
.exitValue();
898 // If we got here, the shell died.
899 setTitle(MessageFormat
.format(i18n
.
900 getString("windowTitleCompleted"), getTitle(), rc
));
903 clearShortcutKeypresses();
904 statusBar
.setText(MessageFormat
.format(i18n
.
905 getString("statusBarCompleted"), rc
));
907 } catch (IllegalThreadStateException e
) {
908 // The shell is still running, do nothing.
912 } // synchronized (emulator)
916 * Check if a mouse press/release/motion event coordinate is over the
919 * @param mouse a mouse-based event
920 * @return whether or not the mouse is on the emulator
922 private boolean mouseOnEmulator(final TMouseEvent mouse
) {
924 synchronized (emulator
) {
925 if (!emulator
.isReading()) {
930 if ((mouse
.getAbsoluteX() >= getAbsoluteX() + 1)
931 && (mouse
.getAbsoluteX() < getAbsoluteX() + getWidth() - 1)
932 && (mouse
.getAbsoluteY() >= getAbsoluteY() + 1)
933 && (mouse
.getAbsoluteY() < getAbsoluteY() + getHeight() - 1)
941 * Draw glyphs for a double-width or double-height VT100 cell to two
944 * @param line the line this VT100 cell is in
945 * @param x the X position to draw the left half to
946 * @param y the Y position to draw to
947 * @param cell the cell to draw
949 private void putDoubleWidthCharXY(final DisplayLine line
, final int x
,
950 final int y
, final Cell cell
) {
952 int textWidth
= getScreen().getTextWidth();
953 int textHeight
= getScreen().getTextHeight();
954 boolean cursorBlinkVisible
= true;
956 if (getScreen() instanceof SwingTerminal
) {
957 SwingTerminal terminal
= (SwingTerminal
) getScreen();
958 cursorBlinkVisible
= terminal
.getCursorBlinkVisible();
959 } else if (getScreen() instanceof ECMA48Terminal
) {
960 ECMA48Terminal terminal
= (ECMA48Terminal
) getScreen();
962 if (!terminal
.hasSixel()) {
963 // The backend does not have sixel support, draw this as text
965 putCharXY(x
, y
, cell
);
966 putCharXY(x
+ 1, y
, ' ', cell
);
969 cursorBlinkVisible
= blinkState
;
971 // We don't know how to dray glyphs to this screen, draw them as
972 // text and bail out.
973 putCharXY(x
, y
, cell
);
974 putCharXY(x
+ 1, y
, ' ', cell
);
978 if ((textWidth
!= lastTextWidth
) || (textHeight
!= lastTextHeight
)) {
979 // Screen size has changed, reset all fonts.
980 setupFonts(textHeight
);
981 lastTextWidth
= textWidth
;
982 lastTextHeight
= textHeight
;
984 assert (doubleFont
!= null);
986 BufferedImage image
= null;
987 if (cell
.isBlink() && !cursorBlinkVisible
) {
988 image
= glyphCacheBlink
.get(cell
);
990 image
= glyphCache
.get(cell
);
993 // Generate glyph and draw it to an image.
994 image
= new BufferedImage(textWidth
* 2, textHeight
* 2,
995 BufferedImage
.TYPE_INT_ARGB
);
996 Graphics2D gr2
= image
.createGraphics();
997 gr2
.setFont(doubleFont
);
999 // Draw the background rectangle, then the foreground character.
1000 if (getScreen() instanceof ECMA48Terminal
) {
1001 // BUG: the background color is coming in the same as the
1002 // foreground color. For now, don't draw it.
1004 gr2
.setColor(SwingTerminal
.attrToBackgroundColor(cell
));
1005 gr2
.fillRect(0, 0, image
.getWidth(), image
.getHeight());
1008 || (cell
.isBlink() && cursorBlinkVisible
)
1010 gr2
.setColor(SwingTerminal
.attrToForegroundColor(cell
));
1011 char [] chars
= new char[1];
1012 chars
[0] = cell
.getChar();
1013 gr2
.drawChars(chars
, 0, 1, doubleTextAdjustX
,
1014 (textHeight
* 2) - doubleMaxDescent
+ doubleTextAdjustY
);
1016 if (cell
.isUnderline() && (line
.getDoubleHeight() != 1)) {
1017 gr2
.fillRect(0, textHeight
- 2, textWidth
, 2);
1022 // Now save this generated image, using a new key that will not
1023 // be mutated by invertCell().
1024 Cell key
= new Cell();
1026 if (cell
.isBlink() && !cursorBlinkVisible
) {
1027 glyphCacheBlink
.put(key
, image
);
1029 glyphCache
.put(key
, image
);
1033 // Now that we have the double-wide glyph drawn, copy the right
1034 // pieces of it to the cells.
1035 Cell left
= new Cell();
1036 Cell right
= new Cell();
1040 BufferedImage leftImage
= null;
1041 BufferedImage rightImage
= null;
1042 switch (line
.getDoubleHeight()) {
1044 // Top half double height
1045 leftImage
= image
.getSubimage(0, 0, textWidth
, textHeight
);
1046 rightImage
= image
.getSubimage(textWidth
, 0, textWidth
, textHeight
);
1049 // Bottom half double height
1050 leftImage
= image
.getSubimage(0, textHeight
, textWidth
, textHeight
);
1051 rightImage
= image
.getSubimage(textWidth
, textHeight
,
1052 textWidth
, textHeight
);
1055 // Either single height double-width, or error fallback
1056 BufferedImage wideImage
= new BufferedImage(textWidth
* 2,
1057 textHeight
, BufferedImage
.TYPE_INT_ARGB
);
1058 Graphics2D grWide
= wideImage
.createGraphics();
1059 grWide
.drawImage(image
, 0, 0, wideImage
.getWidth(),
1060 wideImage
.getHeight(), null);
1062 leftImage
= wideImage
.getSubimage(0, 0, textWidth
, textHeight
);
1063 rightImage
= wideImage
.getSubimage(textWidth
, 0, textWidth
,
1067 left
.setImage(leftImage
);
1068 right
.setImage(rightImage
);
1069 putCharXY(x
, y
, left
);
1070 putCharXY(x
+ 1, y
, right
);
1074 * Set up the single and double-width fonts.
1076 * @param fontSize the size of font to request for the single-width font.
1077 * The double-width font will be 2x this value.
1079 private void setupFonts(final int fontSize
) {
1081 ClassLoader loader
= Thread
.currentThread().getContextClassLoader();
1082 InputStream in
= loader
.getResourceAsStream(SwingTerminal
.FONTFILE
);
1083 Font terminusRoot
= Font
.createFont(Font
.TRUETYPE_FONT
, in
);
1084 Font terminusDouble
= terminusRoot
.deriveFont(Font
.PLAIN
,
1086 doubleFont
= terminusDouble
;
1087 } catch (java
.awt
.FontFormatException e
) {
1088 new TExceptionDialog(getApplication(), e
);
1089 doubleFont
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
* 2);
1090 } catch (java
.io
.IOException e
) {
1091 new TExceptionDialog(getApplication(), e
);
1092 doubleFont
= new Font(Font
.MONOSPACED
, Font
.PLAIN
, fontSize
* 2);
1095 // Get font descent.
1096 BufferedImage image
= new BufferedImage(fontSize
* 10, fontSize
* 10,
1097 BufferedImage
.TYPE_INT_ARGB
);
1098 Graphics2D gr
= image
.createGraphics();
1099 gr
.setFont(doubleFont
);
1100 FontMetrics fm
= gr
.getFontMetrics();
1101 doubleMaxDescent
= fm
.getMaxDescent();
1105 // (Re)create the glyph caches.
1106 glyphCache
= new HashMap
<Cell
, BufferedImage
>();
1107 glyphCacheBlink
= new HashMap
<Cell
, BufferedImage
>();
1109 // Special case: the ECMA48 backend needs to have a timer to drive
1111 if (getScreen() instanceof jexer
.backend
.ECMA48Terminal
) {
1112 // Blink every 500 millis.
1114 getApplication().addTimer(millis
, true,
1117 blinkState
= !blinkState
;
1118 getApplication().doRepaint();